Spaces:
Running
Running
| # 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." | |
| ) | |