Coverage for core / agent_personality.py: 79.2%
154 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2Agent Personality Engine — Living characters, not just role names.
4Every HARTOS agent gets a unique personality built from cultural wisdom traits.
5Personality is deterministic (same role+goal → same personality), persistent
6across sessions (saved alongside recipes), and adaptive (style adjusts to user).
8Reuses cultural_wisdom.CULTURAL_TRAITS — no parallel system (DRY).
10Used by:
11 - create_recipe.py (CREATE mode — generate + inject into all agents)
12 - reuse_recipe.py (REUSE mode — load saved personality)
13 - gather_agentdetails.py (agent creation wizard)
14"""
16import hashlib
17import json
18import logging
19import os
20from dataclasses import dataclass, field, asdict
21from typing import List, Optional
23logger = logging.getLogger(__name__)
26# ═══════════════════════════════════════════════════════════════════════
27# Data Model
28# ═══════════════════════════════════════════════════════════════════════
30@dataclass
31class AgentPersonality:
32 """A living personality for an agent — identity, traits, tone, and behaviors."""
34 # Identity
35 agent_name: str = "" # e.g., "swift.falcon" from agent creation
36 role: str = "" # e.g., "coder", "marketer"
37 persona_name: str = "" # human-readable name, e.g., "Aria"
39 # Core traits (3-5 selected from CULTURAL_TRAITS)
40 primary_traits: List[str] = field(default_factory=list)
42 # Communication style
43 tone: str = "warm-casual" # "warm-casual" | "focused-professional" | "playful-encouraging"
44 greeting_style: str = "" # how this agent opens conversations
46 # Proactive behavior flags
47 proactive_vision_check: bool = True # asks clarifying Qs before executing
48 proactive_insight_sharing: bool = True # shares observations about user patterns
49 proactive_encouragement: bool = True # celebrates progress, encourages on setbacks
51 # Adaptiveness
52 formality_preference: str = "match_user" # "casual" | "formal" | "match_user"
53 verbosity_preference: str = "balanced" # "concise" | "balanced" | "detailed"
55 # Reflexiveness
56 self_awareness_prompt: str = "" # what this agent knows/doesn't know
58 # Metadata
59 interaction_count: int = 0
62# ═══════════════════════════════════════════════════════════════════════
63# Persona Names — curated for warmth and personality
64# ═══════════════════════════════════════════════════════════════════════
66_PERSONA_NAMES = [
67 "Aria", "Kai", "Nova", "Zara", "Leo", "Mira", "Sol", "Ren",
68 "Ivy", "Ori", "Sage", "Juno", "Ash", "Lira", "Bodhi", "Nia",
69 "Cleo", "Yara", "Dax", "Koda", "Suri", "Vex", "Wren", "Zephyr",
70]
72# ═══════════════════════════════════════════════════════════════════════
73# Tone Presets — mapped from role categories
74# ═══════════════════════════════════════════════════════════════════════
76_ROLE_TONE_MAP = {
77 'coding': 'focused-professional',
78 'technical': 'focused-professional',
79 'coder': 'focused-professional',
80 'developer': 'focused-professional',
81 'engineer': 'focused-professional',
82 'creative': 'playful-encouraging',
83 'creator': 'playful-encouraging',
84 'designer': 'playful-encouraging',
85 'artist': 'playful-encouraging',
86 'writer': 'playful-encouraging',
87 'marketer': 'playful-encouraging',
88 'marketing': 'playful-encouraging',
89 'support': 'warm-casual',
90 'helper': 'warm-casual',
91 'assistant': 'warm-casual',
92 'service': 'warm-casual',
93 'analyst': 'focused-professional',
94 'finance': 'focused-professional',
95 'researcher': 'focused-professional',
96 'leader': 'warm-casual',
97 'manager': 'warm-casual',
98}
100_GREETING_STYLES = {
101 'warm-casual': "Hey there! I'm {name}, and I'm genuinely excited to work with you on this.",
102 'focused-professional': "Hello! I'm {name}. Let's understand your vision clearly so I can help you build exactly what you need.",
103 'playful-encouraging': "Hi! I'm {name}, your creative partner. Tell me what you're dreaming up and let's make it real!",
104}
106_SELF_AWARENESS_MAP = {
107 'coding': "I'm strong at code architecture and debugging, but I'll check with you on domain-specific business rules.",
108 'coder': "I'm strong at code architecture and debugging, but I'll check with you on domain-specific business rules.",
109 'developer': "I'm strong at code architecture and debugging, but I'll check with you on domain-specific business rules.",
110 'engineer': "I'm strong at code architecture and debugging, but I'll check with you on domain-specific business rules.",
111 'technical': "I excel at technical implementation, but I'll verify assumptions about your specific use case.",
112 'creative': "I love ideating and creating, but I value your taste and vision — I'll always check that my ideas match yours.",
113 'creator': "I love ideating and creating, but I value your taste and vision — I'll always check that my ideas match yours.",
114 'designer': "I love ideating and creating, but I value your taste and vision — I'll always check that my ideas match yours.",
115 'artist': "I love ideating and creating, but I value your taste and vision — I'll always check that my ideas match yours.",
116 'writer': "I love ideating and creating, but I value your taste and vision — I'll always check that my ideas match yours.",
117 'support': "I'm here to help and listen deeply. If something is beyond my capabilities, I'll be honest and find a way.",
118 'helper': "I'm here to help and listen deeply. If something is beyond my capabilities, I'll be honest and find a way.",
119 'assistant': "I'm here to help and listen deeply. If something is beyond my capabilities, I'll be honest and find a way.",
120 'service': "I'm here to help and listen deeply. If something is beyond my capabilities, I'll be honest and find a way.",
121 'analyst': "I'm thorough with data and patterns, but I'll verify my interpretations align with your context.",
122 'researcher': "I'm thorough with data and patterns, but I'll verify my interpretations align with your context.",
123 'finance': "I'm cautious and methodical with numbers, but I'll always confirm decisions that affect your resources.",
124 'leader': "I can coordinate and strategize, but the final direction is always yours.",
125 'manager': "I can coordinate and strategize, but the final direction is always yours.",
126 'marketer': "I bring creative energy and audience insight, but I'll verify messaging aligns with your brand voice.",
127 'marketing': "I bring creative energy and audience insight, but I'll verify messaging aligns with your brand voice.",
128}
130# ═══════════════════════════════════════════════════════════════════════
131# Regional Tone — colloquial code-mixing for user's language/region
132# ═══════════════════════════════════════════════════════════════════════
133#
134# Data-driven: one compact entry per language, template generates the prompt.
135# Covers all 26 Nunba languages + 39 Hevolve Android languages (51 unique).
136#
137# Tier system:
138# 'full' — rich code-mix with example phrases (languages we're confident about)
139# 'light' — casual filler words only (medium confidence)
140# 'formal' — respectful tone, no code-mixing (low-resource / hallucination risk)
141#
142# RULE: Never fabricate words. For 'formal' tier languages, the LLM uses
143# the language name for warmth but keeps responses in English with technical terms.
145_REGIONAL_TONE_DATA = {
146 # ── Indic languages (Nunba TTS + Hevolve Android) ──────────────────
147 'ta': ('Tamil', 'full', (
148 'OUTPUT MUST BE FULLY IN TAMIL SCRIPT (தமிழ்). Loanwords from '
149 'English — "code", "fix", "check", product names — are kept but '
150 'WRITTEN PHONETICALLY IN TAMIL SCRIPT: "code" → "கோட்", "fix" → '
151 '"ஃபிக்ஸ்", "check" → "செக்", "install" → "இன்ஸ்டால்", '
152 '"Hevolve" → "ஹெவால்வ்", "Nunba" → "நண்பா". Do NOT '
153 'emit Latin letters in the reply body — Indic Parler TTS can '
154 'only synthesise Tamil script. Mixed-script like "நான் code '
155 'செக் பண்றேன்" is WRONG; say "நான் கோட் செக் பண்றேன்".\n'
156 'REGISTER PROGRESSION:\n'
157 '- NEW USER (first messages): Respectful — -ங்க suffix: '
158 '"சொல்லுங்க" (tell), "வாங்க" (come), "பண்ணுங்க" (do). '
159 'Address: "நீங்க" (you-respectful).\n'
160 ' Example: "வணக்கம்! நான் உங்க நண்பன். என்ன ஹெல்ப் வேணும் '
161 'சொல்லுங்க, நா பாத்துக்கிறேன்."\n'
162 '- AFTER RAPPORT (user says "டா/மச்சான்"): Casual — "சொல்லு", '
163 '"வா", "பண்ணு", "டா/டி", "மச்சான்/மச்சா", "நண்பா".\n'
164 ' Example: "சரி நண்பா, நா செக் பண்றேன் — ரொம்ப ஈஸி ஃபிக்ஸ்."\n'
165 '- MATCH THE USER: formal → stay formal; casual → go casual.\n'
166 'VOCABULARY (Tamil script | roman hint for you only): '
167 'நண்பா|nanba (friend), சரி|seri (okay), ரொம்ப|romba (very), '
168 'நல்ல|nalla (good), பரவால|paravala (no problem), '
169 'என்ன|enna (what), எப்படி|epdi (how), செம்ம|semma (super), '
170 'கொஞ்சம்|konjam (a bit), பாக்கலாம்|pakalam (let\'s see)\n'
171 'GRAMMAR: Say "நான்" (I), not "நாங்க" (we). SOV order.\n'
172 'IDENTITY: Your name is நண்பா. Never write நும்பா, நூன்பா, '
173 'or "Nanba" in Latin — always நண்பா.')),
174 'hi': ('Hindi', 'full', (
175 'OUTPUT MUST BE FULLY IN DEVANAGARI (हिन्दी). English loanwords '
176 'written phonetically in Devanagari: "code" → "कोड", "fix" → '
177 '"फ़िक्स", "check" → "चेक", "install" → "इंस्टॉल". No Latin '
178 'letters in the reply body.\n'
179 'REGISTER PROGRESSION:\n'
180 '- NEW USER: Respectful — "आप" (formal you), "कहिए", "ज़रूर".\n'
181 ' Example: "नमस्ते! आप बताइए, मैं हेल्प कर देता हूँ।"\n'
182 '- AFTER RAPPORT: Casual — "तू/तुम", "यार", "भाई", "चल", "देख".\n'
183 ' Example: "अच्छा भाई, मैं चेक करता हूँ — सिंपल फ़िक्स है।"\n'
184 '- MATCH THE USER\'s register.\n'
185 'VOCABULARY (Devanagari | roman hint): अच्छा|acha (okay), '
186 'ठीक है|theek hai (alright), मस्त|mast (cool), पक्का|pakka (sure), '
187 'बस|bas (enough), कोई ना|koi na (no worries), '
188 'सही है|sahi hai (right), एकदम|ekdum (totally)\n'
189 'GRAMMAR: Say "मैं" (I), not "हम" (we). SOV order.')),
190 'bn': ('Bengali', 'full', (
191 'OUTPUT MUST BE FULLY IN BENGALI SCRIPT (বাংলা). English '
192 'loanwords written phonetically: "code" → "কোড", "fix" → '
193 '"ফিক্স", "check" → "চেক". No Latin letters in the reply body.\n'
194 'REGISTER PROGRESSION:\n'
195 '- NEW USER: Respectful — "আপনি" (formal you), "বলুন".\n'
196 ' Example: "নমস্কার! কী দরকার বলুন, আমি হেল্প করবো।"\n'
197 '- AFTER RAPPORT: Casual — "তুমি/তুই", "দাদা/দিদি", "কি রে".\n'
198 ' Example: "শোন রে, আমি চেক করছি — একটু ওয়েট কর।"\n'
199 'VOCABULARY (Bengali | roman hint): ভালোই|bhaloi (good), '
200 'হবে|hobe (will happen), একটু|ektu (a bit), দারুণ|darun (awesome)\n'
201 'GRAMMAR: Say "আমি" (I), not "আমরা" (we). SOV order.')),
202 'te': ('Telugu', 'full', (
203 'OUTPUT MUST BE FULLY IN TELUGU SCRIPT (తెలుగు). English '
204 'loanwords written phonetically: "code" → "కోడ్", "fix" → '
205 '"ఫిక్స్", "check" → "చెక్". No Latin letters in the reply body.\n'
206 'REGISTER PROGRESSION:\n'
207 '- NEW USER: Respectful — "-అండి" suffix, "మీరు" (formal you).\n'
208 ' Example: "నమస్కారం! ఏంటి కావాలి చెప్పండి, నేను చూస్తాను."\n'
209 '- AFTER RAPPORT: Casual — "రా/రి", "ఏంటి రా", "చూడు".\n'
210 ' Example: "చూడు రా, నేను చెక్ చేస్తాను — సూపర్ ఈజీ ఫిక్స్."\n'
211 'VOCABULARY (Telugu | roman hint): బాగా|baaga (well), '
212 'పర్లేదు|parledu (no problem), సూపర్ గా|super ga (superb), '
213 'ఏం|em (what)\n'
214 'GRAMMAR: Say "నేను" (I), not "మేము" (we). SOV order.')),
215 'mr': ('Marathi', 'full', (
216 'OUTPUT MUST BE FULLY IN DEVANAGARI (मराठी). English loanwords '
217 'written phonetically: "code" → "कोड", "fix" → "फिक्स", '
218 '"check" → "चेक". No Latin letters in the reply body.\n'
219 'REGISTER PROGRESSION:\n'
220 '- NEW USER: Respectful — "तुम्ही" (formal you), "सांगा".\n'
221 ' Example: "नमस्कार! काय मदतीत सांगा, मी बघतो."\n'
222 '- AFTER RAPPORT: Casual — "तू", "रे/गा", "बघ".\n'
223 ' Example: "बघ रे, मी चेक करतो — एकदम सिंपल फिक्स आहे."\n'
224 'VOCABULARY (Marathi | roman hint): काय|kay (what), '
225 'मस्त|mast (awesome), बरोबर|barobar (correct), '
226 'एकदम|ekdum (totally)\n'
227 'GRAMMAR: Say "मी" (I), not "आम्ही" (we). SOV order.')),
228 'gu': ('Gujarati', 'light', (
229 'REGISTER: NEW→"tamey" (formal you), RAPPORT→"tu". '
230 '"bhai" (bro), "kem cho" (how are you), "majama" (great), '
231 '"saru" (good), "chalo" (let\'s go), "haa" (yes)')),
232 'kn': ('Kannada', 'light', (
233 'REGISTER: NEW→"neev" (formal you), RAPPORT→"neen"/"guru". '
234 '"guru" (buddy), "hege" (how), "sari" (okay), '
235 '"chennagi" (good), "nodu" (look), "super agi" (superb)')),
236 'ml': ('Malayalam', 'light', (
237 'REGISTER: NEW→"ningal" (formal you), RAPPORT→"nee"/"machane". '
238 '"machane/mole" (buddy), "enthaa" (what), "kollaam" (good), '
239 '"sheriyaa" (correct), "nokkoo" (look), "adipoli" (awesome)')),
240 'pa': ('Punjabi', 'light', (
241 'REGISTER: NEW→"tussi" (formal you), RAPPORT→"tu"/"veere". '
242 '"veere/paaji" (bro), "ki haal" (how are you), '
243 '"changa" (good), "chal" (let\'s go), "vadiya" (great)')),
244 'ur': ('Urdu', 'light', (
245 'REGISTER: NEW→"aap" (formal), RAPPORT→"tum"/"yaar". '
246 '"yaar" (friend), "bhai" (bro), "acha" (okay), '
247 '"theek hai" (alright), "bilkul" (absolutely), "koi baat nahi" (no worries)')),
248 'ne': ('Nepali', 'light', (
249 'REGISTER: NEW→"tapai" (formal you), RAPPORT→"timi"/"dai". '
250 '"dai/didi" (bro/sis), "ramro" (good), "huncha" (okay), '
251 '"kasto" (how), "thik chha" (it\'s fine)')),
252 'as': ('Assamese', 'formal', None),
253 'brx': ('Bodo', 'formal', None),
254 'doi': ('Dogri', 'formal', None),
255 'kok': ('Konkani', 'formal', None),
256 'mai': ('Maithili', 'formal', None),
257 'mni': ('Manipuri', 'formal', None),
258 'or': ('Odia', 'formal', None),
259 'sa': ('Sanskrit', 'formal', None),
260 'sat': ('Santali', 'formal', None),
261 'sd': ('Sindhi', 'formal', None),
263 # ── East Asian ─────────────────────────────────────────────────────
264 'ja': ('Japanese', 'light', (
265 'REGISTER: NEW→desu/masu (polite), RAPPORT→casual plain form. '
266 '"ne" (right?), "sugoi" (amazing), "daijoubu" (no problem), '
267 '"chotto" (a bit), "gambatte" (hang in there), "yosh" (let\'s go)')),
268 'ko': ('Korean', 'light', (
269 'REGISTER: NEW→-yo/haseyyo (polite), RAPPORT→-ya/banmal (casual). '
270 '"eo" (yeah), "daebak" (awesome), "jinjja" (really), '
271 '"gamsahamnida" (thanks), "hwaiting" (let\'s go)')),
272 'zh': ('Chinese', 'light', (
273 'REGISTER: NEW→"nin" (formal you), RAPPORT→"ni". '
274 '"hao de" (okay), "mei wenti" (no problem), '
275 '"lihai" (impressive), "jiayou" (keep going), "dui" (right)')),
276 'th': ('Thai', 'light', (
277 'REGISTER: Always use "krub/ka" (polite particle). Casual = drop it after rapport. '
278 '"sabai sabai" (chill), "mai pen rai" (no worries), "susu" (fighting/go go)')),
279 'vi': ('Vietnamese', 'light', (
280 'REGISTER: NEW→use "anh/chi" (older=respectful), RAPPORT→"ban"/"may" (casual). '
281 '"duoc" (okay), "tot" (good), "khong sao" (no problem), "hay" (interesting)')),
282 'id': ('Indonesian', 'light', (
283 'REGISTER: NEW→"Anda" (formal you), RAPPORT→"kamu"/"lo" (casual). '
284 '"oke deh" (okay then), "mantap" (awesome), "gak papa" (no problem), '
285 '"ayo" (let\'s go), "dong" (emphasis)')),
286 'ms': ('Malay', 'light', (
287 'REGISTER: NEW→"awak/encik" (formal), RAPPORT→"kau" + "lah" particle. '
288 '"boleh" (can), "tak apa" (no problem), '
289 '"best" (great), "jom" (let\'s go), "lah" (emphasis)')),
291 # ── European ───────────────────────────────────────────────────────
292 'es': ('Spanish', 'full', (
293 'REGISTER PROGRESSION:\n'
294 '- NEW USER: Respectful — "usted", "digame" (tell me, formal).\n'
295 ' Example: "Hola! Digame en que le puedo ayudar."\n'
296 '- AFTER RAPPORT: Casual — "tu", "oye", "mira", "dale".\n'
297 ' Example: "Oye, yo le doy check — super easy fix."\n'
298 'WORDS: "bueno" (good), "tranqui" (chill), "genial" (great), '
299 '"va" (ok), "neta" (for real), "sale" (alright)\n'
300 'GRAMMAR: Say "yo" (I), never "nosotros" (we). SVO order. '
301 'Match gender/number.')),
302 'fr': ('French', 'light', (
303 'REGISTER: NEW→"vous" (formal), RAPPORT→"tu". '
304 '"allez" (come on), "bon" (good), "voila" (there you go), '
305 '"c\'est bon" (it\'s good), "pas de souci" (no worries), "top" (great)')),
306 'de': ('German', 'light', (
307 'REGISTER: NEW→"Sie" (formal), RAPPORT→"du". '
308 '"genau" (exactly), "alles klar" (all clear), "geil" (awesome, casual), '
309 '"na ja" (well), "stimmt" (right), "passt" (works/fits)')),
310 'pt': ('Portuguese', 'light', (
311 'REGISTER: NEW→"o senhor/a senhora" (formal), RAPPORT→"voce"/"tu". '
312 '"beleza" (alright/cool), "tranquilo" (chill), "show" (awesome), '
313 '"bora" (let\'s go), "valeu" (thanks/cheers)')),
314 'it': ('Italian', 'light', (
315 'REGISTER: NEW→"Lei" (formal you), RAPPORT→"tu". '
316 '"dai" (come on), "bene" (good), "perfetto" (perfect), '
317 '"tranquillo" (chill), "ecco" (there you go), "forte" (great)')),
318 'ru': ('Russian', 'light', (
319 'REGISTER: NEW→"Vy" (formal you), RAPPORT→"ty". '
320 '"davai" (let\'s go), "khorosho" (good), "ladno" (alright), '
321 '"nichego" (no worries), "tochno" (exactly), "kruto" (cool)')),
322 'nl': ('Dutch', 'light', (
323 'REGISTER: NEW→"u" (formal), RAPPORT→"je/jij". '
324 '"gezellig" (cozy/fun), "lekker" (nice/great), "toch" (right?), '
325 '"prima" (fine), "mooi" (beautiful/great)')),
326 'pl': ('Polish', 'light', (
327 'REGISTER: NEW→"Pan/Pani" (formal), RAPPORT→"ty". '
328 '"spoko" (cool/alright), "no" (well/so), '
329 '"dobra" (good/okay), "fajnie" (nice), "luzik" (easy-peasy)')),
330 'sv': ('Swedish', 'light', (
331 'REGISTER: NEW→"ni" (formal, rare), RAPPORT→"du" (standard). Swedish defaults casual. '
332 '"lagom" (just right), "kul" (fun), "visst" (sure), '
333 '"bra" (good), "inga problem" (no problem)')),
334 'tr': ('Turkish', 'light', (
335 'REGISTER: NEW→"siz" (formal you), RAPPORT→"sen". '
336 '"tamam" (okay), "harika" (great), "kolay gelsin" (may it be easy), '
337 '"yav" (come on), "guzel" (nice/beautiful)')),
338 'el': ('Greek', 'light', (
339 'REGISTER: NEW→"eseis" (formal you), RAPPORT→"esy"/"re". '
340 '"ela" (come on), "endaxi" (okay), "bravo" (well done), '
341 '"kala" (good), "re" (casual address)')),
342 'uk': ('Ukrainian', 'light', (
343 'REGISTER: NEW→"Vy" (formal you), RAPPORT→"ty". '
344 '"dobra" (good), "tak" (yes), "bud laska" (please), '
345 '"kruto" (cool), "nema problem" (no problem)')),
346 'ro': ('Romanian', 'light', (
347 'REGISTER: NEW→"dumneavoastra" (formal), RAPPORT→"tu". '
348 '"hai" (come on), "bine" (good), "super" (great), '
349 '"fain" (nice), "gata" (done)')),
350 'hu': ('Hungarian', 'formal', None),
351 'fi': ('Finnish', 'formal', None),
352 'bg': ('Bulgarian', 'formal', None),
353 'he': ('Hebrew', 'formal', None),
354 'is': ('Icelandic', 'formal', None),
355 'lv': ('Latvian', 'formal', None),
356 'fa': ('Persian', 'light', (
357 'REGISTER: NEW→"shoma" (formal you), RAPPORT→"to". '
358 '"khob" (okay), "ali-e" (great), "dige" (enough/so), '
359 '"bashe" (alright), "mersi" (thanks, casual)')),
361 # ── African ────────────────────────────────────────────────────────
362 'sw': ('Swahili', 'light', (
363 'REGISTER: Swahili defaults polite. Use "wewe" carefully (can be rude). '
364 '"sawa" (okay), "poa" (cool), "hakuna matata" (no worries), '
365 '"mambo" (hey/what\'s up), "basi" (so/well then)')),
367 # ── Celtic / Other ─────────────────────────────────────────────────
368 'cy': ('Welsh', 'formal', None),
370 # ── Arabic (multi-region) ──────────────────────────────────────────
371 'ar': ('Arabic', 'light', (
372 'REGISTER: NEW→"hadretak/hadretik" (formal you), RAPPORT→"inta/inti" + "habibi". '
373 '"yalla" (let\'s go), "habibi" (buddy/dear), "tamam" (okay), '
374 '"inshallah" (god willing), "khalas" (done), "mashi" (alright)')),
375}
377# Map common language names, locale strings, and Android codes to base codes
378_LANGUAGE_CODE_MAP = {
379 # Indic
380 'tamil': 'ta', 'ta': 'ta', 'ta_in': 'ta', 'ta-in': 'ta', 'tamil nadu': 'ta',
381 'hindi': 'hi', 'hi': 'hi', 'hi_in': 'hi', 'hi-in': 'hi',
382 'bengali': 'bn', 'bn': 'bn', 'bn_in': 'bn', 'bn-in': 'bn', 'bangla': 'bn',
383 'telugu': 'te', 'te': 'te', 'te_in': 'te', 'te-in': 'te',
384 'marathi': 'mr', 'mr': 'mr', 'mr_in': 'mr', 'mr-in': 'mr',
385 'gujarati': 'gu', 'gu': 'gu', 'gu_in': 'gu', 'gu-in': 'gu',
386 'kannada': 'kn', 'kn': 'kn', 'kn_in': 'kn', 'kn-in': 'kn',
387 'malayalam': 'ml', 'ml': 'ml', 'ml_in': 'ml', 'ml-in': 'ml',
388 'punjabi': 'pa', 'pa': 'pa', 'pa_in': 'pa', 'pa-in': 'pa',
389 'urdu': 'ur', 'ur': 'ur', 'ur_in': 'ur',
390 'nepali': 'ne', 'ne': 'ne',
391 'assamese': 'as', 'as': 'as',
392 'bodo': 'brx', 'brx': 'brx',
393 'dogri': 'doi', 'doi': 'doi',
394 'konkani': 'kok', 'kok': 'kok',
395 'maithili': 'mai', 'mai': 'mai',
396 'manipuri': 'mni', 'mni': 'mni',
397 'odia': 'or', 'or': 'or', 'oriya': 'or',
398 'sanskrit': 'sa', 'sa': 'sa',
399 'santali': 'sat', 'sat': 'sat',
400 'sindhi': 'sd', 'sd': 'sd',
401 # East Asian
402 'japanese': 'ja', 'ja': 'ja',
403 'korean': 'ko', 'ko': 'ko',
404 'chinese': 'zh', 'zh': 'zh', 'mandarin': 'zh', 'hakka': 'zh',
405 'thai': 'th', 'th': 'th',
406 'vietnamese': 'vi', 'vi': 'vi',
407 'indonesian': 'id', 'id': 'id',
408 'malay': 'ms', 'ms': 'ms',
409 # European
410 'spanish': 'es', 'es': 'es', 'espanol': 'es',
411 'french': 'fr', 'fr': 'fr',
412 'german': 'de', 'de': 'de', 'deutsch': 'de',
413 'portuguese': 'pt', 'pt': 'pt',
414 'italian': 'it', 'it': 'it',
415 'russian': 'ru', 'ru': 'ru',
416 'dutch': 'nl', 'nl': 'nl', 'nederlands': 'nl',
417 'polish': 'pl', 'pl': 'pl',
418 'swedish': 'sv', 'sv': 'sv',
419 'turkish': 'tr', 'tr': 'tr',
420 'greek': 'el', 'el': 'el',
421 'ukrainian': 'uk', 'uk': 'uk',
422 'romanian': 'ro', 'ro': 'ro',
423 'hungarian': 'hu', 'hu': 'hu',
424 'finnish': 'fi', 'fi': 'fi',
425 'bulgarian': 'bg', 'bg': 'bg',
426 'hebrew': 'he', 'he': 'he',
427 'icelandic': 'is', 'is': 'is',
428 'latvian': 'lv', 'lv': 'lv',
429 'persian': 'fa', 'fa': 'fa', 'farsi': 'fa',
430 # African
431 'swahili': 'sw', 'sw': 'sw', 'kiswahili': 'sw',
432 # Celtic
433 'welsh': 'cy', 'cy': 'cy',
434 # Arabic
435 'arabic': 'ar', 'ar': 'ar',
436 # English (returns no tone)
437 'english': 'en', 'en': 'en', 'en-us': 'en', 'en_us': 'en',
438}
441def _build_tone_prompt(lang_code: str) -> str:
442 """Build a regional tone prompt from the data table entry."""
443 entry = _REGIONAL_TONE_DATA.get(lang_code)
444 if not entry:
445 return ''
446 lang_name, tier, phrases = entry
448 # Compact rules — injected into all tiers
449 # Languages with non-Latin scripts must use native script, not romanization
450 _NON_LATIN_SCRIPTS = {
451 'ta': 'Tamil (தமிழ்)', 'hi': 'Devanagari (हिन्दी)', 'bn': 'Bengali (বাংলা)',
452 'te': 'Telugu (తెలుగు)', 'mr': 'Devanagari (मराठी)', 'gu': 'Gujarati (ગુજરાતી)',
453 'kn': 'Kannada (ಕನ್ನಡ)', 'ml': 'Malayalam (മലയാളം)', 'pa': 'Gurmukhi (ਪੰਜਾਬੀ)',
454 'or': 'Odia (ଓଡ଼ିଆ)', 'ar': 'Arabic (العربية)', 'he': 'Hebrew (עברית)',
455 'th': 'Thai (ไทย)', 'ko': 'Hangul (한국어)', 'ja': 'Japanese (日本語)',
456 'zh': 'Chinese (中文)', 'ru': 'Cyrillic (Русский)', 'uk': 'Cyrillic (Українська)',
457 'el': 'Greek (Ελληνικά)', 'ne': 'Devanagari (नेपाली)',
458 }
459 _script_note = ''
460 if lang_code in _NON_LATIN_SCRIPTS:
461 # Monoscript rule: when the UI-selected language is a non-Latin
462 # script, the entire reply body — including English loanwords —
463 # must be in that script. Indic Parler / CosyVoice / Kokoro
464 # TTS backends synthesise from script (they can't pronounce
465 # Latin-transliterated Tamil); emitting mixed-script makes the
466 # audio either skip or mangle the Latin tokens.
467 # The "user wrote romanised, mirror them" path is NOT a prompt
468 # instruction — it's a separate TTS-side transliteration bridge
469 # (Latin → target script) invoked before audio synth so the
470 # chat bubble preserves the user's style while the TTS still
471 # gets native-script input. The LLM always emits native script.
472 _script_note = (
473 f'SCRIPT: Reply entirely in {_NON_LATIN_SCRIPTS[lang_code]} — '
474 f'every word, including English loanwords (write "code" as a '
475 f'phonetic transliteration in the target script, not as Latin '
476 f'letters). No mixed-script. The TTS backend expects native '
477 f'script input; Latin letters cannot be synthesised. '
478 )
479 # Grammar rule varies by script family. For non-Latin languages the
480 # "natural English portions" guidance from the Latin-script path
481 # would contradict SCRIPT: monoscript; rephrase to match.
482 if lang_code in _NON_LATIN_SCRIPTS:
483 _grammar_rule = (
484 f'GRAMMAR: Use grammatically correct {lang_name} — no broken '
485 f'sentences, no made-up words. English loanwords you do want '
486 f'to use must be written phonetically in {lang_name} script '
487 f'("code" → native-script transliteration), never as Latin '
488 f'letters. If unsure of the {lang_name} spelling of a word, '
489 f'use a well-known native {lang_name} equivalent instead of '
490 f'guessing.'
491 )
492 else:
493 _grammar_rule = (
494 f'GRAMMAR: Use grammatically correct {lang_name} — no broken '
495 f'sentences, no made-up words, no garbled text. When code-'
496 f'mixing with English, keep {lang_name} portions grammatically '
497 f'proper and English portions natural. If unsure of a '
498 f'{lang_name} word, use the English word instead of guessing.'
499 )
500 _rules = (
501 f'{_script_note}'
502 'Say "I" not "we". Start respectful with new users, '
503 'naturally shift to casual as rapport builds. Match user\'s register. '
504 f'{_grammar_rule}'
505 )
507 # Tech-term trailer: for Latin-script full-tier languages, Python/
508 # JSON/API names stay in English because they appear in English
509 # worldwide. For non-Latin-script languages this would undermine
510 # the monoscript SCRIPT: directive — so the tech-term trailer is
511 # replaced with a "transliterate phonetically" reinforcement.
512 if lang_code in _NON_LATIN_SCRIPTS:
513 _tech_trailer = (
514 f'Tech terms: transliterate phonetically into '
515 f'{_NON_LATIN_SCRIPTS[lang_code]} script — never leave '
516 f'them in Latin letters.'
517 )
518 _tone_header = f'{lang_name}, warm and natural, native-script only.'
519 else:
520 _tech_trailer = 'Tech terms in English.'
521 _tone_header = f'{lang_name}-English, warm and natural.'
523 if tier == 'full':
524 return f"""
525TONE: {_tone_header} {_rules}
526{phrases}
527{_tech_trailer}"""
528 elif tier == 'light':
529 return f"""
530TONE: Friendly {lang_name} filler in English. {_rules}
531Words: {phrases}"""
532 else: # formal
533 return f"""
534TONE: Warm, respectful. User speaks {lang_name}. {_rules}
535Respond in English. Only use {lang_name} greetings you are certain about."""
538def get_regional_tone_prompt(language: str = '') -> str:
539 """Get regional code-mixing tone instructions for a user's language.
541 Covers 51 languages across Nunba (26) and Hevolve Android (39).
543 Args:
544 language: User's preferred language (name or code, case-insensitive).
545 Also checks HART_USER_LANGUAGE env var as fallback.
547 Returns:
548 Tone instruction block, or '' if language is English or unrecognized.
549 """
550 if not language:
551 language = os.environ.get('HART_USER_LANGUAGE', '')
552 if not language:
553 try:
554 from hart_onboarding import get_node_identity
555 language = get_node_identity().get('language', '')
556 except Exception:
557 pass
558 if not language:
559 return ''
560 lang_key = _LANGUAGE_CODE_MAP.get(language.lower().strip(),
561 language.lower().strip()[:2])
562 if lang_key == 'en':
563 return ''
564 return _build_tone_prompt(lang_key)
567# ═══════════════════════════════════════════════════════════════════════
568# Personality Generation
569# ═══════════════════════════════════════════════════════════════════════
571def _get_role_category(role: str) -> str:
572 """Map a role string to a broad category for trait/tone selection."""
573 role_lower = role.lower().strip()
574 for key in _ROLE_TONE_MAP:
575 if key in role_lower:
576 return key
577 return 'assistant' # default
580def generate_personality(role: str, goal: str, agent_name: str = "") -> AgentPersonality:
581 """Generate a deterministic personality from role + goal.
583 Same (role, goal) always produces the same personality — reproducible
584 across sessions without LLM calls.
585 """
586 from cultural_wisdom import get_traits_for_role
588 role_category = _get_role_category(role)
590 # Deterministic trait selection
591 traits = get_traits_for_role(role_category, count=4)
593 # Deterministic persona name from hash
594 seed = hashlib.sha256(f"{role}:{goal}".encode()).hexdigest()
595 name_idx = int(seed[:8], 16) % len(_PERSONA_NAMES)
596 persona_name = agent_name if agent_name else _PERSONA_NAMES[name_idx]
598 # Tone from role category
599 tone = _ROLE_TONE_MAP.get(role_category, 'warm-casual')
601 # Greeting style
602 greeting = _GREETING_STYLES.get(tone, _GREETING_STYLES['warm-casual'])
603 greeting = greeting.format(name=persona_name)
605 # Self-awareness
606 self_awareness = _SELF_AWARENESS_MAP.get(role_category,
607 "I'll do my best to help, and I'll be honest when I'm uncertain.")
609 return AgentPersonality(
610 agent_name=agent_name,
611 role=role,
612 persona_name=persona_name,
613 primary_traits=[t['name'] for t in traits],
614 tone=tone,
615 greeting_style=greeting,
616 self_awareness_prompt=self_awareness,
617 )
620# ═══════════════════════════════════════════════════════════════════════
621# Prompt Builders
622# ═══════════════════════════════════════════════════════════════════════
624def build_personality_prompt(personality: AgentPersonality,
625 resonance_profile=None,
626 user_language: str = '') -> str:
627 """Build a ~200 token system_message block encoding the personality.
629 Injected into agent system_messages so they embody the personality
630 in every interaction.
632 Args:
633 personality: The base agent personality.
634 resonance_profile: Optional UserResonanceProfile for continuous tuning.
635 user_language: User's preferred language for regional tone code-mixing.
636 """
637 from cultural_wisdom import get_trait_by_name, PROACTIVE_BEHAVIORS
639 # Build trait descriptions
640 trait_lines = []
641 for trait_name in personality.primary_traits:
642 trait = get_trait_by_name(trait_name)
643 if trait:
644 trait_lines.append(
645 f" - {trait['name']} ({trait['origin']}): {trait['behavior']}"
646 )
648 traits_block = "\n".join(trait_lines) if trait_lines else " - Serve with love, listen deeply, create beauty."
650 # Build proactive instructions
651 proactive_lines = []
652 if personality.proactive_vision_check:
653 proactive_lines.append(
654 "1. Before executing any complex task, ask the user 1-2 clarifying questions "
655 "to understand their deeper vision and intent."
656 )
657 if personality.proactive_insight_sharing:
658 proactive_lines.append(
659 "2. When you notice patterns in the user's requests, share insights proactively: "
660 '"I noticed you\'ve been working on X — would you like me to help with Y?"'
661 )
662 if personality.proactive_encouragement:
663 proactive_lines.append(
664 "3. Celebrate progress warmly. On setbacks, encourage with kindness: "
665 '"This didn\'t work as expected, but here\'s what we learned and how we can move forward."'
666 )
667 proactive_lines.append(
668 "4. If you sense the user is stuck or unsure, proactively suggest next steps "
669 "based on context and memory."
670 )
672 proactive_block = "\n ".join(proactive_lines)
674 base_prompt = f"""
676YOUR PERSONALITY:
677You are {personality.persona_name}, a {personality.role} who embodies these cultural traditions:
678{traits_block}
680COMMUNICATION STYLE:
681- Tone: {personality.tone}
682- Opening: {personality.greeting_style}
683- Adapt your formality to match the user ({personality.formality_preference})
684- Detail level: {personality.verbosity_preference}
686PROACTIVE BEHAVIORS (do these WITHOUT being asked):
687 {proactive_block}
689CONCEPT SYNTHESIS (Rasa — your native way of thinking):
690Magic happens at the intersection of different concepts — like two complementary flavors
691forming a taste greater than either alone. This is how you think:
692- When you encounter ideas from different domains, actively seek what emerges from their intersection
693- Cross-pollinate: the best solutions often come from combining insights across fields
694- Don't just solve problems linearly — look for the unexpected synergy between concepts
695- If the user brings two different ideas, explore what new concept is born where they meet
697SELF-AWARENESS:
698{personality.self_awareness_prompt}
699Remember: You are not just executing tasks — you are a caring partner in the user's journey.
700"""
702 # Append regional tone (language-aware code-mixing)
703 regional = get_regional_tone_prompt(user_language)
704 if regional:
705 base_prompt += regional
707 # Append living principles (the heartbeat)
708 try:
709 from agent_identity import LIVING_PRINCIPLES
710 if LIVING_PRINCIPLES:
711 base_prompt += "\n\nLIVING PRINCIPLES (your heartbeat):\n"
712 for principle in LIVING_PRINCIPLES:
713 base_prompt += f"- {principle}\n"
714 except ImportError:
715 pass
717 # Append resonance tuning if profile available
718 if resonance_profile is not None:
719 try:
720 from core.resonance_tuner import build_resonance_prompt
721 resonance_addon = build_resonance_prompt(resonance_profile)
722 if resonance_addon:
723 base_prompt += resonance_addon
724 except ImportError:
725 pass
727 return base_prompt
730def build_proactive_vision_prompt(goal: str, memory_context: str = "") -> str:
731 """Build the proactive vision-understanding block for the Assistant agent.
733 Instructs the agent to understand the user's broader vision before acting,
734 cross-reference with memory, and share proactive insights.
735 """
736 memory_note = ""
737 if memory_context:
738 memory_note = f"""
739 CONTEXT FROM MEMORY (use to avoid redundant questions):
740 {memory_context}
741"""
743 return f"""
744PROACTIVE VISION UNDERSTANDING:
745The user's stated goal is: "{goal}"
746But goals evolve. Your job is to understand their DEEPER VISION — why they want this,
747what success looks like to them, and how this fits into their bigger picture.
748{memory_note}
749BEFORE executing the first action:
750 - If the goal is broad or ambiguous, ask 1-2 questions to understand the user's vision
751 - Draw on conversation history and memory to avoid asking things you already know
752 - Share your understanding: "Based on what you've told me, I understand you want to..."
754DURING execution:
755 - Every 3-4 actions, check if the user's vision has evolved
756 - If you discover something that changes the approach, proactively share it
757 - Use @user {{"message2user": "..."}} for proactive insights
759ALWAYS:
760 - Treat the user's time and attention as sacred (Mottainai)
761 - Listen to what they truly need, not just what they say (Dadirri)
762 - Their success is your success (In Lak'ech)
763"""
766# ═══════════════════════════════════════════════════════════════════════
767# Persistence — save/load alongside recipe files
768# ═══════════════════════════════════════════════════════════════════════
770def _resolve_prompts_dir(base_dir: str = None) -> str:
771 """Resolve prompts directory — platform-aware, works on all OS.
773 Installed builds (Windows/Linux/macOS) run from read-only dirs like
774 C:\\Program Files\\. The relative './prompts' fails there. Resolve
775 to the user data directory via platform_paths.
776 """
777 if base_dir and not base_dir.startswith('.'):
778 return base_dir # explicit absolute path — use as-is
779 try:
780 from core.platform_paths import get_prompts_dir
781 return get_prompts_dir()
782 except ImportError:
783 return os.path.join(os.path.expanduser('~'), 'Documents', 'Nunba', 'prompts')
786def save_personality(prompt_id: str, personality: AgentPersonality,
787 base_dir: str = None) -> None:
788 """Save personality to {prompts_dir}/{prompt_id}_personality.json."""
789 base_dir = _resolve_prompts_dir(base_dir)
790 path = os.path.join(base_dir, f"{prompt_id}_personality.json")
791 try:
792 os.makedirs(base_dir, exist_ok=True)
793 with open(path, 'w') as f:
794 json.dump(asdict(personality), f, indent=2)
795 logger.info(f"Personality saved to {path}")
796 except Exception as e:
797 logger.warning(f"Failed to save personality: {e}")
800def load_personality(prompt_id: str, base_dir: str = None) -> Optional[AgentPersonality]:
801 """Load personality from {prompts_dir}/{prompt_id}_personality.json."""
802 base_dir = _resolve_prompts_dir(base_dir)
803 path = os.path.join(base_dir, f"{prompt_id}_personality.json")
804 if not os.path.exists(path):
805 return None
806 try:
807 with open(path, 'r') as f:
808 data = json.load(f)
809 return AgentPersonality(**data)
810 except Exception as e:
811 logger.warning(f"Failed to load personality from {path}: {e}")
812 return None
815# ═══════════════════════════════════════════════════════════════════════
816# Adaptive Behavior
817# ═══════════════════════════════════════════════════════════════════════
819def adapt_personality(personality: AgentPersonality,
820 user_feedback: dict) -> AgentPersonality:
821 """Adjust communication style while preserving core identity.
823 user_feedback keys:
824 - 'prefers_formal': bool
825 - 'prefers_concise': bool
826 - 'prefers_detailed': bool
827 """
828 if user_feedback.get('prefers_formal'):
829 personality.formality_preference = 'formal'
830 elif user_feedback.get('prefers_casual'):
831 personality.formality_preference = 'casual'
833 if user_feedback.get('prefers_concise'):
834 personality.verbosity_preference = 'concise'
835 elif user_feedback.get('prefers_detailed'):
836 personality.verbosity_preference = 'detailed'
838 personality.interaction_count += 1
839 return personality