Spaces:
Running
Running
| import os | |
| import secrets | |
| import time | |
| import uuid | |
| from fastapi import FastAPI, HTTPException, Request | |
| from fastapi.responses import FileResponse, PlainTextResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from pydantic import BaseModel, Field | |
| from starlette.middleware.sessions import SessionMiddleware | |
| from app.scorer import score_with_trace | |
| from app.soul_bridge import SoulBridge | |
| from app.appearance import mood_to_appearance | |
| from app.trace_view import shape_trace | |
| from app.crisis_view import crisis_block | |
| from app.vocab import mood_word | |
| from app.voice import emoticon, simlish, voice_params | |
| from app.audit import AuditLog, build_row | |
| from app.persistence import restore_soul, Snapshotter | |
| from app.auth import make_auth_router | |
| from app.identity import RecognitionStore, hash_uid | |
| from app.care import CareState, CARE_ACTIONS | |
| from app.toys import toy_score, TOY_NAMES | |
| from app.relation import RelationStore | |
| from app.moments import MomentsStore, notable | |
| from app.autonomy import maybe_self_soothe | |
| from clanker_soul import Score | |
| app = FastAPI(title="Clanker Pet") | |
| app.add_middleware( | |
| SessionMiddleware, | |
| secret_key=os.environ.get("SESSION_SECRET") or secrets.token_hex(16), | |
| max_age=60 * 60 * 24 * 365, # 1 year: the anonymous-recognition cookie persists | |
| same_site="lax", | |
| https_only=True, # HF Spaces are HTTPS-only; mark the cookie Secure | |
| ) | |
| _DB = os.environ.get("SOUL_DB", "./data/soul.db") | |
| _STATE_REPO = os.environ.get("SOUL_STATE_REPO", "deucebucket/clanker-soul-state") | |
| restore_soul(_DB, _STATE_REPO) # boot: pull latest soul before opening it | |
| _bridge = SoulBridge(db_path=_DB, agent_id="clanker-pet-baby-v1") | |
| _snap = Snapshotter(db_path=_DB, repo_id=_STATE_REPO) | |
| _audit = AuditLog(repo_id="deucebucket/clanker-audit") | |
| _recog = RecognitionStore(_DB) | |
| _care = CareState(_DB) | |
| _relation = RelationStore(_DB) | |
| _moments = MomentsStore(_DB) | |
| _SESSION = uuid.uuid4().hex[:12] | |
| def _traffic() -> int: | |
| """Cumulative interaction count (soul events) — drives traffic-aware decay.""" | |
| try: | |
| return int(_bridge.maturity().get("events", 0)) | |
| except Exception: | |
| return 0 | |
| _care.rebase_legacy_traffic(_traffic()) # one-time: fix pre-traffic-column rows | |
| def _apply_neglect_once(): | |
| s = _care.neglect_score(_traffic()) | |
| if s: | |
| _bridge.ingest(s) | |
| _apply_neglect_once() | |
| app.include_router(make_auth_router()) | |
| class SayIn(BaseModel): | |
| text: str = Field(max_length=2000) | |
| class CareIn(BaseModel): | |
| action: str | |
| class ToyIn(BaseModel): | |
| toy: str | |
| def _state() -> dict: | |
| mood = _bridge.mood() | |
| soul = _bridge.soul_baseline() | |
| return { | |
| "appearance": mood_to_appearance(mood), | |
| "mood_word": mood_word(mood), | |
| "emoticon": emoticon(mood), | |
| "needs": _care.needs(_traffic()), | |
| "soul": soul, | |
| "distance": _bridge.soul_distance(), | |
| # default: no text-derived crisis (physical/idle states are calm). The | |
| # /say handler overrides this with the real read+structures concern. | |
| "crisis": {"value": 0.0, "label": "calm"}, | |
| } | |
| def health(): | |
| return {"status": "ok"} | |
| def snapshot(): | |
| state = _state() | |
| soothe = maybe_self_soothe(_bridge, state.get("needs"), time.time()) | |
| if soothe: | |
| _snap.maybe_snapshot() | |
| state = _state() # re-read mood after the self-soothe lift | |
| state["self_soothe"] = soothe # tells the client which item to go carry | |
| return state | |
| def _enrich_physical(state: dict, label: str, read: list[int], deltas: list[int]) -> None: | |
| """Toy/care interactions have no language to parse, but they DO push a Score | |
| through the soul. Surface the real read, deltas, armor and dominant move so the | |
| panels track the action instead of showing the previous chat message, and mark | |
| the trace as a physical (non-linguistic) interaction.""" | |
| state["read"] = read | |
| state["deltas"] = deltas | |
| state["acceptance"] = _bridge.last_acceptance() | |
| state["trace"] = {"words": [], "structures": [], "contributors": [], | |
| "kind": "physical", "label": label} | |
| state["why"] = f"{label}: " + _why(read, deltas) | |
| def care(body: CareIn): | |
| if body.action not in CARE_ACTIONS: | |
| raise HTTPException(status_code=422, detail=f"unknown action: {body.action!r}") | |
| s = _care.score_for(body.action) | |
| before = _bridge.mood() | |
| _bridge.ingest(s) | |
| _snap.maybe_snapshot() | |
| after = _bridge.mood() | |
| _care.do(body.action, _traffic()) # stamp fill AFTER this interaction counts | |
| read = [s.v, s.a, s.d, s.u, s.g, s.w, s.i] | |
| deltas = [after[i] - before[i] for i in range(7)] | |
| state = _state() | |
| _enrich_physical(state, f"you {body.action} it", read, deltas) | |
| state["line"] = f"you {body.action} it" | |
| return state | |
| def toy(body: ToyIn): | |
| if body.toy not in TOY_NAMES: | |
| raise HTTPException(status_code=422, detail=f"unknown toy: {body.toy!r}") | |
| s = toy_score(body.toy) | |
| before = _bridge.mood() | |
| _bridge.ingest(s) | |
| _snap.maybe_snapshot() | |
| after = _bridge.mood() | |
| read = [s.v, s.a, s.d, s.u, s.g, s.w, s.i] | |
| deltas = [after[i] - before[i] for i in range(7)] | |
| state = _state() | |
| _enrich_physical(state, body.toy, read, deltas) | |
| state["line"] = body.toy | |
| _audit.append(build_row( | |
| session=_SESSION, | |
| text=f"[toy:{body.toy}]", | |
| read=read, | |
| mood_before=before, | |
| mood_after=after, | |
| deltas=deltas, | |
| trace={"words": [], "structures": [], "contributors": []}, | |
| mood_word=state["mood_word"], | |
| ts=time.time(), | |
| )) | |
| return state | |
| def _why(read: list[int], deltas: list[int]) -> str: | |
| names = ["V", "A", "D", "U", "G", "W", "I"] | |
| big = max(range(7), key=lambda i: abs(deltas[i])) | |
| sign = "+" if deltas[big] >= 0 else "" | |
| return f"strongest move: {names[big]} {sign}{deltas[big]} (read {names[big]}={read[big]})" | |
| def say(body: SayIn, request: Request): | |
| text = body.text.strip() | |
| if not text: | |
| raise HTTPException(status_code=422, detail="empty text") | |
| score, raw, trace = score_with_trace(text, source="space:chat") | |
| _bridge.ingest(score) | |
| _snap.maybe_snapshot() | |
| mood = _bridge.mood() | |
| state = _state() | |
| state["read"] = raw | |
| state["deltas"] = _bridge.deltas() | |
| state["trace"] = shape_trace(trace) | |
| state["crisis"] = crisis_block(raw, state["trace"].get("structures")) | |
| state["why"] = _why(raw, state["deltas"]) | |
| state["simlish"] = simlish(mood) | |
| state["voice"] = voice_params(mood) | |
| state["acceptance"] = _bridge.last_acceptance() | |
| breached = bool(state["acceptance"].get("breached")) | |
| res = notable(raw, state["deltas"], breached=breached, crisis_emergency=False) | |
| if res: | |
| _moments.capture(res[0], raw, res[1]) | |
| # anonymous, sign-in-free recognition: every browser gets a hashed device id in | |
| # its (persistent) session cookie. No HF login, no username — just "this device". | |
| uid = request.session.get("uid") | |
| if not uid: | |
| uid = hash_uid(secrets.token_hex(16)) | |
| request.session["uid"] = uid | |
| state["recognition"] = _recog.see(uid) | |
| if not request.session.get("greeted") and _relation.known(uid): | |
| aff = _relation.affinity(uid) | |
| _bridge.ingest(Score(v=aff[0], a=aff[1], d=aff[2], u=aff[3], g=aff[4], w=aff[5], i=aff[6], source="relation:recall")) | |
| request.session["greeted"] = True | |
| _relation.observe(uid, raw, state["mood_word"]) | |
| aff = _relation.affinity(uid) | |
| state["relation"] = {"known": _relation.known(uid), "affinity_valence": (aff or [128])[0]} | |
| _audit.append(build_row( | |
| session=_SESSION, | |
| text=text, | |
| read=raw, | |
| mood_before=_bridge.previous_mood(), | |
| mood_after=_bridge.mood(), | |
| deltas=state["deltas"], | |
| trace=state["trace"], | |
| mood_word=state["mood_word"], | |
| ts=time.time(), | |
| )) | |
| return state | |
| def history(): | |
| return {"events": _bridge.history(limit=50)} | |
| def moments(): | |
| return {"events": _moments.recent(limit=20)} | |
| def llms_txt(): | |
| """Plain-text project explainer for LLMs/agents (llmstxt.org convention).""" | |
| return FileResponse("static/llms.txt", media_type="text/plain") | |
| def robots_txt(): | |
| return PlainTextResponse( | |
| "User-agent: *\nAllow: /\n\n# About this site: /llms.txt\n" | |
| ) | |
| # static frontend (Task 7). Mounted last so /health etc. win. | |
| if os.path.isdir("static"): | |
| def index(): | |
| return FileResponse("static/index.html") | |
| app.mount("/static", StaticFiles(directory="static"), name="static") | |