89 lines
2.6 KiB
Python
89 lines
2.6 KiB
Python
"""Operator → cart persistence for chat-saiden.
|
|
|
|
A 'cart' here is the per-operator calibrated config: persona name, voice,
|
|
system prompt, UI palette. Stored as JSON on disk under
|
|
~/.local/share/chat-saiden/operators/<email>.json.
|
|
|
|
Later this can be promoted to a real `marauder cart` once the format
|
|
stabilises.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
from dataclasses import asdict, dataclass, field
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
log = logging.getLogger("chat-saiden.cart")
|
|
|
|
DATA_DIR = Path(
|
|
os.environ.get("CHAT_SAIDEN_DATA_DIR")
|
|
or (Path.home() / ".local/share/chat-saiden")
|
|
)
|
|
OPERATORS_DIR = DATA_DIR / "operators"
|
|
|
|
|
|
@dataclass
|
|
class Cart:
|
|
operator_email: str
|
|
operator_name: str = ""
|
|
persona_name: str = "Samantha"
|
|
cart_tag: str = "" # marauder cart tag — links to ~/.marauder cart DB
|
|
language: str = "en" # en | pl
|
|
voice: str = "en_US-amy-medium"
|
|
system_prompt: str = ""
|
|
# UI calibration outputs. All default to neutral.
|
|
ui_palette: str = "default" # default | rose | morning | evening | sage | paper | ink
|
|
ui_typography: str = "sans" # sans | serif-warm | serif-formal | mixed-modern | mono
|
|
ui_density: str = "normal" # airy | normal | dense
|
|
ui_labels: str = "block" # block | cursive | none | prefix
|
|
created_at: str = ""
|
|
version: int = 3
|
|
|
|
@property
|
|
def is_calibrated(self) -> bool:
|
|
return bool(self.system_prompt and self.persona_name and self.voice)
|
|
|
|
|
|
def _slug(email: str) -> str:
|
|
return re.sub(r"[^a-z0-9._-]", "_", email.lower())
|
|
|
|
|
|
def _path(email: str) -> Path:
|
|
OPERATORS_DIR.mkdir(parents=True, exist_ok=True)
|
|
return OPERATORS_DIR / f"{_slug(email)}.json"
|
|
|
|
|
|
def load(email: str) -> Cart | None:
|
|
p = _path(email)
|
|
if not p.exists():
|
|
return None
|
|
try:
|
|
data = json.loads(p.read_text(encoding="utf-8"))
|
|
# tolerate older carts missing the new ui_* fields
|
|
return Cart(**{k: v for k, v in data.items() if k in Cart.__dataclass_fields__})
|
|
except Exception:
|
|
log.exception("failed to load cart for %s", email)
|
|
return None
|
|
|
|
|
|
def save(cart: Cart) -> None:
|
|
if not cart.created_at:
|
|
cart.created_at = datetime.utcnow().isoformat(timespec="seconds") + "Z"
|
|
p = _path(cart.operator_email)
|
|
p.write_text(json.dumps(asdict(cart), indent=2), encoding="utf-8")
|
|
log.info("saved cart for %s → %s", cart.operator_email, p)
|
|
|
|
|
|
def forget(email: str) -> bool:
|
|
p = _path(email)
|
|
if p.exists():
|
|
p.unlink()
|
|
log.info("forgot cart for %s", email)
|
|
return True
|
|
return False
|