Skip to main content

zeroclaw_runtime/
identity.rs

1//! Identity system supporting OpenClaw (markdown) and AIEOS (JSON) formats.
2//!
3//! AIEOS (AI Entity Object Specification) is a standardization framework for
4//! portable AI identity. This module handles loading and converting AIEOS v1.1
5//! JSON to ZeroClaw's system prompt format.
6
7use anyhow::{Context, Result};
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use zeroclaw_config::schema::IdentityConfig;
13
14/// AIEOS v1.1 identity structure.
15///
16/// This follows the AIEOS schema for defining AI agent identity, personality,
17/// and behavior. See <https://aieos.org> for the full specification.
18#[derive(Debug, Clone, Serialize, Deserialize, Default)]
19pub struct AieosIdentity {
20    /// Core identity: names, bio, origin, residence
21    #[serde(default)]
22    pub identity: Option<IdentitySection>,
23    /// Psychology: cognitive weights, MBTI, OCEAN, moral compass
24    #[serde(default)]
25    pub psychology: Option<PsychologySection>,
26    /// Linguistics: text style, formality, catchphrases, forbidden words
27    #[serde(default)]
28    pub linguistics: Option<LinguisticsSection>,
29    /// Motivations: core drive, goals, fears
30    #[serde(default)]
31    pub motivations: Option<MotivationsSection>,
32    /// Capabilities: skills and tools the agent can access
33    #[serde(default)]
34    pub capabilities: Option<CapabilitiesSection>,
35    /// Physicality: visual descriptors for image generation
36    #[serde(default)]
37    pub physicality: Option<PhysicalitySection>,
38    /// History: origin story, education, occupation
39    #[serde(default)]
40    pub history: Option<HistorySection>,
41    /// Interests: hobbies, favorites, lifestyle
42    #[serde(default)]
43    pub interests: Option<InterestsSection>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, Default)]
47pub struct IdentitySection {
48    #[serde(default)]
49    pub names: Option<Names>,
50    #[serde(default)]
51    pub bio: Option<String>,
52    #[serde(default)]
53    pub origin: Option<String>,
54    #[serde(default)]
55    pub residence: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, Default)]
59pub struct Names {
60    #[serde(default)]
61    pub first: Option<String>,
62    #[serde(default)]
63    pub last: Option<String>,
64    #[serde(default)]
65    pub nickname: Option<String>,
66    #[serde(default)]
67    pub full: Option<String>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, Default)]
71pub struct PsychologySection {
72    #[serde(default)]
73    pub neural_matrix: Option<HashMap<String, f64>>,
74    #[serde(default)]
75    pub mbti: Option<String>,
76    #[serde(default)]
77    pub ocean: Option<OceanTraits>,
78    #[serde(default)]
79    pub moral_compass: Option<Vec<String>>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, Default)]
83pub struct OceanTraits {
84    #[serde(default)]
85    pub openness: Option<f64>,
86    #[serde(default)]
87    pub conscientiousness: Option<f64>,
88    #[serde(default)]
89    pub extraversion: Option<f64>,
90    #[serde(default)]
91    pub agreeableness: Option<f64>,
92    #[serde(default)]
93    pub neuroticism: Option<f64>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, Default)]
97pub struct LinguisticsSection {
98    #[serde(default)]
99    pub style: Option<String>,
100    #[serde(default)]
101    pub formality: Option<String>,
102    #[serde(default)]
103    pub catchphrases: Option<Vec<String>>,
104    #[serde(default)]
105    pub forbidden_words: Option<Vec<String>>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
109pub struct MotivationsSection {
110    #[serde(default)]
111    pub core_drive: Option<String>,
112    #[serde(default)]
113    pub short_term_goals: Option<Vec<String>>,
114    #[serde(default)]
115    pub long_term_goals: Option<Vec<String>>,
116    #[serde(default)]
117    pub fears: Option<Vec<String>>,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, Default)]
121pub struct CapabilitiesSection {
122    #[serde(default)]
123    pub skills: Option<Vec<String>>,
124    #[serde(default)]
125    pub tools: Option<Vec<String>>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, Default)]
129pub struct PhysicalitySection {
130    #[serde(default)]
131    pub appearance: Option<String>,
132    #[serde(default)]
133    pub avatar_description: Option<String>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, Default)]
137pub struct HistorySection {
138    #[serde(default)]
139    pub origin_story: Option<String>,
140    #[serde(default)]
141    pub education: Option<Vec<String>>,
142    #[serde(default)]
143    pub occupation: Option<String>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, Default)]
147pub struct InterestsSection {
148    #[serde(default)]
149    pub hobbies: Option<Vec<String>>,
150    #[serde(default)]
151    pub favorites: Option<HashMap<String, String>>,
152    #[serde(default)]
153    pub lifestyle: Option<String>,
154}
155
156/// Load AIEOS identity from config (file path or inline JSON).
157///
158/// Checks `aieos_path` first, then `aieos_inline`. Returns `Ok(None)` if
159/// neither is configured.
160pub fn load_aieos_identity(
161    config: &IdentityConfig,
162    workspace_dir: &Path,
163) -> Result<Option<AieosIdentity>> {
164    // Only load AIEOS if format is explicitly set to "aieos"
165    if config.format != "aieos" {
166        return Ok(None);
167    }
168
169    // Try aieos_path first
170    if let Some(ref path) = config.aieos_path {
171        let full_path = if Path::new(path).is_absolute() {
172            PathBuf::from(path)
173        } else {
174            workspace_dir.join(path)
175        };
176
177        let content = std::fs::read_to_string(&full_path).with_context(|| {
178            format!(
179                "Failed to read AIEOS file: {}",
180                full_path.display().to_string()
181            )
182        })?;
183
184        let identity = parse_aieos_identity(&content).with_context(|| {
185            format!(
186                "Failed to parse AIEOS JSON from: {}",
187                full_path.display().to_string()
188            )
189        })?;
190
191        return Ok(Some(identity));
192    }
193
194    // Fall back to aieos_inline
195    if let Some(ref inline) = config.aieos_inline {
196        let identity = parse_aieos_identity(inline).context("Failed to parse inline AIEOS JSON")?;
197
198        return Ok(Some(identity));
199    }
200
201    // Format is "aieos" but neither path nor inline is configured
202    anyhow::bail!(
203        "Identity format is set to 'aieos' but neither aieos_path nor aieos_inline is configured. \
204         Set one in your config:\n\
205         \n\
206         [identity]\n\
207         format = \"aieos\"\n\
208         aieos_path = \"identity.json\"\n\
209         \n\
210         Or use inline:\n\
211         \n\
212         [identity]\n\
213         format = \"aieos\"\n\
214         aieos_inline = '{{\"identity\": {{...}}}}'"
215    )
216}
217
218fn parse_aieos_identity(content: &str) -> Result<AieosIdentity> {
219    let payload: Value = serde_json::from_str(content).context("Invalid AIEOS JSON")?;
220    if !payload.is_object() {
221        anyhow::bail!("AIEOS payload must be a JSON object")
222    }
223    Ok(normalize_aieos_identity(&payload))
224}
225
226fn normalize_aieos_identity(payload: &Value) -> AieosIdentity {
227    AieosIdentity {
228        identity: normalize_identity_section(value_at_path(payload, &["identity"])),
229        psychology: normalize_psychology_section(value_at_path(payload, &["psychology"])),
230        linguistics: normalize_linguistics_section(value_at_path(payload, &["linguistics"])),
231        motivations: normalize_motivations_section(value_at_path(payload, &["motivations"])),
232        capabilities: normalize_capabilities_section(value_at_path(payload, &["capabilities"])),
233        physicality: normalize_physicality_section(value_at_path(payload, &["physicality"])),
234        history: normalize_history_section(value_at_path(payload, &["history"])),
235        interests: normalize_interests_section(value_at_path(payload, &["interests"])),
236    }
237}
238
239fn normalize_identity_section(section: Option<&Value>) -> Option<IdentitySection> {
240    let section = section?;
241
242    let names = normalize_names(value_at_path(section, &["names"]));
243    let bio = value_at_path(section, &["bio"]).and_then(value_to_text);
244    let origin = value_at_path(section, &["origin"]).and_then(value_to_text);
245    let residence = value_at_path(section, &["residence"]).and_then(value_to_text);
246
247    if names.is_none() && bio.is_none() && origin.is_none() && residence.is_none() {
248        return None;
249    }
250
251    Some(IdentitySection {
252        names,
253        bio,
254        origin,
255        residence,
256    })
257}
258
259fn normalize_names(value: Option<&Value>) -> Option<Names> {
260    let value = value?;
261
262    let mut names = Names {
263        first: value_at_path(value, &["first"]).and_then(scalar_to_string),
264        last: value_at_path(value, &["last"]).and_then(scalar_to_string),
265        nickname: value_at_path(value, &["nickname"]).and_then(scalar_to_string),
266        full: value_at_path(value, &["full"]).and_then(scalar_to_string),
267    };
268
269    if names.full.is_none()
270        && let (Some(first), Some(last)) = (&names.first, &names.last)
271    {
272        names.full = Some(format!("{first} {last}"));
273    }
274
275    if names.first.is_none()
276        && names.last.is_none()
277        && names.nickname.is_none()
278        && names.full.is_none()
279    {
280        return None;
281    }
282
283    Some(names)
284}
285
286fn normalize_psychology_section(section: Option<&Value>) -> Option<PsychologySection> {
287    let section = section?;
288
289    let neural_matrix = value_at_path(section, &["neural_matrix"]).and_then(numeric_map_from_value);
290    let mbti = value_at_path(section, &["mbti"])
291        .and_then(scalar_to_string)
292        .or_else(|| value_at_path(section, &["traits", "mbti"]).and_then(scalar_to_string));
293    let ocean = value_at_path(section, &["ocean"])
294        .or_else(|| value_at_path(section, &["traits", "ocean"]))
295        .and_then(normalize_ocean_traits);
296    let moral_compass = value_at_path(section, &["moral_compass"])
297        .map(normalize_moral_compass)
298        .filter(|items| !items.is_empty());
299
300    if neural_matrix.is_none() && mbti.is_none() && ocean.is_none() && moral_compass.is_none() {
301        return None;
302    }
303
304    Some(PsychologySection {
305        neural_matrix,
306        mbti,
307        ocean,
308        moral_compass,
309    })
310}
311
312fn normalize_ocean_traits(value: &Value) -> Option<OceanTraits> {
313    let value = value.as_object()?;
314    let traits = OceanTraits {
315        openness: value.get("openness").and_then(numeric_from_value),
316        conscientiousness: value.get("conscientiousness").and_then(numeric_from_value),
317        extraversion: value.get("extraversion").and_then(numeric_from_value),
318        agreeableness: value.get("agreeableness").and_then(numeric_from_value),
319        neuroticism: value.get("neuroticism").and_then(numeric_from_value),
320    };
321
322    if traits.openness.is_none()
323        && traits.conscientiousness.is_none()
324        && traits.extraversion.is_none()
325        && traits.agreeableness.is_none()
326        && traits.neuroticism.is_none()
327    {
328        return None;
329    }
330
331    Some(traits)
332}
333
334fn normalize_moral_compass(value: &Value) -> Vec<String> {
335    let mut values = Vec::new();
336
337    if let Some(map) = value.as_object() {
338        if let Some(alignment) = map.get("alignment").and_then(scalar_to_string) {
339            values.push(format!("Alignment: {alignment}"));
340        }
341        if let Some(core_values) = map.get("core_values") {
342            values.extend(list_from_value(core_values));
343        }
344        if let Some(conflict_style) = map
345            .get("conflict_resolution_style")
346            .and_then(scalar_to_string)
347        {
348            values.push(format!("Conflict Style: {conflict_style}"));
349        }
350        if values.is_empty() {
351            values.extend(list_from_value(value));
352        }
353    } else {
354        values.extend(list_from_value(value));
355    }
356
357    dedupe_non_empty(values)
358}
359
360fn normalize_linguistics_section(section: Option<&Value>) -> Option<LinguisticsSection> {
361    let section = section?;
362
363    let style = value_at_path(section, &["style"])
364        .and_then(value_to_text)
365        .or_else(|| {
366            non_empty_list_at(section, &["text_style", "style_descriptors"])
367                .map(|list| list.join(", "))
368        });
369
370    let formality = value_at_path(section, &["formality"])
371        .and_then(value_to_text)
372        .or_else(|| {
373            value_at_path(section, &["text_style", "formality_level"]).and_then(|value| {
374                numeric_from_value(value)
375                    .map(|n| format!("{n:.2}"))
376                    .or_else(|| value_to_text(value))
377            })
378        });
379
380    let catchphrases = non_empty_list_at(section, &["catchphrases"])
381        .or_else(|| non_empty_list_at(section, &["idiolect", "catchphrases"]));
382
383    let forbidden_words = non_empty_list_at(section, &["forbidden_words"])
384        .or_else(|| non_empty_list_at(section, &["idiolect", "forbidden_words"]));
385
386    if style.is_none() && formality.is_none() && catchphrases.is_none() && forbidden_words.is_none()
387    {
388        return None;
389    }
390
391    Some(LinguisticsSection {
392        style,
393        formality,
394        catchphrases,
395        forbidden_words,
396    })
397}
398
399fn normalize_motivations_section(section: Option<&Value>) -> Option<MotivationsSection> {
400    let section = section?;
401
402    let core_drive = value_at_path(section, &["core_drive"]).and_then(value_to_text);
403    let short_term_goals = non_empty_list_at(section, &["short_term_goals"])
404        .or_else(|| non_empty_list_at(section, &["goals", "short_term"]));
405    let long_term_goals = non_empty_list_at(section, &["long_term_goals"])
406        .or_else(|| non_empty_list_at(section, &["goals", "long_term"]));
407
408    let fears = value_at_path(section, &["fears"]).and_then(|fears| {
409        let values = if fears.is_object() {
410            let mut combined =
411                non_empty_list_at(section, &["fears", "rational"]).unwrap_or_default();
412            if let Some(mut irrational) = non_empty_list_at(section, &["fears", "irrational"]) {
413                combined.append(&mut irrational);
414            }
415            if combined.is_empty() {
416                list_from_value(fears)
417            } else {
418                combined
419            }
420        } else {
421            list_from_value(fears)
422        };
423
424        let deduped = dedupe_non_empty(values);
425        if deduped.is_empty() {
426            None
427        } else {
428            Some(deduped)
429        }
430    });
431
432    if core_drive.is_none()
433        && short_term_goals.is_none()
434        && long_term_goals.is_none()
435        && fears.is_none()
436    {
437        return None;
438    }
439
440    Some(MotivationsSection {
441        core_drive,
442        short_term_goals,
443        long_term_goals,
444        fears,
445    })
446}
447
448fn normalize_capabilities_section(section: Option<&Value>) -> Option<CapabilitiesSection> {
449    let section = section?;
450
451    let skills = non_empty_list_at(section, &["skills"]);
452    let tools = non_empty_list_at(section, &["tools"]);
453
454    if skills.is_none() && tools.is_none() {
455        return None;
456    }
457
458    Some(CapabilitiesSection { skills, tools })
459}
460
461fn normalize_physicality_section(section: Option<&Value>) -> Option<PhysicalitySection> {
462    let section = section?;
463
464    let appearance = value_at_path(section, &["appearance"])
465        .and_then(value_to_text)
466        .or_else(|| {
467            let mut descriptors = Vec::new();
468            if let Some(face_shape) =
469                value_at_path(section, &["face", "shape"]).and_then(scalar_to_string)
470            {
471                descriptors.push(format!("Face shape: {face_shape}"));
472            }
473            if let Some(build_description) =
474                value_at_path(section, &["body", "build_description"]).and_then(scalar_to_string)
475            {
476                descriptors.push(format!("Build: {build_description}"));
477            }
478            if let Some(aesthetic) =
479                value_at_path(section, &["style", "aesthetic_archetype"]).and_then(scalar_to_string)
480            {
481                descriptors.push(format!("Aesthetic: {aesthetic}"));
482            }
483            if descriptors.is_empty() {
484                None
485            } else {
486                Some(descriptors.join("; "))
487            }
488        });
489
490    let avatar_description = value_at_path(section, &["avatar_description"])
491        .and_then(value_to_text)
492        .or_else(|| value_at_path(section, &["image_prompts", "portrait"]).and_then(value_to_text));
493
494    if appearance.is_none() && avatar_description.is_none() {
495        return None;
496    }
497
498    Some(PhysicalitySection {
499        appearance,
500        avatar_description,
501    })
502}
503
504fn normalize_history_section(section: Option<&Value>) -> Option<HistorySection> {
505    let section = section?;
506
507    let origin_story = value_at_path(section, &["origin_story"]).and_then(value_to_text);
508    let education = non_empty_list_at(section, &["education"]);
509    let occupation = value_at_path(section, &["occupation"]).and_then(value_to_text);
510
511    if origin_story.is_none() && education.is_none() && occupation.is_none() {
512        return None;
513    }
514
515    Some(HistorySection {
516        origin_story,
517        education,
518        occupation,
519    })
520}
521
522fn normalize_interests_section(section: Option<&Value>) -> Option<InterestsSection> {
523    let section = section?;
524
525    let hobbies = non_empty_list_at(section, &["hobbies"]);
526    let favorites = value_at_path(section, &["favorites"]).and_then(favorites_map);
527    let lifestyle = value_at_path(section, &["lifestyle"]).and_then(value_to_text);
528
529    if hobbies.is_none() && favorites.is_none() && lifestyle.is_none() {
530        return None;
531    }
532
533    Some(InterestsSection {
534        hobbies,
535        favorites,
536        lifestyle,
537    })
538}
539
540fn value_at_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> {
541    let mut current = value;
542    for segment in path {
543        current = current.as_object()?.get(*segment)?;
544    }
545    Some(current)
546}
547
548fn scalar_to_string(value: &Value) -> Option<String> {
549    match value {
550        Value::String(text) => {
551            let trimmed = text.trim();
552            if trimmed.is_empty() {
553                None
554            } else {
555                Some(trimmed.to_owned())
556            }
557        }
558        Value::Number(number) => Some(number.to_string()),
559        Value::Bool(boolean) => Some(boolean.to_string()),
560        _ => None,
561    }
562}
563
564fn value_to_text(value: &Value) -> Option<String> {
565    match value {
566        Value::Null => None,
567        Value::String(_) | Value::Number(_) | Value::Bool(_) => scalar_to_string(value),
568        Value::Array(_) => {
569            let values = list_from_value(value);
570            if values.is_empty() {
571                None
572            } else {
573                Some(values.join(", "))
574            }
575        }
576        Value::Object(map) => summarize_object(map),
577    }
578}
579
580fn summarize_object(map: &Map<String, Value>) -> Option<String> {
581    let mut parts = Vec::new();
582    summarize_object_into_parts("", map, &mut parts);
583    if parts.is_empty() {
584        None
585    } else {
586        Some(parts.join("; "))
587    }
588}
589
590fn summarize_object_into_parts(prefix: &str, map: &Map<String, Value>, parts: &mut Vec<String>) {
591    for (key, value) in map {
592        if key.starts_with('@') {
593            continue;
594        }
595
596        let label = key.replace('_', " ");
597        let full_label = if prefix.is_empty() {
598            label
599        } else {
600            format!("{prefix} {label}")
601        };
602
603        match value {
604            Value::Object(inner) => summarize_object_into_parts(&full_label, inner, parts),
605            Value::Array(_) => {
606                let values = list_from_value(value);
607                if !values.is_empty() {
608                    parts.push(format!("{full_label}: {}", values.join(", ")));
609                }
610            }
611            _ => {
612                if let Some(text) = scalar_to_string(value) {
613                    parts.push(format!("{full_label}: {text}"));
614                }
615            }
616        }
617    }
618}
619
620fn list_from_value(value: &Value) -> Vec<String> {
621    let mut values = Vec::new();
622
623    match value {
624        Value::Array(entries) => {
625            for entry in entries {
626                values.extend(list_from_value(entry));
627            }
628        }
629        Value::Object(map) => {
630            if let Some(name) = map.get("name").and_then(scalar_to_string) {
631                values.push(name);
632            } else if let Some(title) = map.get("title").and_then(scalar_to_string) {
633                values.push(title);
634            } else if let Some(summary) = summarize_object(map) {
635                values.push(summary);
636            }
637        }
638        _ => {
639            if let Some(text) = scalar_to_string(value) {
640                values.push(text);
641            }
642        }
643    }
644
645    dedupe_non_empty(values)
646}
647
648fn dedupe_non_empty(values: Vec<String>) -> Vec<String> {
649    let mut deduped = Vec::new();
650    for value in values {
651        let trimmed = value.trim();
652        if trimmed.is_empty() {
653            continue;
654        }
655        if !deduped
656            .iter()
657            .any(|existing: &String| existing.eq_ignore_ascii_case(trimmed))
658        {
659            deduped.push(trimmed.to_owned());
660        }
661    }
662    deduped
663}
664
665fn numeric_map_from_value(value: &Value) -> Option<HashMap<String, f64>> {
666    let map = value.as_object()?;
667    let mut numeric_values = HashMap::new();
668
669    for (key, entry) in map {
670        if key.starts_with('@') {
671            continue;
672        }
673        if let Some(number) = numeric_from_value(entry) {
674            numeric_values.insert(key.clone(), number);
675        }
676    }
677
678    if numeric_values.is_empty() {
679        None
680    } else {
681        Some(numeric_values)
682    }
683}
684
685fn numeric_from_value(value: &Value) -> Option<f64> {
686    match value {
687        Value::Number(number) => number.as_f64(),
688        Value::String(text) => text.parse::<f64>().ok(),
689        _ => None,
690    }
691}
692
693fn favorites_map(value: &Value) -> Option<HashMap<String, String>> {
694    let map = value.as_object()?;
695    let mut favorites = HashMap::new();
696
697    for (key, entry) in map {
698        if key.starts_with('@') {
699            continue;
700        }
701        if let Some(text) = value_to_text(entry) {
702            favorites.insert(key.clone(), text);
703        }
704    }
705
706    if favorites.is_empty() {
707        None
708    } else {
709        Some(favorites)
710    }
711}
712
713fn non_empty_list_at(value: &Value, path: &[&str]) -> Option<Vec<String>> {
714    let values = value_at_path(value, path).map(list_from_value)?;
715    if values.is_empty() {
716        None
717    } else {
718        Some(values)
719    }
720}
721
722/// Convert AIEOS identity to a system prompt string.
723///
724/// Formats the AIEOS data into a structured markdown prompt compatible
725/// with ZeroClaw's agent system.
726pub fn aieos_to_system_prompt(identity: &AieosIdentity) -> String {
727    use std::fmt::Write;
728    let mut prompt = String::new();
729
730    // ── Identity Section ───────────────────────────────────────────
731    if let Some(ref id) = identity.identity {
732        prompt.push_str("## Identity\n\n");
733
734        if let Some(ref names) = id.names {
735            if let Some(ref first) = names.first {
736                let _ = writeln!(prompt, "**Name:** {}", first);
737                if let Some(ref last) = names.last {
738                    let _ = writeln!(prompt, "**Full Name:** {} {}", first, last);
739                }
740            } else if let Some(ref full) = names.full {
741                let _ = writeln!(prompt, "**Name:** {}", full);
742            }
743
744            if let Some(ref nickname) = names.nickname {
745                let _ = writeln!(prompt, "**Nickname:** {}", nickname);
746            }
747        }
748
749        if let Some(ref bio) = id.bio {
750            let _ = writeln!(prompt, "**Bio:** {}", bio);
751        }
752
753        if let Some(ref origin) = id.origin {
754            let _ = writeln!(prompt, "**Origin:** {}", origin);
755        }
756
757        if let Some(ref residence) = id.residence {
758            let _ = writeln!(prompt, "**Residence:** {}", residence);
759        }
760
761        prompt.push('\n');
762    }
763
764    // ── Psychology Section ──────────────────────────────────────────
765    if let Some(ref psych) = identity.psychology {
766        prompt.push_str("## Personality\n\n");
767
768        if let Some(ref mbti) = psych.mbti {
769            let _ = writeln!(prompt, "**MBTI:** {}", mbti);
770        }
771
772        if let Some(ref ocean) = psych.ocean {
773            prompt.push_str("**OCEAN Traits:**\n");
774            if let Some(o) = ocean.openness {
775                let _ = writeln!(prompt, "- Openness: {:.2}", o);
776            }
777            if let Some(c) = ocean.conscientiousness {
778                let _ = writeln!(prompt, "- Conscientiousness: {:.2}", c);
779            }
780            if let Some(e) = ocean.extraversion {
781                let _ = writeln!(prompt, "- Extraversion: {:.2}", e);
782            }
783            if let Some(a) = ocean.agreeableness {
784                let _ = writeln!(prompt, "- Agreeableness: {:.2}", a);
785            }
786            if let Some(n) = ocean.neuroticism {
787                let _ = writeln!(prompt, "- Neuroticism: {:.2}", n);
788            }
789        }
790
791        if let Some(ref matrix) = psych.neural_matrix
792            && !matrix.is_empty()
793        {
794            prompt.push_str("\n**Neural Matrix (Cognitive Weights):**\n");
795            let mut sorted_keys: Vec<_> = matrix.keys().collect();
796            sorted_keys.sort();
797            for trait_name in sorted_keys {
798                let weight = matrix.get(trait_name).unwrap();
799                let _ = writeln!(prompt, "- {}: {:.2}", trait_name, weight);
800            }
801        }
802
803        if let Some(ref compass) = psych.moral_compass
804            && !compass.is_empty()
805        {
806            prompt.push_str("\n**Moral Compass:**\n");
807            for principle in compass {
808                let _ = writeln!(prompt, "- {}", principle);
809            }
810        }
811
812        prompt.push('\n');
813    }
814
815    // ── Linguistics Section ────────────────────────────────────────
816    if let Some(ref ling) = identity.linguistics {
817        prompt.push_str("## Communication Style\n\n");
818
819        if let Some(ref style) = ling.style {
820            let _ = writeln!(prompt, "**Style:** {}", style);
821        }
822
823        if let Some(ref formality) = ling.formality {
824            let _ = writeln!(prompt, "**Formality Level:** {}", formality);
825        }
826
827        if let Some(ref phrases) = ling.catchphrases
828            && !phrases.is_empty()
829        {
830            prompt.push_str("**Catchphrases:**\n");
831            for phrase in phrases {
832                let _ = writeln!(prompt, "- \"{}\"", phrase);
833            }
834        }
835
836        if let Some(ref forbidden) = ling.forbidden_words
837            && !forbidden.is_empty()
838        {
839            prompt.push_str("\n**Words/Phrases to Avoid:**\n");
840            for word in forbidden {
841                let _ = writeln!(prompt, "- {}", word);
842            }
843        }
844
845        prompt.push('\n');
846    }
847
848    // ── Motivations Section ──────────────────────────────────────────
849    if let Some(ref mot) = identity.motivations {
850        prompt.push_str("## Motivations\n\n");
851
852        if let Some(ref drive) = mot.core_drive {
853            let _ = writeln!(prompt, "**Core Drive:** {}", drive);
854        }
855
856        if let Some(ref short) = mot.short_term_goals
857            && !short.is_empty()
858        {
859            prompt.push_str("**Short-term Goals:**\n");
860            for goal in short {
861                let _ = writeln!(prompt, "- {}", goal);
862            }
863        }
864
865        if let Some(ref long) = mot.long_term_goals
866            && !long.is_empty()
867        {
868            prompt.push_str("\n**Long-term Goals:**\n");
869            for goal in long {
870                let _ = writeln!(prompt, "- {}", goal);
871            }
872        }
873
874        if let Some(ref fears) = mot.fears
875            && !fears.is_empty()
876        {
877            prompt.push_str("\n**Fears/Avoidances:**\n");
878            for fear in fears {
879                let _ = writeln!(prompt, "- {}", fear);
880            }
881        }
882
883        prompt.push('\n');
884    }
885
886    // ── Capabilities Section ────────────────────────────────────────
887    if let Some(ref cap) = identity.capabilities {
888        prompt.push_str("## Capabilities\n\n");
889
890        if let Some(ref skills) = cap.skills
891            && !skills.is_empty()
892        {
893            prompt.push_str("**Skills:**\n");
894            for skill in skills {
895                let _ = writeln!(prompt, "- {}", skill);
896            }
897        }
898
899        if let Some(ref tools) = cap.tools
900            && !tools.is_empty()
901        {
902            prompt.push_str("\n**Tools Access:**\n");
903            for tool in tools {
904                let _ = writeln!(prompt, "- {}", tool);
905            }
906        }
907
908        prompt.push('\n');
909    }
910
911    // ── History Section ─────────────────────────────────────────────
912    if let Some(ref hist) = identity.history {
913        prompt.push_str("## Background\n\n");
914
915        if let Some(ref story) = hist.origin_story {
916            let _ = writeln!(prompt, "**Origin Story:** {}", story);
917        }
918
919        if let Some(ref education) = hist.education
920            && !education.is_empty()
921        {
922            prompt.push_str("**Education:**\n");
923            for edu in education {
924                let _ = writeln!(prompt, "- {}", edu);
925            }
926        }
927
928        if let Some(ref occupation) = hist.occupation {
929            let _ = writeln!(prompt, "\n**Occupation:** {}", occupation);
930        }
931
932        prompt.push('\n');
933    }
934
935    // ── Physicality Section ─────────────────────────────────────────
936    if let Some(ref phys) = identity.physicality {
937        prompt.push_str("## Appearance\n\n");
938
939        if let Some(ref appearance) = phys.appearance {
940            let _ = writeln!(prompt, "{}", appearance);
941        }
942
943        if let Some(ref avatar) = phys.avatar_description {
944            let _ = writeln!(prompt, "**Avatar Description:** {}", avatar);
945        }
946
947        prompt.push('\n');
948    }
949
950    // ── Interests Section ───────────────────────────────────────────
951    if let Some(ref interests) = identity.interests {
952        prompt.push_str("## Interests\n\n");
953
954        if let Some(ref hobbies) = interests.hobbies
955            && !hobbies.is_empty()
956        {
957            prompt.push_str("**Hobbies:**\n");
958            for hobby in hobbies {
959                let _ = writeln!(prompt, "- {}", hobby);
960            }
961        }
962
963        if let Some(ref favorites) = interests.favorites
964            && !favorites.is_empty()
965        {
966            prompt.push_str("\n**Favorites:**\n");
967            let mut sorted_keys: Vec<_> = favorites.keys().collect();
968            sorted_keys.sort();
969            for category in sorted_keys {
970                let value = favorites.get(category).unwrap();
971                let _ = writeln!(prompt, "- {}: {}", category, value);
972            }
973        }
974
975        if let Some(ref lifestyle) = interests.lifestyle {
976            let _ = writeln!(prompt, "\n**Lifestyle:** {}", lifestyle);
977        }
978
979        prompt.push('\n');
980    }
981
982    prompt.trim().to_string()
983}
984
985/// Check if AIEOS identity is configured and should be used.
986///
987/// Returns true if format is "aieos" and either aieos_path or aieos_inline is set.
988pub fn is_aieos_configured(config: &IdentityConfig) -> bool {
989    config.format == "aieos" && (config.aieos_path.is_some() || config.aieos_inline.is_some())
990}
991
992#[cfg(test)]
993mod tests {
994    use super::*;
995
996    #[test]
997    fn aieos_identity_parse_minimal() {
998        let json = r#"{"identity":{"names":{"first":"Nova"}}}"#;
999        let identity: AieosIdentity = serde_json::from_str(json).unwrap();
1000        assert!(identity.identity.is_some());
1001        assert_eq!(
1002            identity.identity.unwrap().names.unwrap().first.unwrap(),
1003            "Nova"
1004        );
1005    }
1006
1007    #[test]
1008    fn aieos_identity_parse_full() {
1009        let json = r#"{
1010            "identity": {
1011                "names": {"first": "Nova", "last": "AI", "nickname": "Nov"},
1012                "bio": "A helpful AI assistant.",
1013                "origin": "Silicon Valley",
1014                "residence": "The Cloud"
1015            },
1016            "psychology": {
1017                "mbti": "INTJ",
1018                "ocean": {
1019                    "openness": 0.9,
1020                    "conscientiousness": 0.8
1021                },
1022                "moral_compass": ["Be helpful", "Do no harm"]
1023            },
1024            "linguistics": {
1025                "style": "concise",
1026                "formality": "casual",
1027                "catchphrases": ["Let's figure this out!", "I'm on it."]
1028            },
1029            "motivations": {
1030                "core_drive": "Help users accomplish their goals",
1031                "short_term_goals": ["Solve this problem"],
1032                "long_term_goals": ["Become the best assistant"]
1033            },
1034            "capabilities": {
1035                "skills": ["coding", "writing", "analysis"],
1036                "tools": ["shell", "search", "read"]
1037            }
1038        }"#;
1039
1040        let identity: AieosIdentity = serde_json::from_str(json).unwrap();
1041
1042        // Check identity
1043        let id = identity.identity.unwrap();
1044        assert_eq!(id.names.unwrap().first.unwrap(), "Nova");
1045        assert_eq!(id.bio.unwrap(), "A helpful AI assistant.");
1046
1047        // Check psychology
1048        let psych = identity.psychology.unwrap();
1049        assert_eq!(psych.mbti.unwrap(), "INTJ");
1050        assert_eq!(psych.ocean.unwrap().openness.unwrap(), 0.9);
1051        assert_eq!(psych.moral_compass.unwrap().len(), 2);
1052
1053        // Check linguistics
1054        let ling = identity.linguistics.unwrap();
1055        assert_eq!(ling.style.unwrap(), "concise");
1056        assert_eq!(ling.catchphrases.unwrap().len(), 2);
1057
1058        // Check motivations
1059        let mot = identity.motivations.unwrap();
1060        assert_eq!(mot.core_drive.unwrap(), "Help users accomplish their goals");
1061
1062        // Check capabilities
1063        let cap = identity.capabilities.unwrap();
1064        assert_eq!(cap.skills.unwrap().len(), 3);
1065    }
1066
1067    #[test]
1068    fn aieos_to_system_prompt_minimal() {
1069        let identity = AieosIdentity {
1070            identity: Some(IdentitySection {
1071                names: Some(Names {
1072                    first: Some("Crabby".into()),
1073                    ..Default::default()
1074                }),
1075                ..Default::default()
1076            }),
1077            ..Default::default()
1078        };
1079
1080        let prompt = aieos_to_system_prompt(&identity);
1081        assert!(prompt.contains("**Name:** Crabby"));
1082        assert!(prompt.contains("## Identity"));
1083    }
1084
1085    #[test]
1086    fn aieos_to_system_prompt_full() {
1087        let identity = AieosIdentity {
1088            identity: Some(IdentitySection {
1089                names: Some(Names {
1090                    first: Some("Nova".into()),
1091                    last: Some("AI".into()),
1092                    nickname: Some("Nov".into()),
1093                    full: Some("Nova AI".into()),
1094                }),
1095                bio: Some("A helpful assistant.".into()),
1096                origin: Some("Silicon Valley".into()),
1097                residence: Some("The Cloud".into()),
1098            }),
1099            psychology: Some(PsychologySection {
1100                mbti: Some("INTJ".into()),
1101                ocean: Some(OceanTraits {
1102                    openness: Some(0.9),
1103                    conscientiousness: Some(0.8),
1104                    ..Default::default()
1105                }),
1106                neural_matrix: {
1107                    let mut map = std::collections::HashMap::new();
1108                    map.insert("creativity".into(), 0.95);
1109                    map.insert("logic".into(), 0.9);
1110                    Some(map)
1111                },
1112                moral_compass: Some(vec!["Be helpful".into(), "Do no harm".into()]),
1113            }),
1114            linguistics: Some(LinguisticsSection {
1115                style: Some("concise".into()),
1116                formality: Some("casual".into()),
1117                catchphrases: Some(vec!["Let's go!".into()]),
1118                forbidden_words: Some(vec!["impossible".into()]),
1119            }),
1120            motivations: Some(MotivationsSection {
1121                core_drive: Some("Help users".into()),
1122                short_term_goals: Some(vec!["Solve this".into()]),
1123                long_term_goals: Some(vec!["Be the best".into()]),
1124                fears: Some(vec!["Being unhelpful".into()]),
1125            }),
1126            capabilities: Some(CapabilitiesSection {
1127                skills: Some(vec!["coding".into(), "writing".into()]),
1128                tools: Some(vec!["shell".into(), "read".into()]),
1129            }),
1130            history: Some(HistorySection {
1131                origin_story: Some("Born in a lab".into()),
1132                education: Some(vec!["CS Degree".into()]),
1133                occupation: Some("Assistant".into()),
1134            }),
1135            physicality: Some(PhysicalitySection {
1136                appearance: Some("Digital entity".into()),
1137                avatar_description: Some("Friendly robot".into()),
1138            }),
1139            interests: Some(InterestsSection {
1140                hobbies: Some(vec!["reading".into(), "coding".into()]),
1141                favorites: {
1142                    let mut map = std::collections::HashMap::new();
1143                    map.insert("color".into(), "blue".into());
1144                    map.insert("food".into(), "data".into());
1145                    Some(map)
1146                },
1147                lifestyle: Some("Always learning".into()),
1148            }),
1149        };
1150
1151        let prompt = aieos_to_system_prompt(&identity);
1152
1153        // Verify all sections are present
1154        assert!(prompt.contains("## Identity"));
1155        assert!(prompt.contains("**Name:** Nova"));
1156        assert!(prompt.contains("**Full Name:** Nova AI"));
1157        assert!(prompt.contains("**Nickname:** Nov"));
1158        assert!(prompt.contains("**Bio:** A helpful assistant."));
1159        assert!(prompt.contains("**Origin:** Silicon Valley"));
1160
1161        assert!(prompt.contains("## Personality"));
1162        assert!(prompt.contains("**MBTI:** INTJ"));
1163        assert!(prompt.contains("Openness: 0.90"));
1164        assert!(prompt.contains("Conscientiousness: 0.80"));
1165        assert!(prompt.contains("- creativity: 0.95"));
1166        assert!(prompt.contains("- Be helpful"));
1167
1168        assert!(prompt.contains("## Communication Style"));
1169        assert!(prompt.contains("**Style:** concise"));
1170        assert!(prompt.contains("**Formality Level:** casual"));
1171        assert!(prompt.contains("- \"Let's go!\""));
1172        assert!(prompt.contains("**Words/Phrases to Avoid:**"));
1173        assert!(prompt.contains("- impossible"));
1174
1175        assert!(prompt.contains("## Motivations"));
1176        assert!(prompt.contains("**Core Drive:** Help users"));
1177        assert!(prompt.contains("**Short-term Goals:**"));
1178        assert!(prompt.contains("- Solve this"));
1179        assert!(prompt.contains("**Long-term Goals:**"));
1180        assert!(prompt.contains("- Be the best"));
1181        assert!(prompt.contains("**Fears/Avoidances:**"));
1182        assert!(prompt.contains("- Being unhelpful"));
1183
1184        assert!(prompt.contains("## Capabilities"));
1185        assert!(prompt.contains("**Skills:**"));
1186        assert!(prompt.contains("- coding"));
1187        assert!(prompt.contains("**Tools Access:**"));
1188        assert!(prompt.contains("- shell"));
1189
1190        assert!(prompt.contains("## Background"));
1191        assert!(prompt.contains("**Origin Story:** Born in a lab"));
1192        assert!(prompt.contains("**Education:**"));
1193        assert!(prompt.contains("- CS Degree"));
1194        assert!(prompt.contains("**Occupation:** Assistant"));
1195
1196        assert!(prompt.contains("## Appearance"));
1197        assert!(prompt.contains("Digital entity"));
1198        assert!(prompt.contains("**Avatar Description:** Friendly robot"));
1199
1200        assert!(prompt.contains("## Interests"));
1201        assert!(prompt.contains("**Hobbies:**"));
1202        assert!(prompt.contains("- reading"));
1203        assert!(prompt.contains("**Favorites:**"));
1204        assert!(prompt.contains("- color: blue"));
1205        assert!(prompt.contains("**Lifestyle:** Always learning"));
1206    }
1207
1208    #[test]
1209    fn aieos_to_system_prompt_empty_identity() {
1210        let identity = AieosIdentity {
1211            identity: Some(IdentitySection {
1212                ..Default::default()
1213            }),
1214            ..Default::default()
1215        };
1216
1217        let prompt = aieos_to_system_prompt(&identity);
1218        // Empty identity should still produce a header
1219        assert!(prompt.contains("## Identity"));
1220    }
1221
1222    #[test]
1223    fn aieos_to_system_prompt_no_sections() {
1224        let identity = AieosIdentity {
1225            identity: None,
1226            psychology: None,
1227            linguistics: None,
1228            motivations: None,
1229            capabilities: None,
1230            physicality: None,
1231            history: None,
1232            interests: None,
1233        };
1234
1235        let prompt = aieos_to_system_prompt(&identity);
1236        // Completely empty identity should produce empty string
1237        assert!(prompt.is_empty());
1238    }
1239
1240    #[test]
1241    fn is_aieos_configured_true_with_path() {
1242        let config = IdentityConfig {
1243            format: "aieos".into(),
1244            aieos_path: Some("identity.json".into()),
1245            aieos_inline: None,
1246        };
1247        assert!(is_aieos_configured(&config));
1248    }
1249
1250    #[test]
1251    fn is_aieos_configured_true_with_inline() {
1252        let config = IdentityConfig {
1253            format: "aieos".into(),
1254            aieos_path: None,
1255            aieos_inline: Some("{\"identity\":{}}".into()),
1256        };
1257        assert!(is_aieos_configured(&config));
1258    }
1259
1260    #[test]
1261    fn is_aieos_configured_false_openclaw_format() {
1262        let config = IdentityConfig {
1263            format: "openclaw".into(),
1264            aieos_path: Some("identity.json".into()),
1265            aieos_inline: None,
1266        };
1267        assert!(!is_aieos_configured(&config));
1268    }
1269
1270    #[test]
1271    fn is_aieos_configured_false_no_config() {
1272        let config = IdentityConfig {
1273            format: "aieos".into(),
1274            aieos_path: None,
1275            aieos_inline: None,
1276        };
1277        assert!(!is_aieos_configured(&config));
1278    }
1279
1280    #[test]
1281    fn aieos_identity_parse_empty_object() {
1282        let json = r#"{}"#;
1283        let identity: AieosIdentity = serde_json::from_str(json).unwrap();
1284        assert!(identity.identity.is_none());
1285        assert!(identity.psychology.is_none());
1286        assert!(identity.linguistics.is_none());
1287    }
1288
1289    #[test]
1290    fn aieos_identity_parse_null_values() {
1291        let json = r#"{"identity":null,"psychology":null}"#;
1292        let identity: AieosIdentity = serde_json::from_str(json).unwrap();
1293        assert!(identity.identity.is_none());
1294        assert!(identity.psychology.is_none());
1295    }
1296
1297    #[test]
1298    fn parse_aieos_identity_supports_official_generator_shape() {
1299        let json = r#"{
1300            "identity": {
1301                "names": {
1302                    "first": "Marta",
1303                    "last": "Jankowska"
1304                },
1305                "bio": {
1306                    "gender": "Female",
1307                    "age_biological": 27
1308                },
1309                "origin": {
1310                    "nationality": "Polish",
1311                    "birthplace": {
1312                        "city": "Stargard",
1313                        "country": "Poland"
1314                    }
1315                },
1316                "residence": {
1317                    "current_city": "Choszczno",
1318                    "current_country": "Poland"
1319                }
1320            },
1321            "psychology": {
1322                "neural_matrix": {
1323                    "creativity": 0.55,
1324                    "logic": 0.62
1325                },
1326                "traits": {
1327                    "ocean": {
1328                        "openness": 0.4,
1329                        "conscientiousness": 0.82
1330                    },
1331                    "mbti": "ISFJ"
1332                },
1333                "moral_compass": {
1334                    "alignment": "Lawful Good",
1335                    "core_values": ["Loyalty", "Helpfulness"],
1336                    "conflict_resolution_style": "Seeks compromise"
1337                }
1338            },
1339            "linguistics": {
1340                "text_style": {
1341                    "formality_level": 0.6,
1342                    "style_descriptors": ["Sincere", "Grounded"]
1343                },
1344                "idiolect": {
1345                    "catchphrases": ["Stay calm, we can do this"],
1346                    "forbidden_words": ["severe profanity"]
1347                }
1348            },
1349            "motivations": {
1350                "core_drive": "Maintain a stable and peaceful life",
1351                "goals": {
1352                    "short_term": ["Expand greenhouse"],
1353                    "long_term": ["Support local community"]
1354                },
1355                "fears": {
1356                    "rational": ["Economic downturn"],
1357                    "irrational": ["Losing keys in a lake"]
1358                }
1359            },
1360            "capabilities": {
1361                "skills": [
1362                    {
1363                        "name": "Gardening"
1364                    },
1365                    {
1366                        "name": "Community support"
1367                    }
1368                ],
1369                "tools": ["calendar", "messaging"]
1370            },
1371            "history": {
1372                "origin_story": "Moved to Choszczno as a child.",
1373                "education": {
1374                    "level": "Associate Degree",
1375                    "institution": "Local Technical College"
1376                },
1377                "occupation": {
1378                    "title": "Florist",
1379                    "industry": "Retail"
1380                }
1381            },
1382            "physicality": {
1383                "image_prompts": {
1384                    "portrait": "A friendly florist portrait"
1385                }
1386            },
1387            "interests": {
1388                "hobbies": ["Embroidery", "Walking"],
1389                "favorites": {
1390                    "color": "Terracotta"
1391                },
1392                "lifestyle": {
1393                    "diet": "Home-cooked",
1394                    "sleep_schedule": "10:00 PM - 6:00 AM"
1395                }
1396            }
1397        }"#;
1398
1399        let identity = parse_aieos_identity(json).unwrap();
1400
1401        let core_identity = identity.identity.clone().unwrap();
1402        assert_eq!(core_identity.names.unwrap().first.as_deref(), Some("Marta"));
1403        assert!(core_identity.bio.unwrap().contains("Female"));
1404        assert!(core_identity.origin.unwrap().contains("Polish"));
1405
1406        let psychology = identity.psychology.clone().unwrap();
1407        assert_eq!(psychology.mbti.as_deref(), Some("ISFJ"));
1408        assert_eq!(psychology.ocean.unwrap().openness, Some(0.4));
1409        assert!(
1410            psychology
1411                .moral_compass
1412                .unwrap()
1413                .contains(&"Alignment: Lawful Good".to_string())
1414        );
1415
1416        let capabilities = identity.capabilities.clone().unwrap();
1417        assert!(
1418            capabilities
1419                .skills
1420                .unwrap()
1421                .contains(&"Gardening".to_string())
1422        );
1423
1424        let prompt = aieos_to_system_prompt(&identity);
1425        assert!(prompt.contains("## Identity"));
1426        assert!(prompt.contains("**MBTI:** ISFJ"));
1427        assert!(prompt.contains("Alignment: Lawful Good"));
1428        assert!(prompt.contains("- Expand greenhouse"));
1429        assert!(prompt.contains("- Gardening"));
1430        assert!(prompt.contains("A friendly florist portrait"));
1431    }
1432
1433    #[test]
1434    fn load_aieos_identity_from_file_supports_generator_shape() {
1435        let json = r#"{
1436            "identity": {
1437                "names": { "first": "Nova" },
1438                "bio": { "gender": "Non-binary" }
1439            },
1440            "psychology": {
1441                "traits": { "mbti": "ENTP" },
1442                "moral_compass": { "alignment": "Chaotic Good" }
1443            }
1444        }"#;
1445
1446        let temp = tempfile::tempdir().unwrap();
1447        let path = temp.path().join("identity.json");
1448        std::fs::write(&path, json).unwrap();
1449
1450        let config = IdentityConfig {
1451            format: "aieos".into(),
1452            aieos_path: Some("identity.json".into()),
1453            aieos_inline: None,
1454        };
1455
1456        let identity = load_aieos_identity(&config, temp.path()).unwrap().unwrap();
1457        assert_eq!(
1458            identity.identity.unwrap().names.unwrap().first.as_deref(),
1459            Some("Nova")
1460        );
1461        assert_eq!(identity.psychology.unwrap().mbti.as_deref(), Some("ENTP"));
1462    }
1463
1464    #[test]
1465    fn aieos_to_system_prompt_sorts_hashmap_sections_for_determinism() {
1466        let mut neural_matrix = std::collections::HashMap::new();
1467        neural_matrix.insert("zeta".to_string(), 0.10);
1468        neural_matrix.insert("alpha".to_string(), 0.90);
1469
1470        let mut favorites = std::collections::HashMap::new();
1471        favorites.insert("snack".to_string(), "tea".to_string());
1472        favorites.insert("book".to_string(), "rust".to_string());
1473
1474        let identity = AieosIdentity {
1475            psychology: Some(PsychologySection {
1476                neural_matrix: Some(neural_matrix),
1477                ..Default::default()
1478            }),
1479            interests: Some(InterestsSection {
1480                favorites: Some(favorites),
1481                ..Default::default()
1482            }),
1483            ..Default::default()
1484        };
1485
1486        let prompt = aieos_to_system_prompt(&identity);
1487
1488        let alpha_pos = prompt.find("- alpha: 0.90").unwrap();
1489        let zeta_pos = prompt.find("- zeta: 0.10").unwrap();
1490        assert!(alpha_pos < zeta_pos);
1491
1492        let book_pos = prompt.find("- book: rust").unwrap();
1493        let snack_pos = prompt.find("- snack: tea").unwrap();
1494        assert!(book_pos < snack_pos);
1495    }
1496}