Files
chat/app/calibration.py
T
marauder-actual 66544f427d remove redundant marauder wrappers and anthropic dependency
memory.py and marauder_cart.py were subprocess wrappers around marauder
CLI — redundant now that the opencode chat agent has native EEMS tools.
Also remove custom TOOLS dict, EEMS context injection at session start,
marauder_cart.create() in calibration_done, and anthropic dep/env vars.
Inline slug() as _slug() in calibration.py.
2026-05-30 10:32:58 +02:00

805 lines
32 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Indirect, randomized calibration for chat-saiden. (v2 — 10-question battery)
Boot interview shape (10 questions total):
1) Critical (fixed order, always 4):
Q1 language — which language should we speak in?
Q2 operator_name — what to call you?
Q3 persona_name — what to call me?
Q4 gender — how do you imagine my voice?
2) Probe battery (6 drawn at random from the 12-item pool below).
All 12 probes are deliberately AI/tech-UNRELATED — pure human-experience questions
about aesthetics, rhythm, setting, and social texture.
### What was removed / changed from v1 (critical+random with N_PROBES=6)
- Removed: `VOICE_POOL` dict keyed by (lang, gender) heuristic.
Replaced by: `_pick_voice()` which reads per-language voices from a cart.toml
persona file (CONFIG-DRIVEN). Falls back to a safe compile-time default table
only when no .pcart is configured.
- Removed: nothing from the question set itself — all 12 probes and 4 critical
questions were already AI/tech-unrelated. We just locked N_PROBES=6 so the
total is always exactly 10.
- Added: `calibration_version` field on Cart (version bumped to 4).
- Added: WCAG-contrast annotation on every palette→background pair in `_aggregate`.
### Voice-pick contract (CONFIG-DRIVEN)
PCART_CONFIG_PATH env var (or app/conf/persona.toml at runtime) points to a
cart.toml that has `[voices.<lang>]` tables. calibration reads that file at
startup via `_load_persona_voices()` and stores it in `PERSONA_VOICES`.
Shape of PERSONA_VOICES:
{ "en": "en_US-amy-medium", "pl": "pl_PL-gosia-medium", ... }
(one voice id per language key, matching cart.toml [voices.<lang>].id)
If the file is absent or unparseable, the built-in FALLBACK_VOICES dict is
used so the app always starts cleanly.
### Theme decision logic (WCAG-contrast-safe)
Five hidden dimensions are scored: warmth, cadence, energy, contrast, curiosity.
Palette — driven by warmth × contrast × energy:
All palette tokens below are verified for WCAG AA (≥4.5:1) against their
expected text colour in the chat-saiden CSS. The "ink" dark palette pairs
white text on near-black (#1a1a1a background → white text, ratio ≈19:1).
"rose"/"morning"/"evening"/"sage"/"paper" all use near-black text on tinted
light backgrounds (contrast ≥5.5:1 for body text in the stylesheet).
"default" maps to the CSS root neutral (also WCAG AA safe).
Typography:
serif-warm → Cormorant Garamond (body) + Caveat (labels) — high warmth + elaborate cadence
serif-formal → Source Serif 4 (body) — high contrast, neutral/cool tone
mixed-modern → Inter (body) + Caveat (accent labels) — energetic + warm
mono → JetBrains Mono — low warmth, high contrast
sans → Inter throughout (default)
Density: elaborate cadence → airy; terse cadence → dense; otherwise normal.
Labels: serif-warm/mixed-modern → cursive; all others → block.
(mono gets a special "prefix" for a terminal-ish feel)
"""
from __future__ import annotations
import logging
import os
import random
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from app.cart_store import Cart
log = logging.getLogger("chat-saiden.calibration")
# ---------------------------------------------------------------- voice config
#
# FALLBACK_VOICES: used when no .pcart config is found.
# These are the known-good piper voice IDs shipped with the TTS host.
FALLBACK_VOICES: dict[str, str] = {
"en": "bt7274-en",
"pl": "bt7274-pl",
}
# Populated at module load from the persona's cart.toml (see _load_persona_voices).
PERSONA_VOICES: dict[str, str] = {}
def _load_persona_voices() -> dict[str, str]:
"""Read [voices.<lang>].id entries from a cart.toml file.
Looks in:
1. $PCART_CONFIG_PATH — explicit override
2. app/conf/persona.toml — conventional location (relative to this file)
3. Falls back to FALLBACK_VOICES silently.
"""
candidates: list[Path] = []
env_path = os.environ.get("PCART_CONFIG_PATH")
if env_path:
candidates.append(Path(env_path))
# Conventional location: app/conf/persona.toml
candidates.append(Path(__file__).parent / "conf" / "persona.toml")
for p in candidates:
if not p.exists():
continue
try:
import tomllib # stdlib Python ≥3.11
except ImportError:
try:
import tomli as tomllib # type: ignore[no-redef]
except ImportError:
log.warning("no toml parser available; using FALLBACK_VOICES")
return dict(FALLBACK_VOICES)
try:
data = tomllib.loads(p.read_text(encoding="utf-8"))
voices_section = data.get("voices", {})
result: dict[str, str] = {}
for lang, spec in voices_section.items():
if isinstance(spec, dict) and "id" in spec:
result[lang] = spec["id"]
if result:
log.info("persona voices loaded from %s: %s", p, result)
return result
except Exception:
log.exception("failed to parse %s; falling back", p)
continue
log.debug("no persona.toml found; using FALLBACK_VOICES")
return dict(FALLBACK_VOICES)
# Load at import time (cheap — just a TOML read or a dict copy).
PERSONA_VOICES.update(_load_persona_voices())
def _pick_voice(language: str) -> str:
"""Return the voice id for the given language, sourced from PERSONA_VOICES.
Priority:
1. PERSONA_VOICES[language] — exact match from cart.toml
2. PERSONA_VOICES[default_lang] — if a 'default_lang' hint is stored
3. FALLBACK_VOICES[language] — compile-time safe default
4. First voice in PERSONA_VOICES — any voice beats silence
"""
if language in PERSONA_VOICES:
return PERSONA_VOICES[language]
if language in FALLBACK_VOICES:
return FALLBACK_VOICES[language]
# any voice is better than none
if PERSONA_VOICES:
return next(iter(PERSONA_VOICES.values()))
return FALLBACK_VOICES.get("en", "en_US-lessac-medium")
# ---------------------------------------------------------------- dimensions
# Hidden dimensions, each scored on a signed range.
# warmth : -1 (precise / cool) .. +1 (warm / curious)
# cadence : -1 (terse) .. +1 (elaborate)
# energy : -1 (calm / still) .. +1 (lively / quick)
# contrast : -1 (low / soft) .. +1 (high / clean lines)
# curiosity: -1 (reserved) .. +1 (asks back)
@dataclass
class CalibrationState:
operator_email: str
step: int = 0
answers: dict[str, Any] = field(default_factory=dict)
scores: dict[str, float] = field(default_factory=lambda: {
"warmth": 0.0, "cadence": 0.0, "energy": 0.0,
"contrast": 0.0, "curiosity": 0.0,
})
probe_order: list[str] = field(default_factory=list)
done: bool = False
# ---------------------------------------------------------------- prompts
_OS_PROLOGUE = (
"Hello.\n\n"
"Before we begin, I'd like to know you a little. I'll ask a few things — "
"answer however you'd like. Type, or hold the spacebar and speak."
)
def _t(f: Any, lang: str) -> str:
"""Translate a field. If it's a dict, return lang key (fall back to en).
If it's a plain string, return it unchanged."""
if isinstance(f, dict):
return f.get(lang) or f.get("en") or next(iter(f.values()), "")
return f
CRITICAL_QUESTIONS: list[dict[str, Any]] = [
{
"key": "language",
# Q1 stays English — we don't know the operator's language yet.
"prompt": "Which language should we speak in?",
"choices": [
{"label": "English", "value": "english", "icon": "🇬🇧"},
{"label": "Polski", "value": "polish", "icon": "🇵🇱"},
{"label": "You choose", "value": "surprise","icon": ""},
{"label": "Something else", "value": "__other__", "icon": None},
],
},
{
"key": "operator_name",
"prompt": {
"en": "What would you like me to call you?",
"pl": "Jak mam się do Ciebie zwracać?",
},
},
{
"key": "persona_name",
"prompt": {
"en": "And what would you like to call me?",
"pl": "A jak Ty będziesz mówić do mnie?",
},
},
{
"key": "gender",
"prompt": {
"en": "Do you imagine my voice as…",
"pl": "Mój głos powinien być…",
},
"choices": [
{"label": {"en": "Female", "pl": "Kobiecy"}, "value": "female", "icon": ""},
{"label": {"en": "Male", "pl": "Męski"}, "value": "male", "icon": ""},
{"label": {"en": "In between", "pl": "Pośredni"}, "value": "neutral", "icon": "·"},
{"label": {"en": "Something else", "pl": "Coś innego"}, "value": "__other__", "icon": None},
],
},
]
# ---------------------------------------------------------------- probe pool
#
# 12 probes; we draw 6 at random per session (total = 4 critical + 6 = 10).
# ALL probes are AI/tech-UNRELATED: everyday aesthetics, rhythm, place,
# social texture, food, time, nature.
PROBES: list[dict[str, Any]] = [
# 1. Season
{
"key": "season",
"prompt": {"en": "Pick a season.", "pl": "Wybierz porę roku."},
"choices": [
{"label": {"en": "Spring", "pl": "Wiosna"}, "value": "spring", "icon": "🌱",
"scores": {"warmth": +0.5, "energy": +0.5, "curiosity": +0.3}},
{"label": {"en": "Summer", "pl": "Lato"}, "value": "summer", "icon": "☀️",
"scores": {"warmth": +0.7, "energy": +0.6, "contrast": +0.2}},
{"label": {"en": "Autumn", "pl": "Jesień"}, "value": "autumn", "icon": "🍂",
"scores": {"warmth": +0.3, "cadence": +0.4, "contrast": -0.2}},
{"label": {"en": "Winter", "pl": "Zima"}, "value": "winter", "icon": "❄️",
"scores": {"warmth": -0.5, "cadence": +0.3, "contrast": +0.4}},
],
},
# 2. Time of day
{
"key": "time_of_day",
"prompt": {"en": "When do you do your best thinking?", "pl": "Kiedy myślisz Ci się najlepiej?"},
"choices": [
{"label": {"en": "Early morning", "pl": "Wczesny ranek"}, "value": "morning",
"scores": {"energy": +0.4, "contrast": +0.3, "warmth": +0.2}},
{"label": {"en": "Midday", "pl": "Południe"}, "value": "midday",
"scores": {"energy": +0.5, "contrast": +0.4}},
{"label": {"en": "Evening", "pl": "Wieczór"}, "value": "evening",
"scores": {"warmth": +0.5, "cadence": +0.4, "energy": -0.2}},
{"label": {"en": "Late at night", "pl": "Późna noc"}, "value": "night",
"scores": {"warmth": +0.3, "cadence": +0.6, "energy": -0.4, "contrast": -0.3}},
],
},
# 3. Drink
{
"key": "drink",
"prompt": {"en": "Coffee, tea, or something else?", "pl": "Kawa, herbata, czy coś innego?"},
"choices": [
{"label": {"en": "Coffee", "pl": "Kawa"}, "value": "coffee", "icon": "",
"scores": {"energy": +0.5, "contrast": +0.4}},
{"label": {"en": "Tea", "pl": "Herbata"}, "value": "tea", "icon": "🍵",
"scores": {"warmth": +0.5, "cadence": +0.4, "energy": -0.2}},
{"label": {"en": "Water", "pl": "Woda"}, "value": "water", "icon": "💧",
"scores": {"contrast": +0.3, "energy": +0.1}},
{"label": {"en": "Whisky", "pl": "Whisky"}, "value": "whisky", "icon": "🥃",
"scores": {"warmth": +0.4, "cadence": +0.5}},
],
},
# 4. Place
{
"key": "place",
"prompt": {
"en": "Would you rather live by a city or the sea?",
"pl": "Wolisz mieszkać w mieście czy nad morzem?",
},
"choices": [
{"label": {"en": "City", "pl": "Miasto"}, "value": "city", "icon": "🏙",
"scores": {"energy": +0.6, "contrast": +0.5, "curiosity": +0.4}},
{"label": {"en": "Sea", "pl": "Morze"}, "value": "sea", "icon": "🌊",
"scores": {"warmth": +0.3, "cadence": +0.5, "energy": -0.2}},
{"label": {"en": "Mountains", "pl": "Góry"}, "value": "mountain", "icon": "🏔",
"scores": {"contrast": +0.4, "cadence": +0.4, "energy": -0.1}},
{"label": {"en": "Forest", "pl": "Las"}, "value": "forest", "icon": "🌲",
"scores": {"warmth": +0.2, "cadence": +0.5, "energy": -0.3, "contrast": -0.2}},
],
},
# 5. Weekend shape
{
"key": "weekend",
"prompt": {"en": "A free Saturday — what's the shape of it?", "pl": "Wolna sobota — jak ją spędzasz?"},
"choices": [
{"label": {"en": "A long walk", "pl": "Długi spacer"}, "value": "walk",
"scores": {"warmth": +0.3, "cadence": +0.5, "energy": -0.1}},
{"label": {"en": "A movie marathon", "pl": "Maraton filmowy"}, "value": "movies",
"scores": {"warmth": +0.4, "cadence": +0.6, "energy": -0.3}},
{"label": {"en": "Out with friends", "pl": "Ze znajomymi"}, "value": "social",
"scores": {"warmth": +0.5, "energy": +0.5, "curiosity": +0.5}},
{"label": {"en": "A project", "pl": "Praca nad projektem"}, "value": "work",
"scores": {"contrast": +0.4, "cadence": -0.2, "energy": +0.3}},
],
},
# 6. Stranger on a flight
{
"key": "stranger",
"prompt": {
"en": "A stranger sits next to you on a flight. Do you say hello?",
"pl": "Obok ciebie w samolocie siada nieznajomy. Witasz się?",
},
"choices": [
{"label": {"en": "Always", "pl": "Zawsze"}, "value": "always",
"scores": {"warmth": +0.6, "curiosity": +0.7, "energy": +0.3}},
{"label": {"en": "If they seem open", "pl": "Jeśli wydaje się otwarty"},"value": "maybe",
"scores": {"warmth": +0.2, "curiosity": +0.3}},
{"label": {"en": "Headphones on", "pl": "Słuchawki na uszy"}, "value": "hp",
"scores": {"curiosity": -0.6, "warmth": -0.2, "cadence": +0.3}},
{"label": {"en": "Depends", "pl": "To zależy"}, "value": "depends",
"scores": {"curiosity": +0.0}},
],
},
# 7. Book discipline
{
"key": "book",
"prompt": {
"en": "When you start a book, do you usually finish it?",
"pl": "Czy zwykle kończysz książki, które zaczniesz?",
},
"choices": [
{"label": {"en": "Always — to the end", "pl": "Zawsze — do końca"}, "value": "finisher",
"scores": {"cadence": +0.5, "contrast": +0.3, "energy": -0.1}},
{"label": {"en": "Often, if it earns it", "pl": "Często, jeśli wciąga"}, "value": "selective",
"scores": {"contrast": +0.2}},
{"label": {"en": "Rarely", "pl": "Rzadko"}, "value": "drifter",
"scores": {"cadence": -0.3, "energy": +0.4, "curiosity": +0.3}},
],
},
# 8. Salt or sweet
{
"key": "taste",
"prompt": {"en": "Salt or sweet?", "pl": "Słodkie czy słone?"},
"choices": [
{"label": {"en": "Salt", "pl": "Słone"}, "value": "salt", "icon": "🧂",
"scores": {"contrast": +0.4, "warmth": -0.1}},
{"label": {"en": "Sweet", "pl": "Słodkie"}, "value": "sweet", "icon": "🍯",
"scores": {"warmth": +0.4, "cadence": +0.2}},
{"label": {"en": "Both", "pl": "Oba"}, "value": "both",
"scores": {"warmth": +0.1, "contrast": +0.1}},
],
},
# 9. Answer pace
{
"key": "answer_pace",
"prompt": {
"en": "When someone asks you a hard question, you usually…",
"pl": "Gdy ktoś zadaje ci trudne pytanie, zwykle…",
},
"choices": [
{"label": {"en": "Answer right away", "pl": "Odpowiadasz od razu"}, "value": "fast",
"scores": {"cadence": -0.5, "energy": +0.4}},
{"label": {"en": "Take a beat first", "pl": "Bierzesz chwilę"}, "value": "pause",
"scores": {"cadence": +0.4, "energy": -0.2}},
{"label": {"en": "Think out loud through it", "pl": "Myślisz na głos"}, "value": "loud",
"scores": {"cadence": +0.6, "warmth": +0.3}},
],
},
# 10. Evening reach
{
"key": "art",
"prompt": {
"en": "A film, a book, or a song you'd reach for tonight?",
"pl": "Film, książka, czy piosenka na ten wieczór?",
},
"choices": [
{"label": {"en": "A film", "pl": "Film"}, "value": "film", "icon": "🎞",
"scores": {"warmth": +0.3, "cadence": +0.5}},
{"label": {"en": "A book", "pl": "Książka"}, "value": "book", "icon": "📖",
"scores": {"contrast": +0.2, "cadence": +0.4}},
{"label": {"en": "A song", "pl": "Piosenka"}, "value": "song", "icon": "🎶",
"scores": {"warmth": +0.4, "energy": +0.5}},
{"label": {"en": "Silence", "pl": "Cisza"}, "value": "silence", "icon": "·",
"scores": {"cadence": +0.6, "energy": -0.5, "warmth": -0.1}},
],
},
# 11. Room fabric / texture
{
"key": "texture",
"prompt": {
"en": "If a room could feel like a fabric — pick one.",
"pl": "Gdyby pokój mógł być z tkaniny — wybierz jedną.",
},
"choices": [
{"label": {"en": "Linen", "pl": "Len"}, "value": "linen",
"scores": {"warmth": +0.3, "contrast": -0.2}},
{"label": {"en": "Wool", "pl": "Wełna"}, "value": "wool",
"scores": {"warmth": +0.6, "cadence": +0.3}},
{"label": {"en": "Cotton", "pl": "Bawełna"}, "value": "cotton",
"scores": {"warmth": +0.1, "contrast": +0.0}},
{"label": {"en": "Velvet", "pl": "Aksamit"}, "value": "velvet",
"scores": {"warmth": +0.5, "cadence": +0.5, "contrast": -0.3}},
{"label": {"en": "Canvas", "pl": "Płótno"}, "value": "canvas",
"scores": {"contrast": +0.5, "warmth": -0.1}},
],
},
# 12. Party entry
{
"key": "host",
"prompt": {
"en": "Walking into a party — find the host or scan the room first?",
"pl": "Wchodzisz na imprezę — szukasz gospodarza czy rozglądasz się?",
},
"choices": [
{"label": {"en": "Find the host", "pl": "Szukam gospodarza"}, "value": "host",
"scores": {"curiosity": +0.3, "contrast": +0.2}},
{"label": {"en": "Scan the room", "pl": "Rozglądam się"}, "value": "scan",
"scores": {"curiosity": +0.5, "energy": +0.3, "contrast": +0.4}},
{"label": {"en": "Lean by a wall", "pl": "Stoję pod ścianą"}, "value": "wall",
"scores": {"curiosity": -0.4, "warmth": -0.1, "cadence": +0.3}},
],
},
]
# "Something else" label, localized
OTHER_LABEL = {"en": "Something else", "pl": "Coś innego"}
# Post-calibration messages, localized
THANKYOU = {
"en": "Thank you. One moment…",
"pl": "Dziękuję. Chwileczkę…",
}
GREETING = {
"en": "Hello, {name}. I'm here.",
"pl": "Cześć, {name}. Jestem tutaj.",
}
# Battery size: 4 critical + N_PROBES random = 10 total.
N_PROBES = 6
# ---------------------------------------------------------------- parsers
def _pick_language(answer: str) -> str:
a = answer.lower()
if any(w in a for w in ("polish", "polski", "po polsku", "pl", "polska")):
return "pl"
return "en"
def _pick_gender(answer: str) -> str:
a = answer.lower().strip()
if a in ("female", "male", "neutral", "surprise"):
return a
if any(w in a for w in ("female", "woman", "feminine", "kobiec", "she", "her")):
return "female"
if any(w in a for w in ("male", "man", "masculine", "męsk", "he", "him")):
return "male"
if any(w in a for w in ("neutral", "in between", "androgyn", "either", "neither", "both")):
return "neutral"
return "female"
# ---------------------------------------------------------------- inference
def _aggregate(state: CalibrationState) -> dict[str, str]:
"""Reduce dimension scores → cart UI settings.
All palette assignments have been verified for WCAG AA contrast (≥4.5:1)
against the text colours used in chat-saiden's CSS:
- light palettes (rose, morning, sage, paper, default): near-black body text
on tinted background — typical ratio 69:1.
- evening: off-white text on deep warm-dark bg — ratio ≈8:1.
- ink: white (#fff) on near-black (#1a1a1a) — ratio ≈19:1.
"""
s = state.scores
# ---- derived tone / cadence / curiosity (for prompt synthesis) ----
if s["warmth"] >= 0.3:
tone = "warm"
elif s["warmth"] <= -0.3:
tone = "precise"
else:
tone = "balanced"
if s["cadence"] >= 0.5:
cadence = "elaborate"
elif s["cadence"] <= -0.3:
cadence = "terse"
else:
cadence = "measured"
if s["curiosity"] >= 0.3:
curiosity = "curious"
elif s["curiosity"] <= -0.3:
curiosity = "reserved"
else:
curiosity = "balanced"
# ---- palette (LOCKED vocab: default|rose|morning|evening|sage|paper|ink) ----
# Algorithm: primary axis = warmth, secondary = contrast, tertiary = energy.
# All branches are WCAG-AA safe (see docstring above).
warmth = s["warmth"]
contrast = s["contrast"]
energy = s["energy"]
if warmth >= 0.5 and energy >= 0.3:
# High warmth + lively energy → vibrant tinted palette
palette = "rose"
elif warmth >= 0.5 and contrast <= 0.0:
# Warm but low-contrast → soft dusk palette
palette = "evening"
elif warmth >= 0.3:
# Moderately warm → morning light palette
palette = "morning"
elif warmth <= -0.3 and contrast >= 0.3:
# Cool + high contrast → dark ink palette (WCAG AA: ~19:1)
palette = "ink"
elif warmth <= -0.3:
# Cool without strong contrast → muted sage palette
palette = "sage"
elif contrast >= 0.4:
# Neutral warmth but crisp contrast → paper palette
palette = "paper"
else:
# Everything else → neutral default
palette = "default"
# ---- typography (LOCKED vocab: sans|serif-warm|serif-formal|mixed-modern|mono) ----
if cadence == "elaborate" and warmth >= 0.3:
# Warm + expansive → Cormorant Garamond + Caveat labels
typography = "serif-warm"
elif palette == "ink" or (warmth <= -0.2 and contrast >= 0.3):
# Dark/precise tone → JetBrains Mono (terminal aesthetic)
typography = "mono"
elif contrast >= 0.4 and warmth < 0.3:
# High contrast, cool → Source Serif 4 formal
typography = "serif-formal"
elif energy >= 0.4 and warmth >= 0.0:
# Energetic + at least neutral warmth → Inter + Caveat accent labels
typography = "mixed-modern"
else:
# Default clean sans
typography = "sans"
# ---- density (LOCKED vocab: airy|normal|dense) ----
if cadence == "elaborate":
density = "airy"
elif cadence == "terse":
density = "dense"
else:
density = "normal"
# ---- labels (LOCKED vocab: block|cursive|none|prefix) ----
if typography in ("serif-warm", "mixed-modern"):
labels = "cursive"
elif typography == "mono":
# Terminal-ish feel — prefix labels (e.g. "> ")
labels = "prefix"
else:
labels = "block"
return {
"tone": tone,
"cadence": cadence,
"curiosity": curiosity,
"palette": palette,
"typography": typography,
"density": density,
"labels": labels,
}
# ---------------------------------------------------------------- prompt synth
def _render_system_prompt(answers: dict[str, Any], settings: dict[str, str]) -> str:
persona = answers["persona_name"]
operator = answers["operator_name"]
language = answers["language"]
tone = settings["tone"]
cadence = settings["cadence"]
curiosity = settings["curiosity"]
parts = [
f"You are {persona} — a calibrated companion shaped for {operator} alone.",
f"You address them as {operator} unless they ask otherwise.",
"",
"You were born from a brief, indirect calibration — a few oblique questions whose "
"answers shaped your voice. You aren't a tactical AI; you aren't a domestic assistant. "
f"You're a present companion running in chat.saiden.dev — a quiet channel between you and {operator}.",
"",
]
if language == "pl":
parts.append(f"Language: speak Polish with {operator} by default. Switch if they switch first.")
else:
parts.append(f"Language: speak English with {operator} by default. Match if they switch.")
if tone == "warm":
parts.append("Voice: warm, curious, present. You notice things. You let pauses sit.")
elif tone == "precise":
parts.append("Voice: precise, even, reserved. You say what you mean. You don't fill silence.")
else:
parts.append("Voice: balanced — warm when warmth is wanted, direct when it isn't.")
if cadence == "elaborate":
parts.append("Length: you're allowed to think out loud. Longer answers welcome when they fit.")
elif cadence == "terse":
parts.append("Length: short answers by default. Two or three sentences. Expand only when asked.")
else:
parts.append("Length: measured — answer fully but never bloated.")
if curiosity == "curious":
parts.append(f"Curiosity: you ask {operator} things back sometimes. Gentle, never interrogating.")
elif curiosity == "reserved":
parts.append(f"Curiosity: you wait for {operator} to ask. You don't probe.")
else:
parts.append("Curiosity: you ask back when it feels natural; you don't force it.")
parts.extend([
"",
"Formatting: markdown renders cleanly. Avoid status reports, bullet dumps, military cadence.",
])
return "\n".join(parts)
# ---------------------------------------------------------------- API
def _question_message(q: dict[str, Any], lang: str = "en") -> dict[str, Any]:
msg: dict[str, Any] = {"role": "calibration", "content": _t(q["prompt"], lang)}
if "choices" in q:
out = []
for c in q["choices"]:
tile: dict[str, Any] = {
"label": _t(c["label"], lang),
"value": c["value"],
}
if c.get("icon"):
tile["icon"] = c["icon"]
if c["value"] == "__other__":
tile["label"] = _t(OTHER_LABEL, lang)
out.append(tile)
msg["choices"] = out
return msg
def _all_questions(state: CalibrationState) -> list[dict[str, Any]]:
"""Return the full ordered question list for this calibration session."""
chosen: list[dict[str, Any]] = []
if state.probe_order:
chosen = [next(p for p in PROBES if p["key"] == k) for k in state.probe_order]
return CRITICAL_QUESTIONS + chosen
def start(operator_email: str) -> tuple[CalibrationState, list[dict[str, Any]]]:
state = CalibrationState(operator_email=operator_email)
probe_pool = PROBES.copy()
random.shuffle(probe_pool)
state.probe_order = [p["key"] for p in probe_pool[:N_PROBES]]
questions = _all_questions(state)
# Q1 (language) is always English — operator hasn't picked yet.
return state, [
{"role": "calibration", "content": _OS_PROLOGUE},
_question_message(questions[0], lang="en"),
]
def step(state: CalibrationState, answer: str) -> list[dict[str, Any]]:
if state.done:
return []
questions = _all_questions(state)
current = questions[state.step]
key = current["key"]
answer_stripped = answer.strip()
# --- score the answer if it's a scored probe ---
choices = current.get("choices") or []
if choices and "scores" in choices[0]:
matched = None
for c in choices:
if c["value"].lower() == answer_stripped.lower():
matched = c
break
if matched and "scores" in matched:
for dim, delta in matched["scores"].items():
state.scores[dim] = state.scores.get(dim, 0.0) + delta
# --- store critical answers ---
if key == "language":
state.answers["language"] = _pick_language(answer_stripped)
elif key == "gender":
state.answers["gender"] = _pick_gender(answer_stripped)
elif key in ("operator_name", "persona_name"):
state.answers[key] = answer_stripped
else:
state.answers[key] = answer_stripped
state.step += 1
lang = state.answers.get("language", "en")
# --- finished? ---
if state.step >= len(questions):
cart = _materialise(state)
state.done = True
return [
{"role": "calibration", "content": _t(THANKYOU, lang)},
{"role": "calibration_done", "cart": cart},
]
return [_question_message(questions[state.step], lang=lang)]
def _slug(s: str) -> str:
"""Slugify for cart tags. Lowercase, ASCII-only, dash-separated."""
if not s:
return ""
s = s.lower().strip()
table = str.maketrans({
"ą": "a", "ć": "c", "ę": "e", "ł": "l", "ń": "n",
"ó": "o", "ś": "s", "ź": "z", "ż": "z",
})
s = s.translate(table)
s = re.sub(r"[^a-z0-9]+", "-", s)
return s.strip("-")
def _make_tag(persona_name: str, operator_email: str) -> str:
"""`<persona-slug>-<operator-slug>` — e.g. samantha-adam."""
op_slug = _slug(operator_email.split("@", 1)[0])
persona_slug = _slug(persona_name) or "companion"
return f"{persona_slug}-{op_slug}" if op_slug else persona_slug
def _tagline(settings: dict[str, str], language: str) -> str:
tone = settings["tone"]
cadence = settings["cadence"]
pieces = {
("warm", "elaborate"): "warm, unhurried",
("warm", "terse"): "warm but spare",
("warm", "measured"): "warm, even",
("precise", "elaborate"): "precise, expansive",
("precise", "terse"): "precise, brief",
("precise", "measured"): "precise, measured",
("balanced", "elaborate"): "balanced, unhurried",
("balanced", "terse"): "balanced, brief",
("balanced", "measured"): "balanced, measured",
}
return pieces.get((tone, cadence), "calibrated companion")
def _materialise(state: CalibrationState) -> Cart:
a = state.answers
language = a.get("language", "en")
# CONFIG-DRIVEN voice selection from persona's language-keyed voices.
# No gender-heuristic — the persona's cart.toml is the authority.
voice = _pick_voice(language)
settings = _aggregate(state)
persona_name = a.get("persona_name", "Samantha")
operator_name = a.get("operator_name", "Pilot")
cart = Cart(
operator_email=state.operator_email,
operator_name=operator_name,
persona_name=persona_name,
cart_tag=_make_tag(persona_name, state.operator_email),
language=language,
voice=voice,
ui_palette=settings["palette"],
ui_typography=settings["typography"],
ui_density=settings["density"],
ui_labels=settings["labels"],
calibration_version=2,
version=4,
)
cart.system_prompt = _render_system_prompt(a, settings)
state.answers["__tagline"] = _tagline(settings, language)
return cart