1use 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
19pub struct AieosIdentity {
20 #[serde(default)]
22 pub identity: Option<IdentitySection>,
23 #[serde(default)]
25 pub psychology: Option<PsychologySection>,
26 #[serde(default)]
28 pub linguistics: Option<LinguisticsSection>,
29 #[serde(default)]
31 pub motivations: Option<MotivationsSection>,
32 #[serde(default)]
34 pub capabilities: Option<CapabilitiesSection>,
35 #[serde(default)]
37 pub physicality: Option<PhysicalitySection>,
38 #[serde(default)]
40 pub history: Option<HistorySection>,
41 #[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
156pub fn load_aieos_identity(
161 config: &IdentityConfig,
162 workspace_dir: &Path,
163) -> Result<Option<AieosIdentity>> {
164 if config.format != "aieos" {
166 return Ok(None);
167 }
168
169 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 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 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
722pub fn aieos_to_system_prompt(identity: &AieosIdentity) -> String {
727 use std::fmt::Write;
728 let mut prompt = String::new();
729
730 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 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 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 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 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 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 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 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
985pub 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 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 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 let ling = identity.linguistics.unwrap();
1055 assert_eq!(ling.style.unwrap(), "concise");
1056 assert_eq!(ling.catchphrases.unwrap().len(), 2);
1057
1058 let mot = identity.motivations.unwrap();
1060 assert_eq!(mot.core_drive.unwrap(), "Help users accomplish their goals");
1061
1062 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 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 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 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}