Socrates_docker / characters.py
AlessandroAmodioNGI's picture
feat: full-colour character roster on intro + Cielo coming-soon awareness
d7a4046
Raw
History Blame Contribute Delete
16.8 kB
# 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."
)