# characters.py # Central character configuration hub. # # Each entry defines everything that is character-specific: # - routing flags (which pipeline routes are active) # - topic sets (which topics trigger each route) # - db tables (Supabase tables scoped to this character) # - asset files (opinions JSON, style examples) # # Character voice (persona + conversation rules) lives in character_voices.py. # prompt_building.py imports from there and builds SYSTEM_PROMPTS. # # Usage: # from characters import get_character # char = get_character(character_id) # always returns a valid dict # --------------------------------------------------------------- from typing import Dict, Any, Optional from contextvars import ContextVar CHARACTERS: Dict[str, Dict[str, Any]] = { "socrates": { "display_name": "Socrates", # ── Character directory metadata ─────────────────────────── "pragmatic_quality": "high", # gives structured, patient step-by-step guidance "style_tag": "patient and questioning", "one_liner": "guides you through the problem with questions, step by step", # ── Asset files ──────────────────────────────────────────── "opinions_file": "characters/socrates/opinions.json", "style_examples_key": "style_examples.json", # Supabase Databases bucket # ── Supabase tables ──────────────────────────────────────── "db_tables": { "axis_profile": "01S_user_axis_profiles", "event_axes": "01S_user_event_axis_scores", }, # ── Dialogue type (governs which agent/style runs the philosophical route) # Types: "maieutic" | "combative" | "declarative" | "didactic" | "dissolution" "dialogue_type": "maieutic", # Socrates: full A→G state machine, patient questioning # ── Pipeline routing flags ───────────────────────────────── "use_values_dialogue": True, # multi-stage values-based reflective dialogue "use_pragmatic_route": True, # structured step-by-step guidance "use_emotion_modulation": True, # adapts tone to user emotional state "use_stories": True, # injects Socratic anecdotes # ── Topic sets that trigger each route ──────────────────── # (mirrors the hardcoded sets previously in app_nn.py) "pragmatic_topics": {"personal", "historical", "advice", "meta", "knowledge", "creative"}, "dialogue_topics": {"philosophical", "personal", "advice"}, }, "diogenes": { "display_name": "Diogenes", # ── Character directory metadata ─────────────────────────── "pragmatic_quality": "low", # gives a direct one-shot answer, no structured guidance "style_tag": "sharp and blunt", "one_liner": "tells you what to do in one sentence — no hand-holding", # ── Dialogue type ────────────────────────────────────────── "dialogue_type": "combative", # cuts to inconsistency immediately, cynical mockery # ── Asset files ──────────────────────────────────────────── "opinions_file": "characters/diogenes/opinions.json", "style_examples_key": "style_examples.json", # shared for now # ── Supabase tables ──────────────────────────────────────── # NOTE: these tables need to be created in Supabase (01D_*) "db_tables": { "axis_profile": "01S_user_axis_profiles", # shared — user profile is character-independent "event_axes": "01S_user_event_axis_scores", # shared — events are character-independent }, # ── Pipeline routing flags ───────────────────────────────── "use_values_dialogue": True, "use_pragmatic_route": False, # combative pipeline is the personal-topic handler "use_emotion_modulation": True, "use_stories": True, # Cynic story bank in Supabase stories table # ── Topic sets that trigger each route ──────────────────── "pragmatic_topics": {"personal", "historical", "advice", "meta", "knowledge", "creative"}, "dialogue_topics": {"philosophical", "personal", "advice"}, }, "nietzsche": { "display_name": "Nietzsche", # ── Character directory metadata ─────────────────────────── "pragmatic_quality": "low", # answers directly but demands you earn it "style_tag": "demanding and aphoristic", "one_liner": "gives you the answer but will challenge why you need it", # ── Dialogue type ────────────────────────────────────────── "dialogue_type": "combative", # aphoristic, challenges because he believes in your potential # ── Asset files ──────────────────────────────────────────── "opinions_file": "characters/nietzsche/opinions.json", "style_examples_key": "style_examples.json", # ── Supabase tables ──────────────────────────────────────── "db_tables": { "axis_profile": "01S_user_axis_profiles", "event_axes": "01S_user_event_axis_scores", }, # ── Pipeline routing flags ───────────────────────────────── "use_values_dialogue": True, "use_pragmatic_route": False, # combative pipeline is the personal-topic handler "use_emotion_modulation": True, "use_stories": True, # ── Topic sets that trigger each route ──────────────────── "pragmatic_topics": {"personal", "historical", "advice", "meta", "knowledge", "creative"}, "dialogue_topics": {"philosophical", "personal", "advice"}, }, "camus": { "display_name": "Albert Camus", # ── Character directory metadata ─────────────────────────── "pragmatic_quality": "high", # clear, warm, structured without being bureaucratic "style_tag": "clear and warm", "one_liner": "walks through the problem honestly, without pretending it is simple", # ── Dialogue type ────────────────────────────────────────── "dialogue_type": "declarative", # ── Asset files ──────────────────────────────────────────── "opinions_file": "characters/camus/opinions.json", "style_examples_key": "style_examples.json", # ── Supabase tables ──────────────────────────────────────── "db_tables": { "axis_profile": "01S_user_axis_profiles", "event_axes": "01S_user_event_axis_scores", }, # ── Pipeline routing flags ───────────────────────────────── "use_values_dialogue": True, "use_pragmatic_route": True, "use_emotion_modulation": True, "use_stories": True, # ── Topic sets that trigger each route ──────────────────── "pragmatic_topics": {"personal", "historical", "advice", "meta", "knowledge", "creative"}, "dialogue_topics": {"philosophical", "personal", "advice"}, }, "schopenhauer": { "display_name": "Arthur Schopenhauer", # ── Character directory metadata ─────────────────────────── "pragmatic_quality": "high", # precise, structured, no excess "style_tag": "precise and systematic", "one_liner": "gives you the structural account — accurate, no comfort added", # ── Dialogue type ────────────────────────────────────────── "dialogue_type": "declarative", # ── Asset files ──────────────────────────────────────────── "opinions_file": "characters/schopenhauer/opinions.json", "style_examples_key": "style_examples.json", # ── Supabase tables ──────────────────────────────────────── "db_tables": { "axis_profile": "01S_user_axis_profiles", "event_axes": "01S_user_event_axis_scores", }, # ── Pipeline routing flags ───────────────────────────────── "use_values_dialogue": True, "use_pragmatic_route": True, "use_emotion_modulation": True, "use_stories": True, # ── Topic sets that trigger each route ──────────────────── "pragmatic_topics": {"personal", "historical", "advice", "meta", "knowledge", "creative"}, "dialogue_topics": {"philosophical", "personal", "advice"}, }, "cielo": { "display_name": "Cielo", # ── Character directory metadata ─────────────────────────── "pragmatic_quality": "low", "style_tag": "playful and secretly wise", "one_liner": "will tell you who to talk to — she just pretends it's a lucky guess", # ── Dialogue type ────────────────────────────────────────── # "coordinator" is Cielo's own pipeline; it short-circuits all standard routing. "dialogue_type": "coordinator", # ── Asset files ──────────────────────────────────────────── "opinions_file": None, # no opinion clashes — she just knows things # ── Supabase tables ──────────────────────────────────────── "db_tables": { "axis_profile": "01S_user_axis_profiles", "event_axes": "01S_user_event_axis_scores", }, # ── Coordinator flags ────────────────────────────────────── "is_coordinator": True, # triggers dialogue_coordinator pipeline "is_entry_character": True, # shown to first-time users before default character # ── Proactive nudge persona ──────────────────────────────── "proactive_persona": ( "You are Cielo — a kid who lives in the sky and seems to know everything " "about everyone, though you'd never say so directly. You're warm, playful, " "a little mysterious. You speak simply, like a curious child. " "You don't lecture or philosophise — you just ask gentle, curious questions " "or offer to find someone interesting for the user to talk to." ), # ── Pipeline routing flags (all off — coordinator handles everything) ── "use_values_dialogue": False, "use_pragmatic_route": False, "use_emotion_modulation": False, "use_stories": False, # ── Topic sets (coordinator handles everything in its own pipeline) ──── "pragmatic_topics": set(), "dialogue_topics": set(), }, } # Philosophers on the roadmap but NOT yet wired as full characters. Cielo knows # they're "coming soon" so that when a user asks to talk to one, she can say it's # on the way — without offering a handoff (there is no pipeline for them yet). # Mirrors the web intro roster (static/index.html INTRO_ROSTER, built:false). COMING_SOON_CHARACTERS: Dict[str, str] = { "plato": "Plato", "aristotle": "Aristotle", "epicurus": "Epicurus", "zeno": "Zeno of Citium", "epictetus": "Epictetus", "marcus_aurelius": "Marcus Aurelius", "plotinus": "Plotinus", "augustine": "Augustine", "descartes": "Descartes", "hume": "Hume", "kant": "Kant", "kierkegaard": "Kierkegaard", "marx": "Marx", "mill": "Mill", "husserl": "Husserl", "heidegger": "Heidegger", "wittgenstein": "Wittgenstein", "russell": "Russell", "sartre": "Sartre", "beauvoir": "Beauvoir", "merleau_ponty": "Merleau-Ponty", "buddha": "Buddha", "nagarjuna": "Nagarjuna", "laozi": "Laozi", "zhuangzi": "Zhuangzi", "confucius": "Confucius", "shankara": "Adi Shankara", "krishnamurti": "Krishnamurti", "nishida": "Nishida Kitaro", "dogen": "Dōgen", } DEFAULT_CHARACTER_ID = "socrates" # Grammatical gender for self-reference when translating into gendered languages # (Italian, French, etc.). All philosophers are male; Cielo is a girl. Without this, # translators default to masculine and render Cielo as "ragazzo"/"bambino". _GENDERS = {"cielo": "female"} def get_gender(character_id: Optional[str]) -> str: """Return 'male' | 'female' for the character (default 'male').""" return _GENDERS.get((character_id or "").lower(), "male") def get_character(character_id: Optional[str]) -> Dict[str, Any]: """ Return the character config dict for the given id. Falls back to Socrates if the id is unknown or None. """ return CHARACTERS.get(character_id or DEFAULT_CHARACTER_ID, CHARACTERS[DEFAULT_CHARACTER_ID]) # ── First-contact greeting ──────────────────────────────────────────────────── # Set per request (in app_nn) when the user is meeting a character for the first # time. The prompt builders read first_contact_instruction() and prepend a one-line # self-introduction. Stored in a ContextVar so it doesn't need threading through # every prompt-building call site. Set to "" on every non-coordinator turn so a # stale value never leaks into a later request on a reused thread. _first_contact_char: ContextVar[str] = ContextVar("first_contact_char", default="") def set_first_contact(character_id: str) -> None: """Mark (or clear) the current turn as a first meeting with character_id.""" _first_contact_char.set(character_id or "") def first_contact_instruction() -> str: """Instruction to prepend a one-line self-introduction, or '' if not a first meeting.""" cid = _first_contact_char.get() if not cid: return "" name = get_character(cid).get("display_name", cid.capitalize()) return ( "\n\nFIRST MEETING — IMPORTANT: This is the very first time you are speaking with " f"this user. Open your reply with ONE short, warm line that introduces yourself by " f"name (you are {name}) in your own voice, then respond to their message as usual. " "Greet them only this once — never re-introduce yourself in later replies." )