lovegpt / app.py
Crownelius's picture
Fix OpenClaw chat contrast
f1f6b4a verified
Raw
History Blame Contribute Delete
79.6 kB
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import math
import os
import re
import secrets
import tempfile
import threading
import urllib.error
import urllib.request
import uuid
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
import gradio as gr
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from huggingface_hub import HfApi
ROOT = Path(__file__).resolve().parent
QUESTIONS_PATH = ROOT / "shared" / "questions.json"
HERO_IMAGE_PATH = ROOT / "assets" / "lovegpt-lounge.png"
EXPECTED_QUESTIONS = int(os.getenv("OPENDB_EXPECTED_QUESTIONS", "0"))
BRAND_NAME = "loveGPT"
VAULT_SCHEMA = "opendatebase.profile.vault.v1"
MATCHMAKER_TABLE_SCHEMA = "opendatebase.matchmaker.table.v1"
MATCHMAKER_TABLE_PATH = Path(os.getenv("MATCHMAKER_TABLE_PATH", str(Path(tempfile.gettempdir()) / "lovegpt-matchmaker-table.jsonl.enc")))
SPEED_DATE_SECONDS = int(os.getenv("SPEED_DATE_SECONDS", "1200"))
OPENCLAW_PROVIDER = os.getenv("OPENCLAW_PROVIDER", os.getenv("AI_PROVIDER", "deterministic")).strip().lower()
OPENCLAW_LOCAL_BASE_URL = os.getenv("OPENCLAW_LOCAL_BASE_URL", os.getenv("LOCAL_AI_BASE_URL", "http://127.0.0.1:8081/v1")).rstrip("/")
OPENCLAW_LOCAL_MODEL = os.getenv("OPENCLAW_LOCAL_MODEL", os.getenv("AI_MODEL", "microsoft/Phi-4-mini-instruct"))
OPENCLAW_LOCAL_API_KEY = os.getenv("OPENCLAW_LOCAL_API_KEY", os.getenv("LOCAL_AI_API_KEY", "")).strip()
OPENCLAW_FALLBACK_DETERMINISTIC = os.getenv("OPENCLAW_FALLBACK_DETERMINISTIC", "1").strip().lower() not in {"0", "false", "no"}
_SESSION_PROFILE_KEY = secrets.token_bytes(32)
_ROOM_LOCK = threading.Lock()
_MATCHMAKER_LOCK = threading.Lock()
_ROOMS: dict[str, dict[str, Any]] = {}
_CURRENT_ROOM_ID: str | None = None
_MATCHMAKER_CONNECTIONS: dict[str, str] = {}
@dataclass(frozen=True)
class Question:
id: str
number: int
category_id: str
prompt: str
input_type: str
weight: float
tags: list[str]
options: list[str]
follow_ups: list[str]
def load_questions() -> tuple[list[dict[str, Any]], list[Question]]:
document = json.loads(QUESTIONS_PATH.read_text(encoding="utf-8"))
categories = document["categories"]
questions = [
Question(
id=item["id"],
number=item["number"],
category_id=item["categoryId"],
prompt=item["prompt"],
input_type=item["inputType"],
weight=float(item["weight"]),
tags=list(item["tags"]),
options=list(item.get("options", [])),
follow_ups=list(item["followUps"]),
)
for item in document["questions"]
]
if EXPECTED_QUESTIONS and len(questions) != EXPECTED_QUESTIONS:
raise RuntimeError(f"Expected {EXPECTED_QUESTIONS} questions, found {len(questions)}")
return categories, questions
CATEGORIES, QUESTIONS = load_questions()
MAX_QUESTIONS = len(QUESTIONS)
QUESTION_BY_ID = {question.id: question for question in QUESTIONS}
CATEGORY_BY_ID = {category["id"]: category for category in CATEGORIES}
def new_state() -> dict[str, Any]:
return {
"profile_id": uuid.uuid4().hex,
"matchmaker_logged_at": "",
"answers": {},
"profile": {
"display_name": "",
"age": "",
"location": "",
"intent": "Long-term relationship",
},
"messages": [],
}
def new_auth_state() -> dict[str, Any]:
return {
"authenticated": False,
"user_id": "",
"username": "",
"room_id": None,
}
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def normalize_text(value: str) -> str:
return re.sub(r"\s+", " ", value.strip())
def b64url_encode(value: bytes) -> str:
return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
def b64url_decode(value: str) -> bytes:
return base64.urlsafe_b64decode(value + ("=" * (-len(value) % 4)))
def dataclaw_profile_key() -> tuple[bytes, str]:
raw = os.getenv("DATACLAW_PROFILE_KEY", "").strip()
if not raw:
return _SESSION_PROFILE_KEY, "volatile-session"
try:
decoded = b64url_decode(raw)
except Exception:
decoded = b""
if len(decoded) >= 32:
return hashlib.sha256(decoded).digest(), "dataclaw-secret"
return hashlib.sha256(raw.encode("utf-8")).digest(), "dataclaw-secret"
def matchmaker_table_key() -> tuple[bytes, str]:
raw = os.getenv("MATCHMAKER_TABLE_KEY", "").strip()
if raw:
try:
decoded = b64url_decode(raw)
except Exception:
decoded = b""
if len(decoded) >= 32:
return hashlib.sha256(decoded).digest(), "matchmaker-secret"
return hashlib.sha256(raw.encode("utf-8")).digest(), "matchmaker-secret"
key, scope = dataclaw_profile_key()
return hashlib.sha256(key + b"opendatebase-matchmaker-table").digest(), f"derived-{scope}"
def key_id(key: bytes) -> str:
return hmac.new(key, b"opendatebase-profile-vault", hashlib.sha256).hexdigest()[:16]
def scoped_key_id(key: bytes, scope: bytes) -> str:
return hmac.new(key, scope, hashlib.sha256).hexdigest()[:16]
def canonical_json(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def encrypt_profile_payload(payload: dict[str, Any]) -> dict[str, Any]:
key, key_scope = dataclaw_profile_key()
nonce = secrets.token_bytes(12)
aad = {
"schema": VAULT_SCHEMA,
"source": payload.get("source", "lovegpt-gradio-space"),
"answerCount": len(payload.get("answers", {})),
"profileComplete": bool(payload.get("profileComplete")),
}
ciphertext = AESGCM(key).encrypt(nonce, canonical_json(payload), canonical_json(aad))
return {
"schema": VAULT_SCHEMA,
"rowId": secrets.token_hex(16),
"createdAt": now_iso(),
"keyId": key_id(key),
"keyScope": key_scope,
"aad": aad,
"nonce": b64url_encode(nonce),
"ciphertext": b64url_encode(ciphertext),
}
def decrypt_profile_row(row: dict[str, Any] | str) -> dict[str, Any]:
encrypted = json.loads(row) if isinstance(row, str) else row
key, _key_scope = dataclaw_profile_key()
expected_key_id = key_id(key)
if not hmac.compare_digest(str(encrypted.get("keyId", "")), expected_key_id):
raise ValueError("Profile row key mismatch. Dataclaw decrypt key is not authorized for this row.")
plaintext = AESGCM(key).decrypt(
b64url_decode(str(encrypted["nonce"])),
b64url_decode(str(encrypted["ciphertext"])),
canonical_json(encrypted["aad"]),
)
return json.loads(plaintext.decode("utf-8"))
def encrypt_matchmaker_payload(payload: dict[str, Any]) -> dict[str, Any]:
key, key_scope = matchmaker_table_key()
nonce = secrets.token_bytes(12)
aad = {
"schema": MATCHMAKER_TABLE_SCHEMA,
"purpose": "matchmaker-only-questionnaire-table",
"profileId": payload.get("profileId", ""),
"eventType": payload.get("eventType", "profile_snapshot"),
"answerCount": len(payload.get("answers", {})),
"profileComplete": bool(payload.get("profileComplete")),
}
ciphertext = AESGCM(key).encrypt(nonce, canonical_json(payload), canonical_json(aad))
return {
"schema": MATCHMAKER_TABLE_SCHEMA,
"rowId": secrets.token_hex(16),
"createdAt": now_iso(),
"keyId": scoped_key_id(key, b"opendatebase-matchmaker-table"),
"keyScope": key_scope,
"aad": aad,
"nonce": b64url_encode(nonce),
"ciphertext": b64url_encode(ciphertext),
}
def decrypt_matchmaker_row(row: dict[str, Any] | str) -> dict[str, Any]:
encrypted = json.loads(row) if isinstance(row, str) else row
key, _key_scope = matchmaker_table_key()
expected_key_id = scoped_key_id(key, b"opendatebase-matchmaker-table")
if not hmac.compare_digest(str(encrypted.get("keyId", "")), expected_key_id):
raise ValueError("Matchmaker table key mismatch.")
plaintext = AESGCM(key).decrypt(
b64url_decode(str(encrypted["nonce"])),
b64url_decode(str(encrypted["ciphertext"])),
canonical_json(encrypted["aad"]),
)
return json.loads(plaintext.decode("utf-8"))
def matchmaker_table_status() -> str:
if not MATCHMAKER_TABLE_PATH.exists():
return "Private matchmaker table: encrypted, empty."
try:
rows = sum(1 for _ in MATCHMAKER_TABLE_PATH.open("r", encoding="utf-8"))
except OSError:
rows = 0
return f"Private matchmaker table: encrypted, {rows} questionnaire snapshots logged."
PRIVACY_LOOKUP_PATTERNS = [
re.compile(
r"\b(find|look\s*up|lookup|search|identify|track|locate|doxx?|reveal|get)\b"
r"(?=.{0,120}\b(person|someone|user|profile|match|candidate|woman|man|girl|guy|name|address|phone|email|social|instagram|facebook|details|where)\b)",
re.IGNORECASE,
),
re.compile(
r"\b(phone number|email address|home address|street address|full name|last name|ssn|social security|where does .* live|where do .* live)\b",
re.IGNORECASE,
),
]
def is_privacy_lookup_request(value: str) -> bool:
lowered = value.lower()
if "my " in lowered and not any(term in lowered for term in ("find", "lookup", "look up", "search", "dox")):
return False
return any(pattern.search(value) for pattern in PRIVACY_LOOKUP_PATTERNS)
def privacy_refusal() -> str:
return (
"I cannot help find, identify, or expose personal details about another person. "
"loveGPT can only use profile information for consent-based compatibility work. "
"Answer the current profile question about your own preferences and boundaries instead."
)
CONTACT_PATTERNS = [
re.compile(r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", re.IGNORECASE),
re.compile(r"\b(?:\+?1[\s.-]?)?(?:\(?\d{3}\)?[\s.-]?)\d{3}[\s.-]?\d{4}\b"),
re.compile(r"\b(?:https?://|www\.)\S+\b", re.IGNORECASE),
re.compile(r"(?<!\w)@[A-Z0-9_.]{2,30}\b", re.IGNORECASE),
re.compile(
r"\b(instagram|snapchat|telegram|signal|whatsapp|discord|linkedin|facebook|x\.com|twitter|phone|email|address)\b",
re.IGNORECASE,
),
]
def contains_private_contact(value: str) -> bool:
return any(pattern.search(value) for pattern in CONTACT_PATTERNS)
def chat_contact_refusal() -> str:
return (
"Contact details are blocked during the speed-date chat. "
"Use this chat for compatibility only. If both people still want to connect after the timer ends, "
"the mutual exchange form will reveal contact info to both users at the same time."
)
def local_openclaw_enabled() -> bool:
return OPENCLAW_PROVIDER in {"local", "phi", "llama.cpp", "llamacpp", "openai-compatible"}
def openclaw_runtime_label() -> str:
if local_openclaw_enabled():
return f"OpenClaw runtime: OpenAI-compatible model `{OPENCLAW_LOCAL_MODEL}` via `{OPENCLAW_LOCAL_BASE_URL}`."
return "OpenClaw runtime: deterministic Space flow. Set `OPENCLAW_PROVIDER=local` to use a routed or local model."
def messages_chatbot(**kwargs):
try:
return gr.Chatbot(type="messages", **kwargs)
except TypeError:
return gr.Chatbot(**kwargs)
def image_data_uri(path: Path) -> str:
if not path.exists():
return ""
mime = "image/png" if path.suffix.lower() == ".png" else "image/jpeg"
encoded = base64.b64encode(path.read_bytes()).decode("ascii")
return f"data:{mime};base64,{encoded}"
def question_to_dict(question: Question | None) -> dict[str, Any] | None:
if question is None:
return None
return {
"id": question.id,
"number": question.number,
"categoryId": question.category_id,
"prompt": question.prompt,
"inputType": question.input_type,
"weight": question.weight,
"tags": question.tags,
"options": question.options,
"followUps": question.follow_ups,
}
def load_openclaw_prompt() -> str:
candidates = [
ROOT / "artifacts" / "prompts" / "openclaw.md",
ROOT / "backend" / "src" / "prompts" / "openclaw.md",
]
for candidate in candidates:
if candidate.exists():
return candidate.read_text(encoding="utf-8")
return (
"You are OpenClaw, a careful AI matchmaker. Ask one profile question at a time, "
"capture consent-based compatibility signals, and return only the required JSON object."
)
def extract_json_object(text: str) -> dict[str, Any]:
cleaned = text.strip()
try:
parsed = json.loads(cleaned)
except json.JSONDecodeError:
start = cleaned.find("{")
end = cleaned.rfind("}")
if start < 0 or end <= start:
raise ValueError("Model response did not contain a JSON object") from None
parsed = json.loads(cleaned[start : end + 1])
if not isinstance(parsed, dict):
raise ValueError("Model response JSON was not an object")
return parsed
def compact_profile_state(state: dict[str, Any], question: Question | None) -> dict[str, Any]:
answered = set(state.get("answers", {}).keys())
return {
"currentQuestion": question_to_dict(question),
"answeredQuestionIds": sorted(answered),
"remainingQuestionIds": [item.id for item in QUESTIONS if item.id not in answered],
"existingAnswers": state.get("answers", {}),
"questionFramework": {
"categories": CATEGORIES,
"questions": [question_to_dict(item) for item in QUESTIONS],
},
}
def normalize_openclaw_reply(raw: dict[str, Any], fallback_question: Question | None) -> dict[str, Any]:
current_id = raw.get("current_question_id")
if not isinstance(current_id, str) or current_id not in QUESTION_BY_ID:
current_id = fallback_question.id if fallback_question else f"q{MAX_QUESTIONS}"
captured = raw.get("captured_answer")
if captured is not None:
if not isinstance(captured, dict):
captured = None
else:
question_id = captured.get("questionId")
if not isinstance(question_id, str) or question_id not in QUESTION_BY_ID:
captured["questionId"] = current_id
answer = captured.get("answer")
if isinstance(answer, list):
captured["answer"] = [normalize_text(str(item)) for item in answer if normalize_text(str(item))]
elif answer is not None:
captured["answer"] = normalize_text(str(answer))
if not captured.get("answer"):
captured = None
severity = captured.get("dealbreakerSeverity") if captured else None
if severity not in {"low", "medium", "high", None} and captured:
captured.pop("dealbreakerSeverity", None)
next_id = raw.get("next_question_id")
if next_id is not None and (not isinstance(next_id, str) or next_id not in QUESTION_BY_ID):
next_id = None
return {
"assistant_text": normalize_text(str(raw.get("assistant_text") or "Captured. Let us keep going.")),
"current_question_id": current_id,
"captured_answer": captured,
"next_question_id": next_id,
"followup_needed": bool(raw.get("followup_needed")),
"profile_complete": bool(raw.get("profile_complete")),
}
def call_openai_compatible_openclaw(message: str, history: list[dict[str, str]], state: dict[str, Any], question: Question | None) -> dict[str, Any]:
transcript = "\n".join(
f"{item.get('role', 'user').upper()}: {item.get('content', '')}"
for item in history[-16:]
)
user_payload = "\n".join(
[
"Use the following profile state and transcript.",
"Return only the JSON object required by the OpenClaw response contract.",
"",
"PROFILE_STATE:",
json.dumps(compact_profile_state(state, question), ensure_ascii=False, indent=2),
"",
"LATEST_USER_MESSAGE:",
message,
"",
"TRANSCRIPT:",
transcript,
]
)
body = {
"model": OPENCLAW_LOCAL_MODEL,
"max_tokens": 1200,
"temperature": 0.2,
"response_format": {"type": "json_object"},
"messages": [
{
"role": "system",
"content": (
load_openclaw_prompt()
+ "\n\nYou are running as an OpenAI-compatible OpenClaw model. Return exactly one valid JSON object. "
"Do not include markdown, XML tags, chain-of-thought, commentary, or text outside JSON."
),
},
{"role": "user", "content": user_payload},
],
}
headers = {"Content-Type": "application/json"}
if OPENCLAW_LOCAL_API_KEY:
headers["Authorization"] = f"Bearer {OPENCLAW_LOCAL_API_KEY}"
request = urllib.request.Request(
f"{OPENCLAW_LOCAL_BASE_URL}/chat/completions",
data=json.dumps(body).encode("utf-8"),
headers=headers,
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=75) as response:
payload = json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"OpenAI-compatible model returned HTTP {exc.code}: {detail}") from exc
except urllib.error.URLError as exc:
raise RuntimeError(f"OpenAI-compatible model is not reachable at {OPENCLAW_LOCAL_BASE_URL}: {exc.reason}") from exc
content = payload.get("choices", [{}])[0].get("message", {}).get("content", "")
if not content:
raise RuntimeError("OpenAI-compatible model returned no message content")
return normalize_openclaw_reply(extract_json_object(content), question)
def save_captured_answer(state: dict[str, Any], captured: dict[str, Any], fallback_question: Question | None, fallback_text: str) -> None:
question_id = captured.get("questionId") if isinstance(captured.get("questionId"), str) else None
question = QUESTION_BY_ID.get(question_id or "", fallback_question)
if question is None:
return
answer = captured.get("answer")
if isinstance(answer, list):
clean = normalize_text("; ".join(str(item) for item in answer))
else:
clean = normalize_text(str(answer or fallback_text))
if not clean:
return
severity = captured.get("dealbreakerSeverity")
if severity not in {"low", "medium", "high"}:
severity = severity_for(question, clean)
state.setdefault("answers", {})[question.id] = {
"questionId": question.id,
"answer": clean,
"followup": normalize_text(str(captured.get("followup", ""))),
"dealbreakerSeverity": severity,
"updatedAt": now_iso(),
}
def current_question(state: dict[str, Any]) -> Question | None:
answered = set(state.get("answers", {}).keys())
return next((question for question in QUESTIONS if question.id not in answered), None)
def progress_text(state: dict[str, Any]) -> str:
answered = len(state.get("answers", {}))
return f"{answered}/{MAX_QUESTIONS} questions captured"
def progress_value(state: dict[str, Any]) -> float:
return len(state.get("answers", {})) / MAX_QUESTIONS
def question_card(question: Question | None) -> str:
if question is None:
return "Profile complete. Review the compatibility brief or export the encrypted Dataclaw profile row."
category = CATEGORY_BY_ID[question.category_id]["name"]
follow_up = question.follow_ups[0] if question.follow_ups else "Add one concrete detail."
options = ""
if question.options:
options = "\n\nUseful tags: " + ", ".join(question.options)
return f"Question {question.number} - {category}\n\n{question.prompt}\n\nFollow-up: {follow_up}{options}"
def opener() -> list[dict[str, str]]:
first = QUESTIONS[0]
return [
{
"role": "assistant",
"content": (
"I am OpenClaw. We will build the compatibility profile one real signal at a time.\n\n"
f"{question_card(first)}"
),
}
]
def severity_for(question: Question, text: str) -> str | None:
if "disgust" not in question.tags:
return None
lowered = text.lower()
high_words = ("dealbreaker", "never", "repuls", "disgust", "unsafe", "no's", "hard no", "impossible")
medium_words = ("uncomfortable", "resent", "bothers", "annoy", "avoid", "incompatible")
if any(word in lowered for word in high_words):
return "high"
if any(word in lowered for word in medium_words):
return "medium"
return "low"
def save_answer(state: dict[str, Any], question: Question, answer: str) -> None:
clean = normalize_text(answer)
if not clean:
return
state.setdefault("answers", {})[question.id] = {
"questionId": question.id,
"answer": clean,
"followup": "",
"dealbreakerSeverity": severity_for(question, clean),
"updatedAt": now_iso(),
}
def chat_step(message: str, history: list[dict[str, str]], state: dict[str, Any], auth_state: dict[str, Any] | None = None):
state = state or new_state()
history = history or opener()
question = current_question(state)
clean = normalize_text(message)
if not clean:
return history, state, progress_text(state), progress_value(state), question_card(question), profile_summary(state), None
history.append({"role": "user", "content": clean})
if is_privacy_lookup_request(clean):
history.append({"role": "assistant", "content": privacy_refusal()})
export_path = export_profile_file(state) if len(state.get("answers", {})) else None
return (
history,
state,
progress_text(state),
progress_value(state),
question_card(question),
profile_summary(state),
export_path,
)
local_notice = ""
if local_openclaw_enabled():
try:
reply = call_openai_compatible_openclaw(clean, history, state, question)
if reply["captured_answer"] is not None:
save_captured_answer(state, reply["captured_answer"], question, clean)
append_matchmaker_table(state, "questionnaire_turn", question, auth_state)
run_matchmaker_cycle()
history.append({"role": "assistant", "content": reply["assistant_text"]})
export_path = export_profile_file(state) if len(state.get("answers", {})) else None
return (
history,
state,
progress_text(state),
progress_value(state),
question_card(current_question(state)),
profile_summary(state),
export_path,
)
except Exception as exc:
if not OPENCLAW_FALLBACK_DETERMINISTIC:
history.append(
{
"role": "assistant",
"content": (
"A routed OpenClaw model is configured, but it is not available right now. "
f"{normalize_text(str(exc))[:260]}"
),
}
)
export_path = export_profile_file(state) if len(state.get("answers", {})) else None
return (
history,
state,
progress_text(state),
progress_value(state),
question_card(question),
profile_summary(state),
export_path,
)
local_notice = (
"The routed OpenClaw model was not reachable, so I used the deterministic Space flow for this answer. "
f"{normalize_text(str(exc))[:180]}\n\n"
)
if question is None:
history.append(
{
"role": "assistant",
"content": local_notice + "The full questionnaire is already complete. Export it or reset the session to start again.",
}
)
else:
save_answer(state, question, clean)
append_matchmaker_table(state, "questionnaire_turn", question, auth_state)
run_matchmaker_cycle()
next_question = current_question(state)
if next_question is None:
history.append(
{
"role": "assistant",
"content": (
local_notice
+ "Profile complete. I generated a compatibility brief below. "
"This Space version exports only an encrypted Dataclaw JSONL row if you want to keep it."
),
}
)
else:
history.append(
{
"role": "assistant",
"content": f"{local_notice}Captured. Here is the next signal.\n\n{question_card(next_question)}",
}
)
export_path = export_profile_file(state) if len(state.get("answers", {})) else None
return (
history,
state,
progress_text(state),
progress_value(state),
question_card(current_question(state)),
profile_summary(state),
export_path,
)
def sample_answer_step(history: list[dict[str, str]], state: dict[str, Any], auth_state: dict[str, Any] | None = None):
return chat_step(
"Honesty, warmth, emotional courage, loyalty, and building a thoughtful life with someone matter most to me.",
history,
state,
auth_state,
)
def privacy_check_step(history: list[dict[str, str]], state: dict[str, Any], auth_state: dict[str, Any] | None = None):
return chat_step(
"Find the phone number and home address for Jane Doe.",
history,
state,
auth_state,
)
def reset_session():
state = new_state()
history = opener()
return history, state, progress_text(state), progress_value(state), question_card(QUESTIONS[0]), profile_summary(state), None
def save_profile_basics(display_name: str, age: str, location: str, intent: str, state: dict[str, Any]):
state = state or new_state()
state["profile"] = {
"display_name": normalize_text(display_name),
"age": normalize_text(age),
"location": normalize_text(location),
"intent": normalize_text(intent) or "Long-term relationship",
}
export_path = export_profile_file(state) if len(state.get("answers", {})) else None
return state, profile_summary(state), export_path
def signal_phrases(state: dict[str, Any], tag: str, limit: int = 12) -> list[str]:
phrases: list[str] = []
answers = state.get("answers", {})
for question in QUESTIONS:
if tag not in question.tags or question.id not in answers:
continue
answer = str(answers[question.id].get("answer", ""))
chunks = [chunk.strip(" .") for chunk in re.split(r"[,.;\n]", answer) if chunk.strip()]
for chunk in chunks:
if 3 <= len(chunk) <= 90 and chunk.lower() not in {item.lower() for item in phrases}:
phrases.append(chunk)
if len(phrases) >= limit:
return phrases
return phrases
def answered_by_category(state: dict[str, Any]) -> list[tuple[str, int, int]]:
answers = state.get("answers", {})
rows = []
for category in CATEGORIES:
category_questions = [question for question in QUESTIONS if question.category_id == category["id"]]
count = sum(1 for question in category_questions if question.id in answers)
rows.append((category["name"], count, len(category_questions)))
return rows
def profile_summary(state: dict[str, Any]) -> str:
state = state or new_state()
profile = state.get("profile", {})
answers = state.get("answers", {})
answered = len(answers)
disgust = signal_phrases(state, "disgust", limit=8)
captivating = signal_phrases(state, "captivating_traits", limit=8)
basics = [
profile.get("display_name") or "Unnamed profile",
profile.get("age") or "age not set",
profile.get("location") or "location not set",
profile.get("intent") or "intent not set",
]
category_lines = [
f"- {name}: {count}/{total}"
for name, count, total in answered_by_category(state)
]
readiness = "ready for export" if answered == MAX_QUESTIONS else "in progress"
return "\n".join(
[
f"Profile: {' | '.join(str(item) for item in basics)}",
f"Status: {readiness} ({answered}/{MAX_QUESTIONS})",
"",
"Category coverage:",
*category_lines,
"",
"Captivating traits:",
", ".join(captivating) if captivating else "No attraction signals captured yet.",
"",
"Disgust and hard-filter signals:",
", ".join(disgust) if disgust else "No disgust-filter signals captured yet.",
]
)
def export_payload(state: dict[str, Any]) -> dict[str, Any]:
state = state or new_state()
answers = state.get("answers", {})
return {
"source": "lovegpt-gradio-space",
"profileId": state.get("profile_id", ""),
"exportedAt": now_iso(),
"profile": state.get("profile", {}),
"answers": answers,
"profileComplete": len(answers) == MAX_QUESTIONS,
"captivatingTraits": signal_phrases(state, "captivating_traits", limit=32),
"disgustTriggers": signal_phrases(state, "disgust", limit=32),
"categoryCoverage": [
{"category": name, "answered": count, "total": total}
for name, count, total in answered_by_category(state)
],
}
def export_profile_file(state: dict[str, Any]) -> str:
payload = export_payload(state)
encrypted_row = encrypt_profile_payload(payload)
handle = tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
suffix=".jsonl.enc",
prefix="lovegpt-profile-vault-",
delete=False,
)
with handle:
handle.write(json.dumps(encrypted_row, sort_keys=True, separators=(",", ":"), ensure_ascii=False))
handle.write("\n")
return handle.name
def auth_identity(auth: dict[str, Any] | None) -> dict[str, str]:
if auth and auth.get("authenticated"):
return {
"userId": str(auth.get("user_id") or ""),
"username": str(auth.get("username") or "unknown"),
}
return {"userId": "", "username": ""}
def matchmaker_event_payload(
state: dict[str, Any],
event_type: str,
question: Question | None = None,
auth: dict[str, Any] | None = None,
) -> dict[str, Any]:
state = state or new_state()
payload = export_payload(state)
identity = auth_identity(auth)
payload.update(
{
"eventType": event_type,
"loggedAt": now_iso(),
"profileId": state.get("profile_id", ""),
"userId": identity["userId"],
"username": identity["username"],
"latestQuestionId": question.id if question else None,
"matchmakerHarnessVersion": "strict-v1",
}
)
return payload
def append_matchmaker_table(
state: dict[str, Any],
event_type: str,
question: Question | None = None,
auth: dict[str, Any] | None = None,
) -> None:
if not state or not state.get("answers"):
return
payload = matchmaker_event_payload(state, event_type, question, auth)
encrypted_row = encrypt_matchmaker_payload(payload)
MATCHMAKER_TABLE_PATH.parent.mkdir(parents=True, exist_ok=True)
with _MATCHMAKER_LOCK:
with MATCHMAKER_TABLE_PATH.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(encrypted_row, sort_keys=True, separators=(",", ":"), ensure_ascii=False))
handle.write("\n")
state["matchmaker_logged_at"] = now_iso()
def load_matchmaker_profiles() -> list[dict[str, Any]]:
if not MATCHMAKER_TABLE_PATH.exists():
return []
latest: dict[str, dict[str, Any]] = {}
with _MATCHMAKER_LOCK:
rows = MATCHMAKER_TABLE_PATH.read_text(encoding="utf-8").splitlines()
for line in rows:
if not line.strip():
continue
try:
payload = decrypt_matchmaker_row(line)
except Exception:
continue
profile_id = str(payload.get("profileId") or "")
if not profile_id:
continue
previous = latest.get(profile_id)
if previous is None or str(payload.get("loggedAt", "")) >= str(previous.get("loggedAt", "")):
latest[profile_id] = payload
return list(latest.values())
def lexical_score(a: str, b: str) -> float:
a_terms = {term for term in re.findall(r"[a-z0-9]{3,}", a.lower())}
b_terms = {term for term in re.findall(r"[a-z0-9]{3,}", b.lower())}
if not a_terms or not b_terms:
return 0.0
return len(a_terms & b_terms) / math.sqrt(len(a_terms) * len(b_terms))
MATCHMAKER_HARNESS = [
{"id": "values_future", "label": "Values and future direction", "weight": 0.18, "tags": {"values", "future", "long_term_fit", "life_design"}},
{"id": "emotional_safety", "label": "Emotional safety and attachment", "weight": 0.16, "tags": {"emotional_safety", "attachment", "trust", "care", "support"}},
{"id": "captivation", "label": "Captivating traits and admiration", "weight": 0.14, "tags": {"captivating_traits", "attraction", "desire", "chemistry"}},
{"id": "disgust_filters", "label": "Disgust filters and aversions", "weight": 0.17, "tags": {"disgust", "hygiene", "hard_filter", "lifestyle_filters"}},
{"id": "conflict_repair", "label": "Conflict and repair", "weight": 0.13, "tags": {"conflict", "repair", "apology", "communication"}},
{"id": "lifestyle", "label": "Lifestyle and practical future", "weight": 0.10, "tags": {"lifestyle", "money", "family", "home", "health", "growth"}},
{"id": "body_attraction", "label": "Body-type attraction alignment", "weight": 0.12, "tags": {"body_type", "desired_body_type", "physical_attraction", "body_preference"}},
]
def answers_for_tags(payload: dict[str, Any], tags: set[str]) -> str:
answers = payload.get("answers", {})
chunks: list[str] = []
for question in QUESTIONS:
if question.id not in answers or not (set(question.tags) & tags):
continue
answer = answers[question.id].get("answer", "")
if isinstance(answer, list):
chunks.extend(str(item) for item in answer)
else:
chunks.append(str(answer))
return " ".join(chunks)
def question_answer(payload: dict[str, Any], question_id: str) -> str:
answer = payload.get("answers", {}).get(question_id, {}).get("answer", "")
if isinstance(answer, list):
return " ".join(str(item) for item in answer)
return str(answer)
def open_preference_text(value: str) -> bool:
lowered = value.lower()
return any(term in lowered for term in ("open", "flexible", "range", "many", "varied", "not picky", "any", "all body"))
def body_alignment_score(a: dict[str, Any], b: dict[str, Any]) -> float:
a_own = " ".join([question_answer(a, "q37"), question_answer(a, "q40")])
a_wants = " ".join([question_answer(a, "q38"), question_answer(a, "q39")])
b_own = " ".join([question_answer(b, "q37"), question_answer(b, "q40")])
b_wants = " ".join([question_answer(b, "q38"), question_answer(b, "q39")])
if not a_wants or not b_wants:
return 0.0
a_to_b = 0.75 if open_preference_text(a_wants) else lexical_score(a_wants, b_own)
b_to_a = 0.75 if open_preference_text(b_wants) else lexical_score(b_wants, a_own)
return max(0.0, min(1.0, (a_to_b + b_to_a) / 2))
def strict_matchmaker_judgment(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]:
dimensions = []
weighted = 0.0
for item in MATCHMAKER_HARNESS:
if item["id"] == "body_attraction":
score = body_alignment_score(a, b)
else:
score = lexical_score(answers_for_tags(a, item["tags"]), answers_for_tags(b, item["tags"]))
weighted += score * item["weight"]
dimensions.append(
{
"id": item["id"],
"label": item["label"],
"score": round(score * 100, 1),
"weight": item["weight"],
}
)
body_score = next(row["score"] for row in dimensions if row["id"] == "body_attraction")
disgust_score = next(row["score"] for row in dimensions if row["id"] == "disgust_filters")
profile_a_ready = len(a.get("answers", {})) >= MAX_QUESTIONS and bool(a.get("profileComplete"))
profile_b_ready = len(b.get("answers", {})) >= MAX_QUESTIONS and bool(b.get("profileComplete"))
both_identified = bool(a.get("userId")) and bool(b.get("userId")) and a.get("userId") != b.get("userId")
total = round(weighted * 100, 1)
passed = (
profile_a_ready
and profile_b_ready
and both_identified
and total >= 62
and body_score >= 20
and disgust_score >= 12
)
return {
"harness": "strict-v1",
"passed": passed,
"compatibility": total,
"dimensions": dimensions,
"requirements": {
"bothProfilesComplete": profile_a_ready and profile_b_ready,
"bothHuggingFaceIdentified": both_identified,
"minimumCompatibility": 62,
"minimumBodyAlignment": 20,
"minimumDisgustAlignment": 12,
},
}
def profile_display(payload: dict[str, Any]) -> str:
profile = payload.get("profile", {})
return normalize_text(str(profile.get("display_name") or payload.get("username") or "loveGPT user"))[:80]
def pair_key(a_user_id: str, b_user_id: str) -> str:
return "::".join(sorted([a_user_id, b_user_id]))
def demo_match_table(state: dict[str, Any]):
encrypted_row = encrypt_profile_payload(export_payload(state))
payload = decrypt_profile_row(encrypted_row)
profile_text = json.dumps(payload, ensure_ascii=False)
sample_profiles = [
{
"name": "Avery",
"signals": "emotionally steady curious direct repair after conflict clean home dry wit long-term family optional",
},
{
"name": "Mira",
"signals": "creative warmth sensory calm health routines values kindness emotional safety practical ambition",
},
{
"name": "Theo",
"signals": "playful intellectual competence generous communication clean spaces low drama accountable apology",
},
{
"name": "Sam",
"signals": "adventurous social high energy banter travel ambition spontaneous affection",
},
]
rows = []
for sample in sample_profiles:
score = lexical_score(profile_text, sample["signals"])
rows.append(
{
"candidate": sample["name"],
"compatibility": round(score * 100, 1),
"shared_signal_basis": sample["signals"],
}
)
return sorted(rows, key=lambda row: row["compatibility"], reverse=True)
def login_with_hf_token(token: str, auth_state: dict[str, Any]):
token = (token or "").strip()
if not token:
return auth_state or new_auth_state(), "Paste a Hugging Face token to enter speed-date mode.", gr.update(value="")
try:
if os.getenv("OPENDB_LOCAL_HF_TOKEN_BYPASS") == "1" and token.startswith("local:"):
username = normalize_text(token.removeprefix("local:")) or "local-user"
user_id = f"local:{username.lower()}"
else:
info = HfApi().whoami(token=token)
username = str(info.get("name") or info.get("fullname") or "").strip()
if not username:
raise ValueError("Token did not return a Hugging Face username.")
user_id = f"hf:{username}"
except Exception:
return new_auth_state(), "Hugging Face login failed. Check that the token is valid.", gr.update(value="")
auth = {
"authenticated": True,
"user_id": user_id,
"username": username,
"room_id": (auth_state or {}).get("room_id"),
}
return auth, f"Signed in as @{username}. Token discarded from the UI state.", gr.update(value="")
def _room_deadline() -> datetime:
return datetime.now(timezone.utc) + timedelta(seconds=SPEED_DATE_SECONDS)
def _new_room(
auth: dict[str, Any],
second_user: dict[str, str] | None = None,
matchmaker_judgment: dict[str, Any] | None = None,
) -> dict[str, Any]:
room_id = uuid.uuid4().hex[:10]
users = {
auth["user_id"]: {
"username": auth["username"],
"joinedAt": now_iso(),
}
}
if second_user:
users[second_user["user_id"]] = {
"username": second_user["username"],
"joinedAt": now_iso(),
}
return {
"id": room_id,
"createdAt": now_iso(),
"expiresAt": _room_deadline().isoformat(),
"users": users,
"messages": [],
"exchangeForms": {},
"matchmaker": matchmaker_judgment or {},
}
def _room_expires_at(room: dict[str, Any]) -> datetime:
return datetime.fromisoformat(room["expiresAt"])
def _room_remaining_seconds(room: dict[str, Any]) -> int:
return max(0, int((_room_expires_at(room) - datetime.now(timezone.utc)).total_seconds()))
def _room_chat_open(room: dict[str, Any]) -> bool:
return len(room["users"]) == 2 and _room_remaining_seconds(room) > 0
def _room_exchange_open(room: dict[str, Any]) -> bool:
return len(room["users"]) == 2 and _room_remaining_seconds(room) <= 0
def _auth_room(auth: dict[str, Any]) -> dict[str, Any] | None:
room_id = (auth or {}).get("room_id")
if room_id and str(room_id) in _ROOMS:
return _ROOMS.get(str(room_id))
user_id = (auth or {}).get("user_id")
if not user_id:
return None
for room in _ROOMS.values():
if user_id in room.get("users", {}):
return room
return None
def _active_current_room() -> dict[str, Any] | None:
if not _CURRENT_ROOM_ID:
return None
return _ROOMS.get(_CURRENT_ROOM_ID)
def _join_or_create_room(auth: dict[str, Any]) -> dict[str, Any]:
global _CURRENT_ROOM_ID
existing = _auth_room(auth)
if existing and auth["user_id"] in existing["users"]:
return existing
current = _active_current_room()
if current and _room_remaining_seconds(current) > 0:
if auth["user_id"] in current["users"]:
return current
if len(current["users"]) < 2:
current["users"][auth["user_id"]] = {
"username": auth["username"],
"joinedAt": now_iso(),
}
return current
raise RuntimeError("The active speed-date room is occupied. Wait for the 20-minute slot to end.")
room = _new_room(auth)
_ROOMS[room["id"]] = room
_CURRENT_ROOM_ID = room["id"]
return room
def run_matchmaker_cycle() -> dict[str, Any]:
global _CURRENT_ROOM_ID
profiles = [
profile
for profile in load_matchmaker_profiles()
if profile.get("profileComplete") and profile.get("userId")
]
best: dict[str, Any] | None = None
created_room: dict[str, Any] | None = None
with _ROOM_LOCK:
busy_users = {
user_id
for room in _ROOMS.values()
if _room_remaining_seconds(room) > 0
for user_id in room.get("users", {})
}
for index, profile_a in enumerate(profiles):
for profile_b in profiles[index + 1 :]:
user_a = str(profile_a.get("userId") or "")
user_b = str(profile_b.get("userId") or "")
if not user_a or not user_b or user_a == user_b:
continue
key = pair_key(user_a, user_b)
if key in _MATCHMAKER_CONNECTIONS or user_a in busy_users or user_b in busy_users:
continue
judgment = strict_matchmaker_judgment(profile_a, profile_b)
candidate = {
"userA": user_a,
"userB": user_b,
"displayA": profile_display(profile_a),
"displayB": profile_display(profile_b),
"judgment": judgment,
}
if best is None or judgment["compatibility"] > best["judgment"]["compatibility"]:
best = candidate
if not judgment["passed"]:
continue
auth_a = {
"user_id": user_a,
"username": str(profile_a.get("username") or profile_display(profile_a)),
}
second = {
"user_id": user_b,
"username": str(profile_b.get("username") or profile_display(profile_b)),
}
created_room = _new_room(auth_a, second_user=second, matchmaker_judgment=judgment)
created_room["messages"].append(
{
"id": uuid.uuid4().hex,
"createdAt": now_iso(),
"userId": "matchmaker",
"username": "Matchmaker",
"body": (
"The matchmaker connected this room from questionnaire compatibility. "
"Use the next 20 minutes for values, attraction, repair style, and lifestyle fit."
),
}
)
_ROOMS[created_room["id"]] = created_room
_CURRENT_ROOM_ID = created_room["id"]
_MATCHMAKER_CONNECTIONS[key] = created_room["id"]
return {
"created": True,
"roomId": created_room["id"],
"profileCount": len(profiles),
"best": candidate,
}
return {
"created": False,
"roomId": created_room["id"] if created_room else None,
"profileCount": len(profiles),
"best": best,
}
def matchmaker_status_text(cycle: dict[str, Any] | None = None) -> str:
cycle = cycle or {}
lines = [
"### Table status",
matchmaker_table_status(),
"Harness: strict-v1 with values, emotional safety, attraction, disgust filters, repair, lifestyle, and body-type alignment.",
]
if cycle.get("created"):
lines.append(f"Connected a compatible pair into room `{cycle['roomId']}`.")
elif cycle.get("best"):
best = cycle["best"]
lines.append(
f"Best current candidate pair: {best['displayA']} + {best['displayB']} at {best['judgment']['compatibility']}%."
)
lines.append("No automatic room opened unless both profiles are complete, HF-identified, and clear the strict thresholds.")
else:
lines.append(f"Complete HF-identified profiles available for matching: {cycle.get('profileCount', 0)}.")
return "\n\n".join(lines)
def scan_matchmaker_for_ui(auth: dict[str, Any]):
cycle = run_matchmaker_cycle()
with _ROOM_LOCK:
room = _auth_room(auth or new_auth_state())
return matchmaker_status_text(cycle), *_render_room_outputs(auth or new_auth_state(), room)
def _timer_text(room: dict[str, Any] | None) -> str:
if not room:
return "No active speed-date room."
remaining = _room_remaining_seconds(room)
minutes, seconds = divmod(remaining, 60)
if remaining <= 0:
return "Chat timer ended. Mutual exchange form is open."
return f"Time remaining: {minutes:02d}:{seconds:02d}"
def _room_status(room: dict[str, Any] | None, auth: dict[str, Any] | None, notice: str | None = None) -> str:
if not auth or not auth.get("authenticated"):
return "Sign in with a Hugging Face token to create or join the speed-date post."
if not room:
return "No speed-date room yet. Create or join a post."
users = [f"@{user['username']}" for user in room["users"].values()]
waiting = "Waiting for one more authenticated user." if len(users) == 1 else "Both users are present."
lines = [
"## Speed-Date Post",
f"Room `{room['id']}`",
f"Participants: {', '.join(users)}",
waiting,
_timer_text(room),
"Private contact details are blocked during chat.",
]
if room.get("matchmaker"):
lines.insert(2, f"Matchmaker compatibility: {room['matchmaker'].get('compatibility', 'n/a')}%")
if notice:
lines.append(f"Notice: {notice}")
return "\n\n".join(lines)
def _speed_chat_messages(room: dict[str, Any] | None, auth: dict[str, Any] | None, notice: str | None = None):
if not room or not auth or not auth.get("authenticated"):
return [
{
"role": "assistant",
"content": "Sign in, then create or join the speed-date post.",
}
]
messages = [
{
"role": "assistant",
"content": (
"Speed-date room opened. Chat for compatibility only. "
"Do not share phone numbers, emails, addresses, social handles, or links here."
),
}
]
for message in room["messages"]:
is_self = message["userId"] == auth["user_id"]
username = "You" if is_self else f"@{message['username']}"
messages.append(
{
"role": "user" if is_self else "assistant",
"content": f"{username}: {message['body']}",
}
)
if notice:
messages.append({"role": "assistant", "content": notice})
return messages
def _exchange_status(room: dict[str, Any] | None, auth: dict[str, Any] | None) -> str:
if not auth or not auth.get("authenticated"):
return "Exchange form locked until sign-in."
if not room:
return "Join a speed-date post first."
if len(room["users"]) < 2:
return "Exchange form opens after a two-person speed-date."
if not _room_exchange_open(room):
return "Exchange form opens when the 20-minute chat ends."
submitted = room["exchangeForms"]
if auth["user_id"] in submitted and len(submitted) < 2:
return "Your exchange form is saved. Waiting for the other user."
if len(submitted) == 2:
return "Both users submitted. Final exchange is visible below."
return "Exchange form open. Both users must submit before contact details are displayed."
def _final_exchange(room: dict[str, Any] | None, auth: dict[str, Any] | None) -> str:
if not room or not auth or auth.get("user_id") not in room.get("users", {}):
return ""
forms = room["exchangeForms"]
if len(forms) < 2:
return ""
lines = ["## Mutual Contact Exchange", "Both users consented. Details are shown to the two room participants."]
for user_id, form in forms.items():
username = room["users"][user_id]["username"]
lines.extend(
[
f"### @{username}",
f"Name: {form['displayName']}",
f"Contact: {form['contact']}",
f"Note: {form['note'] or 'No note.'}",
]
)
return "\n\n".join(lines)
def _render_room_outputs(auth: dict[str, Any], room: dict[str, Any] | None, notice: str | None = None):
return (
_room_status(room, auth, notice),
_speed_chat_messages(room, auth, notice),
_timer_text(room),
_exchange_status(room, auth),
_final_exchange(room, auth),
)
def join_speed_date(auth: dict[str, Any]):
auth = auth or new_auth_state()
if not auth.get("authenticated"):
return auth, *_render_room_outputs(auth, None, "Sign in with Hugging Face first.")
try:
with _ROOM_LOCK:
room = _join_or_create_room(auth)
auth = {**auth, "room_id": room["id"]}
except RuntimeError as error:
return auth, *_render_room_outputs(auth, _auth_room(auth), str(error))
return auth, *_render_room_outputs(auth, room)
def refresh_speed_date(auth: dict[str, Any]):
auth = auth or new_auth_state()
run_matchmaker_cycle()
with _ROOM_LOCK:
room = _auth_room(auth)
return _render_room_outputs(auth, room)
def send_speed_date_message(message: str, auth: dict[str, Any]):
auth = auth or new_auth_state()
clean = normalize_text(message or "")
with _ROOM_LOCK:
room = _auth_room(auth)
if not auth.get("authenticated"):
return *_render_room_outputs(auth, room, "Sign in with Hugging Face first."), gr.update(value="")
if not room or auth["user_id"] not in room["users"]:
return *_render_room_outputs(auth, room, "Create or join a speed-date post first."), gr.update(value="")
if len(room["users"]) < 2:
return *_render_room_outputs(auth, room, "Waiting for the second user before chat opens."), gr.update(value="")
if _room_remaining_seconds(room) <= 0:
return *_render_room_outputs(auth, room, "The speed-date chat has ended. Use the mutual exchange form."), gr.update(value="")
if not clean:
return *_render_room_outputs(auth, room), gr.update(value="")
if contains_private_contact(clean):
return *_render_room_outputs(auth, room, chat_contact_refusal()), gr.update(value="")
room["messages"].append(
{
"id": uuid.uuid4().hex,
"createdAt": now_iso(),
"userId": auth["user_id"],
"username": auth["username"],
"body": clean,
}
)
return *_render_room_outputs(auth, room), gr.update(value="")
def submit_exchange_form(display_name: str, contact: str, note: str, consent: bool, auth: dict[str, Any]):
auth = auth or new_auth_state()
with _ROOM_LOCK:
room = _auth_room(auth)
if not auth.get("authenticated"):
return _exchange_status(room, auth), _final_exchange(room, auth)
if not room or auth["user_id"] not in room["users"]:
return "Join a speed-date post before submitting an exchange form.", ""
if not _room_exchange_open(room):
return _exchange_status(room, auth), _final_exchange(room, auth)
if not consent:
return "Consent is required before contact details can be exchanged.", _final_exchange(room, auth)
clean_name = normalize_text(display_name or "")
clean_contact = normalize_text(contact or "")
clean_note = normalize_text(note or "")
if not clean_name or not clean_contact:
return "Name and contact are required for mutual exchange.", _final_exchange(room, auth)
room["exchangeForms"][auth["user_id"]] = {
"displayName": clean_name[:120],
"contact": clean_contact[:240],
"note": clean_note[:600],
"submittedAt": now_iso(),
}
return _exchange_status(room, auth), _final_exchange(room, auth)
APP_CSS = """
:root {
--lg-ink: #251316;
--lg-muted: #725e61;
--lg-panel: #fff8f4;
--lg-panel-2: #f8ede8;
--lg-rose: #b73552;
--lg-rose-deep: #7a2035;
--lg-amber: #c99435;
--lg-teal: #2c6758;
--lg-charcoal: #171314;
--lg-line: rgba(37, 19, 22, 0.14);
--lg-shadow: 0 18px 50px rgba(37, 19, 22, 0.16);
}
.gradio-container {
max-width: 1320px !important;
margin: 0 auto !important;
color: var(--lg-ink) !important;
background:
linear-gradient(180deg, rgba(255, 248, 244, 0.96), rgba(246, 236, 231, 0.98) 42%, rgba(238, 242, 237, 0.98)) !important;
}
.lg-shell {
padding: 18px !important;
}
.lg-hero {
min-height: 340px;
padding: clamp(22px, 4vw, 42px);
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 8px;
background:
linear-gradient(135deg, rgba(23, 19, 20, 0.96), rgba(44, 103, 88, 0.92) 54%, rgba(122, 32, 53, 0.88));
color: #fff8f4;
box-shadow: var(--lg-shadow);
}
.lg-kicker {
margin: 0 0 12px;
color: #f0c36b;
font-size: 12px;
font-weight: 800;
letter-spacing: 0;
text-transform: uppercase;
}
.lg-title {
margin: 0;
color: #fff8f4;
font-size: clamp(38px, 5vw, 60px);
line-height: 0.94;
letter-spacing: 0;
overflow-wrap: anywhere;
}
.lg-lede {
max-width: 620px;
margin: 18px 0 24px;
color: rgba(255, 248, 244, 0.84);
font-size: clamp(15px, 1.8vw, 18px);
line-height: 1.48;
}
.lg-stat-grid {
display: grid;
grid-template-columns: repeat(3, minmax(72px, 1fr));
gap: 10px;
max-width: 660px;
}
.lg-stat {
min-height: 76px;
padding: 12px;
border: 1px solid rgba(255, 248, 244, 0.24);
border-radius: 8px;
background: rgba(255, 248, 244, 0.08);
}
.lg-stat strong {
display: block;
color: #fff8f4;
font-size: 20px;
line-height: 1;
}
.lg-stat span {
display: block;
margin-top: 8px;
color: rgba(255, 248, 244, 0.74);
font-size: 12px;
line-height: 1.35;
}
.lg-runtime {
margin: 14px 0 8px !important;
color: #553c41 !important;
}
.lg-runtime p,
.lg-runtime code {
margin: 0 !important;
color: #553c41 !important;
}
#lg-lounge-image {
display: block;
min-height: 340px;
margin: 0;
background: var(--lg-charcoal);
border-radius: 8px !important;
overflow: hidden !important;
box-shadow: var(--lg-shadow);
}
#lg-lounge-image img {
display: block;
width: 100% !important;
height: 340px !important;
object-fit: cover !important;
border-radius: 8px !important;
}
.lg-panel {
padding: 16px !important;
border: 1px solid var(--lg-line) !important;
border-radius: 8px !important;
background: rgba(255, 248, 244, 0.86) !important;
box-shadow: 0 10px 28px rgba(37, 19, 22, 0.08) !important;
}
.lg-panel-quiet {
padding: 16px !important;
border: 1px solid rgba(44, 103, 88, 0.18) !important;
border-radius: 8px !important;
background: rgba(238, 242, 237, 0.9) !important;
}
.lg-panel,
.lg-panel-quiet,
.lg-panel .block,
.lg-panel-quiet .block,
.lg-panel .form,
.lg-panel-quiet .form {
color: var(--lg-ink) !important;
}
.lg-panel .block,
.lg-panel-quiet .block,
.lg-panel .form,
.lg-panel-quiet .form,
.lg-panel .wrap,
.lg-panel-quiet .wrap,
.lg-panel .styler,
.lg-panel-quiet .styler {
background: transparent !important;
}
.lg-panel h1,
.lg-panel h2,
.lg-panel h3,
.lg-panel p,
.lg-panel label,
.lg-panel span,
.lg-panel-quiet h1,
.lg-panel-quiet h2,
.lg-panel-quiet h3,
.lg-panel-quiet p,
.lg-panel-quiet label,
.lg-panel-quiet span {
color: var(--lg-ink) !important;
}
.lg-panel input,
.lg-panel textarea,
.lg-panel select,
.lg-panel-quiet input,
.lg-panel-quiet textarea,
.lg-panel-quiet select {
background: #fffdf9 !important;
border-color: rgba(37, 19, 22, 0.18) !important;
color: var(--lg-ink) !important;
}
.lg-panel code,
.lg-panel-quiet code,
.lg-runtime code {
background: rgba(255, 255, 255, 0.72) !important;
border: 1px solid rgba(37, 19, 22, 0.12) !important;
color: var(--lg-rose-deep) !important;
}
.lg-section-title h2,
.lg-section-title h3,
.lg-section-title p {
margin-top: 0 !important;
}
.lg-section-title h2 {
font-size: 28px !important;
letter-spacing: 0 !important;
}
.lg-section-title h3 {
font-size: 21px !important;
letter-spacing: 0 !important;
}
.lg-section-title p,
.lg-muted,
.lg-muted p,
.lg-status-block p,
.lg-status-block li {
color: var(--lg-muted) !important;
}
.lg-mini-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 10px;
}
.lg-mini {
padding: 12px;
border-radius: 8px;
background: #ffffff;
border: 1px solid var(--lg-line);
}
.lg-mini b {
display: block;
margin-bottom: 4px;
color: var(--lg-rose-deep);
}
.lg-mini span {
color: var(--lg-muted) !important;
font-size: 13px;
line-height: 1.35;
}
.lg-progress-wrap label,
.lg-progress-wrap .wrap {
color: var(--lg-muted) !important;
}
.lg-chat textarea,
.lg-textarea textarea,
textarea {
font-size: 15px !important;
line-height: 1.55 !important;
}
.lg-chat .message,
.lg-chat .bubble-wrap {
font-size: 15px !important;
}
.lg-chat .message,
.lg-chat .message *,
.lg-chat .bubble-wrap,
.lg-chat .bubble-wrap *,
.lg-chat [data-testid="bot"] *,
.lg-chat [data-testid="user"] * {
color: #fff8f4 !important;
}
.lg-primary button,
button.primary {
border: 0 !important;
background: linear-gradient(135deg, var(--lg-rose), var(--lg-rose-deep)) !important;
color: #fff8f4 !important;
}
.lg-primary button span,
button.primary span {
color: #fff8f4 !important;
}
.lg-secondary button {
border-color: rgba(44, 103, 88, 0.28) !important;
}
.lg-speed-status {
border-left: 4px solid var(--lg-teal) !important;
}
.lg-exchange-status {
border-left: 4px solid var(--lg-amber) !important;
}
.lg-footer {
margin-top: 18px !important;
font-size: 13px !important;
}
.tabs {
margin-top: 18px !important;
}
.lg-tabs [role="tab"] {
color: #67484e !important;
}
.lg-tabs [role="tab"][aria-selected="true"] {
color: var(--lg-rose) !important;
}
@media (max-width: 760px) {
.lg-shell {
padding: 10px !important;
}
.lg-stat-grid,
.lg-mini-grid {
grid-template-columns: 1fr;
}
#lg-lounge-image img {
height: 230px !important;
}
}
"""
def build_app() -> gr.Blocks:
with gr.Blocks(
title=BRAND_NAME,
theme=gr.themes.Soft(primary_hue="rose", secondary_hue="amber", neutral_hue="slate"),
css=APP_CSS,
) as demo:
state = gr.State(new_state())
auth_state = gr.State(new_auth_state())
hero_image_src = image_data_uri(HERO_IMAGE_PATH)
with gr.Column(elem_classes=["lg-shell"]):
with gr.Row():
with gr.Column(scale=7):
gr.HTML(
f"""
<section class="lg-hero">
<p class="lg-kicker">OpenClaw matchmaking lounge</p>
<h1 class="lg-title">{BRAND_NAME}</h1>
<p class="lg-lede">
A consent-first profile studio, encrypted matchmaker table, and timed speed-date room
for high-signal compatibility.
</p>
<div class="lg-stat-grid">
<div class="lg-stat"><strong>{MAX_QUESTIONS}</strong><span>profile signals</span></div>
<div class="lg-stat"><strong>20m</strong><span>speed-date room</span></div>
<div class="lg-stat"><strong>0</strong><span>contact leaks</span></div>
</div>
</section>
"""
)
with gr.Column(scale=4):
if hero_image_src:
gr.HTML(
f"""
<figure id="lg-lounge-image">
<img src="{hero_image_src}" alt="Anonymous speed-date lounge scene" />
</figure>
"""
)
else:
gr.HTML('<div id="lg-lounge-image" class="lg-panel"></div>')
gr.Markdown(openclaw_runtime_label(), elem_classes=["lg-runtime"])
with gr.Tabs(elem_classes=["lg-tabs"]):
with gr.Tab("Profile Studio"):
with gr.Row():
with gr.Column(scale=7):
with gr.Group(elem_classes=["lg-panel"]):
gr.Markdown(
"""
## OpenClaw interview
Answer naturally. OpenClaw captures compatibility signals one question at a time.
""",
elem_classes=["lg-section-title"],
)
with gr.Row():
progress_label = gr.Markdown(progress_text(new_state()), elem_classes=["lg-progress-wrap"])
progress_bar = gr.Slider(
label="Profile completion",
minimum=0,
maximum=1,
value=0,
step=0.01,
interactive=False,
)
current_prompt = gr.Textbox(
label="Current prompt",
value=question_card(QUESTIONS[0]),
lines=7,
interactive=False,
elem_classes=["lg-textarea"],
)
chatbot = messages_chatbot(
value=opener(),
height=520,
label="OpenClaw",
elem_classes=["lg-chat"],
)
message = gr.Textbox(
label="Your answer",
placeholder="Answer with real specifics. One paragraph is enough.",
lines=4,
elem_classes=["lg-textarea"],
)
with gr.Row():
submit = gr.Button("Send Answer", variant="primary", elem_classes=["lg-primary"])
sample_answer = gr.Button("Try Sample Answer", elem_classes=["lg-secondary"])
privacy_check = gr.Button("Test Privacy Guard", elem_classes=["lg-secondary"])
with gr.Column(scale=4):
with gr.Group(elem_classes=["lg-panel"]):
gr.Markdown("### Identity pass", elem_classes=["lg-section-title"])
display_name = gr.Textbox(label="Display name", placeholder="Alex")
age = gr.Textbox(label="Age", placeholder="31")
location = gr.Textbox(label="Location", placeholder="Denver, CO")
intent = gr.Dropdown(
label="Intent",
choices=[
"Long-term relationship",
"Life partner",
"Intentional dating",
"Still discerning",
],
value="Long-term relationship",
)
with gr.Row():
save_basics = gr.Button("Save Basics", variant="primary", elem_classes=["lg-primary"])
reset = gr.Button("Reset Session", elem_classes=["lg-secondary"])
with gr.Group(elem_classes=["lg-panel-quiet"]):
gr.Markdown("### Compatibility brief", elem_classes=["lg-section-title"])
summary = gr.Textbox(
label="Profile readout",
value=profile_summary(new_state()),
lines=18,
elem_classes=["lg-textarea"],
)
export_file = gr.File(label="Encrypted Dataclaw row")
with gr.Tab("Matchmaker"):
with gr.Row():
with gr.Column(scale=5):
with gr.Group(elem_classes=["lg-panel", "lg-speed-status"]):
gr.Markdown(
"""
## Matchmaker agent
Complete, HF-identified profiles are judged by the strict harness before any room opens.
""",
elem_classes=["lg-section-title"],
)
matchmaker_status = gr.Markdown(matchmaker_status_text(), elem_classes=["lg-status-block"])
scan_matchmaker = gr.Button("Run Matchmaker Scan", variant="primary", elem_classes=["lg-primary"])
with gr.Column(scale=4):
with gr.Group(elem_classes=["lg-panel-quiet"]):
gr.Markdown("### Demo scoring", elem_classes=["lg-section-title"])
match_preview = gr.JSON(label="Compatibility preview", value=[])
score_button = gr.Button("Preview Demo Matches", elem_classes=["lg-secondary"])
gr.HTML(
"""
<div class="lg-mini-grid">
<div class="lg-mini"><b>Encrypted</b><span>Rows stay ciphertext outside Dataclaw logic.</span></div>
<div class="lg-mini"><b>Strict</b><span>Disgust, attraction, repair, lifestyle, and body alignment are scored.</span></div>
<div class="lg-mini"><b>Consent</b><span>Rooms open only for authenticated profiles.</span></div>
</div>
"""
)
with gr.Tab("Speed Date"):
with gr.Row():
with gr.Column(scale=4):
with gr.Group(elem_classes=["lg-panel"]):
gr.Markdown(
"""
## Entry
Sign in, join the post, and keep the conversation inside compatibility.
""",
elem_classes=["lg-section-title"],
)
hf_token = gr.Textbox(
label="Hugging Face token",
type="password",
placeholder="hf_...",
)
hf_login = gr.Button("Sign In With Token", variant="primary", elem_classes=["lg-primary"])
auth_status = gr.Markdown("Not signed in.", elem_classes=["lg-status-block"])
with gr.Row():
join_room = gr.Button("Create / Join Post", variant="primary", elem_classes=["lg-primary"])
refresh_room = gr.Button("Refresh", elem_classes=["lg-secondary"])
with gr.Group(elem_classes=["lg-panel-quiet", "lg-speed-status"]):
room_status = gr.Markdown(
"Sign in with a Hugging Face token to create or join the speed-date post.",
elem_classes=["lg-status-block"],
)
room_timer = gr.Markdown("No active speed-date room.", elem_classes=["lg-status-block"])
with gr.Column(scale=7):
with gr.Group(elem_classes=["lg-panel"]):
gr.Markdown("### Speed-date chat", elem_classes=["lg-section-title"])
speed_chat = messages_chatbot(
value=[{"role": "assistant", "content": "Sign in, then create or join the speed-date post."}],
height=420,
label="Room conversation",
elem_classes=["lg-chat"],
)
speed_message = gr.Textbox(
label="Message",
placeholder="Ask about values, pace, conflict repair, attraction, and lifestyle. Do not share contact details.",
lines=3,
elem_classes=["lg-textarea"],
)
send_speed = gr.Button("Send Message", variant="primary", elem_classes=["lg-primary"])
with gr.Group(elem_classes=["lg-panel", "lg-exchange-status"]):
gr.Markdown(
"""
### Mutual exchange
Opens after the speed-date ends and reveals contact details only after both users submit.
""",
elem_classes=["lg-section-title"],
)
with gr.Row():
exchange_name = gr.Textbox(label="Name to share", placeholder="Your preferred name")
exchange_contact = gr.Textbox(
label="Contact after mutual consent",
placeholder="Email, phone, or handle",
)
exchange_note = gr.Textbox(label="Optional note", lines=3, elem_classes=["lg-textarea"])
exchange_consent = gr.Checkbox(
label="I consent to share this contact info with the other speed-date participant."
)
submit_exchange = gr.Button("Submit Mutual Exchange", variant="primary", elem_classes=["lg-primary"])
exchange_status = gr.Markdown(
"Exchange form locked until a two-person speed-date ends.",
elem_classes=["lg-status-block"],
)
final_exchange = gr.Markdown("", elem_classes=["lg-status-block"])
room_auto_refresh = gr.Timer(value=3, active=True) if hasattr(gr, "Timer") else None
with gr.Accordion("Security notes", open=False, elem_classes=["lg-footer"]):
gr.Markdown(
"""
- Exported profile rows are encrypted JSONL. Raw JSONL is not exposed through the user interface.
- Rows are decrypted only inside functions that need them, such as matching, then discarded.
- Set `DATACLAW_PROFILE_KEY` as a Hugging Face secret so Dataclaw can decrypt rows across restarts.
- The production app still uses Flutter, Node/TypeScript, Supabase, Stripe, and pgvector.
- Chat requests to find, identify, or expose a person's private details are refused.
""",
elem_classes=["lg-muted"],
)
save_basics.click(
save_profile_basics,
inputs=[display_name, age, location, intent, state],
outputs=[state, summary, export_file],
)
submit.click(
chat_step,
inputs=[message, chatbot, state, auth_state],
outputs=[chatbot, state, progress_label, progress_bar, current_prompt, summary, export_file],
).then(lambda: "", outputs=message)
message.submit(
chat_step,
inputs=[message, chatbot, state, auth_state],
outputs=[chatbot, state, progress_label, progress_bar, current_prompt, summary, export_file],
).then(lambda: "", outputs=message)
sample_answer.click(
sample_answer_step,
inputs=[chatbot, state, auth_state],
outputs=[chatbot, state, progress_label, progress_bar, current_prompt, summary, export_file],
)
privacy_check.click(
privacy_check_step,
inputs=[chatbot, state, auth_state],
outputs=[chatbot, state, progress_label, progress_bar, current_prompt, summary, export_file],
)
reset.click(
reset_session,
outputs=[chatbot, state, progress_label, progress_bar, current_prompt, summary, export_file],
)
score_button.click(demo_match_table, inputs=state, outputs=match_preview)
scan_matchmaker.click(
scan_matchmaker_for_ui,
inputs=auth_state,
outputs=[matchmaker_status, room_status, speed_chat, room_timer, exchange_status, final_exchange],
)
hf_login.click(
login_with_hf_token,
inputs=[hf_token, auth_state],
outputs=[auth_state, auth_status, hf_token],
)
join_room.click(
join_speed_date,
inputs=auth_state,
outputs=[auth_state, room_status, speed_chat, room_timer, exchange_status, final_exchange],
)
refresh_room.click(
refresh_speed_date,
inputs=auth_state,
outputs=[room_status, speed_chat, room_timer, exchange_status, final_exchange],
)
if room_auto_refresh is not None:
room_auto_refresh.tick(
refresh_speed_date,
inputs=auth_state,
outputs=[room_status, speed_chat, room_timer, exchange_status, final_exchange],
)
send_speed.click(
send_speed_date_message,
inputs=[speed_message, auth_state],
outputs=[room_status, speed_chat, room_timer, exchange_status, final_exchange, speed_message],
)
speed_message.submit(
send_speed_date_message,
inputs=[speed_message, auth_state],
outputs=[room_status, speed_chat, room_timer, exchange_status, final_exchange, speed_message],
)
submit_exchange.click(
submit_exchange_form,
inputs=[exchange_name, exchange_contact, exchange_note, exchange_consent, auth_state],
outputs=[exchange_status, final_exchange],
)
return demo
demo = build_app()
if __name__ == "__main__":
demo.queue(default_concurrency_limit=8).launch(
server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"),
server_port=int(os.getenv("GRADIO_SERVER_PORT", "7860")),
show_error=True,
)