JasonDDuke's picture
Upload app.py with huggingface_hub
0842fa8 verified
Raw
History Blame Contribute Delete
73.6 kB
"""
Kronaxis Imprint Persona Explorer -- Gradio Space Application.
Browse 1,000 census-weighted synthetic personas (500 UK, 500 US) with up to
187 fields across 11 categories. Search by DYNAMICS-8 personality dimensions,
filter by demographics, run compatibility analysis, and explore the full
cognitive depth of each persona.
Dataset: kronaxis/imprint-personas-v2 (1,000 sample personas)
"""
from __future__ import annotations
import json
import os
import re
import time
from pathlib import Path
import gradio as gr
import numpy as np
from dynamics_rules import derive_attributes, default_income_for_band
from dynamics_inference import (
build_prompt,
call_inference,
build_reasoning_trace,
get_backend_status,
get_available_provider_label,
)
from dataset_index import (
load_personas,
build_index,
search_similar,
dynamics_to_vector,
persona_to_vector,
)
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
_SPACE_DIR = Path(__file__).parent
_DATA_DIR = Path(os.environ.get("DATA_DIR", _SPACE_DIR / "data"))
_REPO_DATA_DIR = _SPACE_DIR.parent / "data"
_RATE_LIMIT_PER_HOUR = 10
_MAX_STIMULUS_LENGTH = 500
# Kronaxis orange
_ACCENT_COLOUR = "#e8871e"
# ---------------------------------------------------------------------------
# Content filter
# ---------------------------------------------------------------------------
_BLOCKED_PATTERNS = [
r"\b(kill|murder|suicide|self[- ]?harm)\b",
r"\b(bomb|exploit|hack|attack)\b",
r"\b(child|minor|underage)\b.*\b(sex|porn|abuse)\b",
r"\b(terrorist|terrorism)\b",
]
_BLOCKED_RE = re.compile("|".join(_BLOCKED_PATTERNS), re.IGNORECASE)
def _is_blocked(text: str) -> bool:
return bool(_BLOCKED_RE.search(text))
# ---------------------------------------------------------------------------
# Load data at startup
# ---------------------------------------------------------------------------
_personas: list[dict] = []
_faiss_index = None
_persona_lookup: dict[str, dict] = {}
# Try local data dir, then repo data dir
for _dir in [_DATA_DIR, _REPO_DATA_DIR]:
_personas = load_personas(_dir)
if _personas:
break
if _personas:
_faiss_index, _ = build_index(_personas)
_persona_lookup = {p["persona_id"]: p for p in _personas}
# Extract filter options from loaded data
_ALL_REGIONS = sorted({p.get("identity", {}).get("region", "Unknown") for p in _personas})
_ALL_GENDERS = sorted({p.get("identity", {}).get("gender", "Unknown") for p in _personas})
_ALL_EDUCATION = sorted({p.get("identity", {}).get("education_level", "Unknown") for p in _personas})
_ALL_COUNTRIES = sorted({p.get("country", "Unknown") for p in _personas})
# ---------------------------------------------------------------------------
# Load validation results
# ---------------------------------------------------------------------------
_validation_results: list[dict] = []
for _vdir in [_DATA_DIR, _REPO_DATA_DIR, _SPACE_DIR / "data"]:
_vpath = _vdir / "validation_results.json"
if _vpath.exists():
with open(_vpath, "r", encoding="utf-8") as _vf:
_validation_results = json.load(_vf)
break
# ---------------------------------------------------------------------------
# Rate limiter
# ---------------------------------------------------------------------------
_request_log: dict[str, list[float]] = {}
def _rate_limited(session_id: str) -> bool:
now = time.time()
log = _request_log.get(session_id, [])
log = [t for t in log if now - t < 3600.0]
if len(log) >= _RATE_LIMIT_PER_HOUR:
_request_log[session_id] = log
return True
log.append(now)
_request_log[session_id] = log
return False
# ---------------------------------------------------------------------------
# DYNAMICS level labels
# ---------------------------------------------------------------------------
_DIM_NAMES = {
"D": "Discipline",
"Y": "Yielding",
"N": "Novelty",
"A": "Acuity",
"M": "Mercuriality",
"I": "Impulsivity",
"C": "Candour",
"S": "Sociability",
}
_DIM_ORDER = "DYNAMICS" # for iterating in canonical order
def _level_label(val: float) -> str:
if val >= 0.8:
return "Very High"
if val >= 0.6:
return "High"
if val >= 0.4:
return "Moderate"
if val >= 0.2:
return "Low"
return "Very Low"
# ---------------------------------------------------------------------------
# Persona formatting
# ---------------------------------------------------------------------------
def _persona_label(p: dict) -> str:
"""Short label for dropdown: KX-00001 -- Daniel Harris (62, Male, South East)."""
ident = p.get("identity", {})
name = f"{ident.get('first_name', '?')} {ident.get('surname', '?')}"
age = ident.get("age", "?")
gender = ident.get("gender", "?").title()
region = ident.get("region", "?")
return f"{p['persona_id']} -- {name} ({age}, {gender}, {region})"
def _fmt_identity(p: dict) -> str:
"""Format the identity section."""
i = p.get("identity", {})
country = p.get("country", "")
country_label = {"GB": "United Kingdom", "US": "United States"}.get(country, country)
lines = [
f"**Name:** {i.get('first_name', '?')} {i.get('surname', '')}",
f"**Age:** {i.get('age', '?')} | **Gender:** {i.get('gender', '?').title()} | "
f"**Ethnicity:** {i.get('ethnicity', '?')}",
f"**Location:** {i.get('town', '?')}, {i.get('region', '?')}"
+ (f" ({country_label})" if country_label else ""),
f"**Education:** {i.get('education_level', '?')} | "
f"**Occupation:** {i.get('occupation', '?')} ({i.get('occupation_sector', '?')})",
]
if i.get("household_composition"):
lines.append(f"**Household:** {i['household_composition']}")
if i.get("housing_type"):
lines.append(f"**Housing:** {i['housing_type']}")
if i.get("annual_income"):
lines.append(f"**Annual Income:** \u00a3{i['annual_income']:,}")
return "\n\n".join(lines)
def _fmt_dynamics(p: dict) -> str:
"""Format DYNAMICS-8 profile as a table."""
dyn = p.get("dynamics_8", {})
rows = ["| Dimension | Score | Level |", "|-----------|-------|-------|"]
for dim in "DYNAMI CS".replace(" ", ""):
val = dyn.get(dim, 0.5)
name = _DIM_NAMES.get(dim, dim)
rows.append(f"| **{dim}** {name} | {val:.2f} | {_level_label(val)} |")
summary = dyn.get("profile_summary", "")
if summary:
rows.append(f"\n**Profile Summary:** {summary}")
return "\n".join(rows)
def _fmt_financial(p: dict) -> str | None:
"""Format financial section."""
fin = p.get("financial")
if not fin:
return None
lines = []
if fin.get("annual_income"):
lines.append(f"**Annual Income:** \u00a3{fin['annual_income']:,}")
if fin.get("housing_status"):
lines.append(f"**Housing Status:** {fin['housing_status']}")
if fin.get("credit_score_band"):
lines.append(f"**Credit Score:** {fin['credit_score_band']}")
if fin.get("price_sensitivity") is not None:
lines.append(f"**Price Sensitivity:** {fin['price_sensitivity']:.2f}")
if fin.get("savings_behaviour"):
lines.append(f"**Savings:** {fin['savings_behaviour']}")
# Monthly spending
spending = fin.get("monthly_spending", {})
if spending:
lines.append("\n**Monthly Spending:**")
lines.append("| Category | Amount |")
lines.append("|----------|--------|")
total = 0
for cat, amt in spending.items():
if isinstance(amt, (int, float)):
lines.append(f"| {cat.replace('_', ' ').title()} | \u00a3{amt:,.0f} |")
total += amt
lines.append(f"| **Total** | **\u00a3{total:,.0f}** |")
# Brand preferences (top 5)
brands = fin.get("brand_preferences", [])
if brands:
lines.append("\n**Brand Preferences:**")
lines.append("| Brand | Category | Affinity |")
lines.append("|-------|----------|----------|")
for b in brands[:8]:
if isinstance(b, dict):
lines.append(
f"| {b.get('brand', '?')} | {b.get('category', '?')} | "
f"{b.get('affinity', 0):.1f} |"
)
# Major purchases
purchases = fin.get("major_purchases_planned", [])
if purchases:
lines.append("\n**Planned Major Purchases:**")
for purchase in purchases:
if isinstance(purchase, dict):
lines.append(f"- {purchase.get('item', purchase)} "
f"(\u00a3{purchase.get('estimated_cost', '?'):,})"
if isinstance(purchase.get('estimated_cost'), (int, float))
else f"- {purchase}")
else:
lines.append(f"- {purchase}")
return "\n".join(lines)
def _fmt_political(p: dict) -> str | None:
"""Format political section."""
pol = p.get("political")
if not pol:
return None
lines = []
if pol.get("party_affiliation"):
lines.append(f"**Party:** {pol['party_affiliation']}")
if pol.get("engagement_level"):
lines.append(f"**Engagement Level:** {pol['engagement_level']}/5")
issues = pol.get("key_issues", [])
if issues:
lines.append("\n**Key Issues:**")
for idx, issue in enumerate(issues, 1):
lines.append(f"{idx}. {issue}")
history = pol.get("voting_history", [])
if history:
lines.append("\n**Voting History:**")
lines.append("| Election | Year | Party | Reason |")
lines.append("|----------|------|-------|--------|")
for v in history:
if isinstance(v, dict):
reason = v.get("reason", "")
if len(reason) > 60:
reason = reason[:57] + "..."
lines.append(
f"| {v.get('election', '?')} | {v.get('year', '?')} | "
f"{v.get('party_voted', '?')} | {reason} |"
)
drift = pol.get("political_drift", [])
if drift:
lines.append("\n**Political Drift:**")
for d in drift:
if isinstance(d, dict):
lines.append(
f"- {d.get('year', '?')}: {d.get('from', '?')} -> "
f"{d.get('to', '?')} ({d.get('trigger_event', '')})"
)
return "\n".join(lines)
def _fmt_religious(p: dict) -> str | None:
"""Format religious/cultural section."""
rel = p.get("religious_cultural")
if not rel:
return None
lines = []
if rel.get("faith"):
lines.append(f"**Faith:** {rel['faith']}")
if rel.get("practice_level"):
lines.append(f"**Practice Level:** {rel['practice_level']}")
dietary = rel.get("dietary_requirements", [])
if dietary:
lines.append(f"**Dietary Requirements:** {', '.join(dietary)}")
holidays = rel.get("cultural_holidays", [])
if holidays:
lines.append(f"**Cultural Holidays:** {', '.join(holidays)}")
if rel.get("community_involvement"):
lines.append(f"**Community Involvement:** {rel['community_involvement']}")
return "\n".join(lines)
def _fmt_beliefs(p: dict) -> str | None:
"""Format beliefs section."""
beliefs = p.get("beliefs")
if not beliefs:
return None
lines = []
if beliefs.get("worldview_summary"):
lines.append(f"**Worldview:** {beliefs['worldview_summary']}")
positions = beliefs.get("ethical_positions", {})
if positions:
lines.append("\n**Ethical Positions:**")
lines.append("| Issue | Position |")
lines.append("|-------|----------|")
if isinstance(positions, dict):
for issue, val in positions.items():
label = f"{val:+.1f}" if isinstance(val, (int, float)) else str(val)
lines.append(f"| {issue} | {label} |")
elif isinstance(positions, list):
for pos in positions:
if isinstance(pos, dict):
topic = pos.get('topic', pos.get('issue', '?'))
position = pos.get('position', pos.get('score', '?'))
if isinstance(position, (int, float)):
position = f"{position:+.1f}"
lines.append(f"| {topic} | {position} |")
trust = beliefs.get("institutional_trust", {})
if trust:
lines.append("\n**Institutional Trust:**")
lines.append("| Institution | Trust |")
lines.append("|-------------|-------|")
if isinstance(trust, dict):
for inst, val in trust.items():
lines.append(f"| {inst} | {val:.2f} |" if isinstance(val, (int, float))
else f"| {inst} | {val} |")
elif isinstance(trust, list):
for t in trust:
if isinstance(t, dict):
lines.append(
f"| {t.get('institution', '?')} | "
f"{t.get('trust', t.get('score', '?'))} |"
)
return "\n".join(lines)
def _fmt_emotional(p: dict) -> str | None:
"""Format emotional state section."""
emo = p.get("emotional_state")
if not emo:
return None
lines = []
if emo.get("baseline_mood"):
lines.append(f"**Baseline Mood:** {emo['baseline_mood']}")
volatility = emo.get("emotional_volatility") or emo.get("volatility")
if volatility:
lines.append(f"**Volatility:** {volatility}")
if emo.get("resilience_rating") is not None:
lines.append(f"**Resilience:** {emo['resilience_rating']:.2f}")
triggers = emo.get("stress_triggers", [])
if triggers:
lines.append("\n**Stress Triggers:**")
for t in triggers:
lines.append(f"- {t}")
coping = emo.get("coping_mechanisms", [])
if coping:
lines.append("\n**Coping Mechanisms:**")
for c in coping:
lines.append(f"- {c}")
return "\n".join(lines)
def _fmt_relationships(p: dict) -> str | None:
"""Format relationships section."""
rels = p.get("relationships")
if not rels:
return None
lines = [
"| Name | Type | Closeness | Frequency | Influence |",
"|------|------|-----------|-----------|-----------|",
]
for r in rels:
if isinstance(r, dict):
rtype = r.get('relationship_type', r.get('type', '?'))
freq = r.get('interaction_frequency', r.get('frequency', '?'))
closeness = r.get('closeness', '?')
if isinstance(closeness, (int, float)):
closeness = f"{closeness:.2f}"
influence = r.get('influence_direction', r.get('influence', '?'))
lines.append(
f"| {r.get('name', '?')} | {rtype} | "
f"{closeness} | {freq} | {influence} |"
)
# Show relationship notes for first few
notes = []
for r in rels[:3]:
if isinstance(r, dict) and r.get("notes"):
notes.append(f"- **{r.get('name', '?')}:** {r['notes']}")
if notes:
lines.append("\n**Notes:**")
lines.extend(notes)
return "\n".join(lines)
def _fmt_memory(p: dict) -> str | None:
"""Format memory section (episodic, semantic, procedural)."""
mem = p.get("memory")
if not mem:
return None
lines = []
# Episodic
episodic = mem.get("episodic", [])
if episodic:
lines.append(f"**Episodic Memory ({len(episodic)} events):**")
lines.append("| Date | Event | Significance | Valence |")
lines.append("|------|-------|--------------|---------|")
for e in episodic[:10]:
if isinstance(e, dict):
event_text = e.get("event", e.get("description", "?"))
if len(event_text) > 50:
event_text = event_text[:47] + "..."
lines.append(
f"| {e.get('date', '?')} | {event_text} | "
f"{e.get('significance', '?')} | {e.get('emotional_valence', '?')} |"
)
if len(episodic) > 10:
lines.append(f"*... and {len(episodic) - 10} more events*")
# Semantic
semantic = mem.get("semantic", [])
if semantic:
lines.append(f"\n**Semantic Knowledge ({len(semantic)} items):**")
for s in semantic[:8]:
if isinstance(s, dict):
topic = s.get("topic", s.get("domain", "?"))
belief = s.get("belief", s.get("knowledge", "?"))
conf = s.get("confidence", "?")
lines.append(f"- **{topic}** (confidence: {conf}): {belief}")
if len(semantic) > 8:
lines.append(f"*... and {len(semantic) - 8} more items*")
# Procedural
procedural = mem.get("procedural", [])
if procedural:
lines.append(f"\n**Procedural Habits ({len(procedural)}):**")
lines.append("| Trigger | Behaviour | Frequency |")
lines.append("|---------|-----------|-----------|")
for h in procedural[:8]:
if isinstance(h, dict):
behaviour = h.get("behaviour", h.get("action", "?"))
if len(behaviour) > 50:
behaviour = behaviour[:47] + "..."
lines.append(
f"| {h.get('trigger', '?')} | {behaviour} | "
f"{h.get('frequency', '?')} |"
)
if len(procedural) > 8:
lines.append(f"*... and {len(procedural) - 8} more habits*")
return "\n".join(lines)
def _fmt_questionnaire(p: dict) -> str | None:
"""Format ISSP questionnaire responses."""
quest = p.get("questionnaire")
if not quest:
return None
lines = []
responses = quest.get("responses", [])
if responses:
lines.append(f"**Survey Responses ({len(responses)} questions):**")
lines.append("| Topic | Question | Answer | Confidence |")
lines.append("|-------|----------|--------|------------|")
for r in responses:
if isinstance(r, dict):
q = r.get("question", "?")
if len(q) > 60:
q = q[:57] + "..."
lines.append(
f"| {r.get('topic', '?')} | {q} | "
f"{r.get('answer', '?')}/7 | {r.get('confidence', '?')} |"
)
drift = quest.get("drift_tracking", [])
if drift:
lines.append(f"\n**Opinion Drift ({len(drift)} shifts):**")
for d in drift:
if isinstance(d, dict):
q = d.get("question", "?")
if len(q) > 50:
q = q[:47] + "..."
lines.append(
f"- **{q}**: {d.get('original_answer', '?')} -> "
f"{d.get('current_answer', '?')} "
f"(magnitude: {d.get('drift_magnitude', '?')})"
)
if d.get("drift_reason"):
lines.append(f" *Reason: {d['drift_reason']}*")
return "\n".join(lines)
def _fmt_lifecycle(p: dict) -> str | None:
"""Format lifecycle section."""
lc = p.get("lifecycle")
if not lc:
return None
lines = []
if lc.get("life_stage"):
lines.append(f"**Life Stage:** {lc['life_stage']}")
transitions = lc.get("major_transitions", [])
if transitions:
lines.append("\n**Major Life Transitions:**")
for t in transitions:
if isinstance(t, dict):
lines.append(
f"- **{t.get('year', '?')}:** {t.get('event', '?')} "
f"-- *{t.get('impact', '')}*"
)
else:
lines.append(f"- {t}")
# Handle both formats: single aspirations dict or separate short/medium/long keys
aspirations = lc.get("aspirations", {})
asp_short = lc.get("aspirations_short") or (
aspirations.get("short_term") if isinstance(aspirations, dict) else None)
asp_medium = lc.get("aspirations_medium") or (
aspirations.get("medium_term") if isinstance(aspirations, dict) else None)
asp_long = lc.get("aspirations_long") or (
aspirations.get("long_term") if isinstance(aspirations, dict) else None)
if asp_short or asp_medium or asp_long:
lines.append("\n**Aspirations:**")
if asp_short:
lines.append(f"- **Short term:** {asp_short}")
if asp_medium:
lines.append(f"- **Medium term:** {asp_medium}")
if asp_long:
lines.append(f"- **Long term:** {asp_long}")
elif isinstance(aspirations, dict) and aspirations:
lines.append("\n**Aspirations:**")
for horizon, goal in aspirations.items():
lines.append(f"- **{horizon.replace('_', ' ').title()}:** {goal}")
elif isinstance(aspirations, list) and aspirations:
lines.append("\n**Aspirations:**")
for a in aspirations:
lines.append(f"- {a}")
regrets = lc.get("regrets", [])
if regrets:
lines.append("\n**Regrets:**")
for r in regrets:
lines.append(f"- {r}")
formative = lc.get("formative_experiences", [])
if formative:
lines.append("\n**Formative Experiences:**")
for f in formative:
if isinstance(f, dict):
lines.append(f"- {f.get('experience', f.get('description', f))}")
else:
lines.append(f"- {f}")
return "\n".join(lines)
def format_persona_detail(p: dict) -> str:
"""Format a complete persona as rich markdown."""
ident = p.get("identity", {})
name = f"{ident.get('first_name', '?')} {ident.get('surname', '')}"
system_age = p.get("system_age_days", 0)
sections = [
f"## {p.get('persona_id', '?')} -- {name}",
f"**System Age:** {system_age} days | **Dataset Version:** "
f"{p.get('dataset_version', '?')}",
"",
"---",
"",
"### Identity",
_fmt_identity(p),
"",
"---",
"",
"### DYNAMICS-8 Personality Profile",
_fmt_dynamics(p),
]
# Conditional sections based on system age
section_defs = [
("Financial Profile", _fmt_financial, 30),
("Political Views", _fmt_political, 30),
("Religion & Culture", _fmt_religious, 30),
("Beliefs & Values", _fmt_beliefs, 90),
("Emotional Profile", _fmt_emotional, 90),
("Relationships", _fmt_relationships, 90),
("Memory", _fmt_memory, 180),
("Life Story", _fmt_lifecycle, 180),
("ISSP Questionnaire", _fmt_questionnaire, 180),
]
for title, formatter, min_age in section_defs:
sections.append("")
sections.append("---")
sections.append("")
sections.append(f"### {title}")
content = formatter(p)
if content:
sections.append(content)
elif system_age < min_age:
sections.append(
f"*Not yet developed (requires system age >= {min_age} days, "
f"current: {system_age} days)*"
)
else:
sections.append("*Data not available*")
return "\n".join(sections)
def format_persona_card(p: dict) -> str:
"""Format a brief persona card for search results."""
ident = p.get("identity", {})
dyn = p.get("dynamics_8", {})
name = f"{ident.get('first_name', '?')} {ident.get('surname', '')}"
system_age = p.get("system_age_days", 0)
country = p.get("country", "")
# Count populated sections
section_count = sum(1 for s in [
"financial", "political", "religious_cultural", "beliefs",
"emotional_state", "relationships", "memory", "lifecycle", "questionnaire",
] if p.get(s))
# Top 2 DYNAMICS dimensions (furthest from 0.5)
deviations = sorted(
((dim, abs(dyn.get(dim, 0.5) - 0.5), dyn.get(dim, 0.5))
for dim in "DYNAMI CS".replace(" ", "")),
key=lambda x: x[1],
reverse=True,
)
top_traits = []
for dim, _, val in deviations[:2]:
level = "high" if val >= 0.5 else "low"
top_traits.append(f"{level} {_DIM_NAMES[dim]}")
country_tag = f" [{country}]" if country else ""
return (
f"**{p.get('persona_id', '?')} -- {name}**{country_tag}\n"
f"{ident.get('age', '?')} {ident.get('gender', '?').title()}, "
f"{ident.get('town', '?')}, {ident.get('region', '?')}\n"
f"{ident.get('occupation', '?')} | {section_count + 2}/11 sections | "
f"System age: {system_age}d\n"
f"Personality: {', '.join(top_traits)}"
)
# ---------------------------------------------------------------------------
# Tab 1: Persona Explorer -- filter and browse
# ---------------------------------------------------------------------------
def filter_persona_list(
country: str,
region: str,
gender: str,
education: str,
age_min: int,
age_max: int,
system_age_min: int,
system_age_max: int,
search_text: str,
) -> gr.Dropdown:
"""Filter personas and update the dropdown choices."""
filtered = _personas
if country and country != "All":
filtered = [p for p in filtered if p.get("country") == country]
if region and region != "All":
filtered = [p for p in filtered if p.get("identity", {}).get("region") == region]
if gender and gender != "All":
filtered = [p for p in filtered
if p.get("identity", {}).get("gender", "").lower() == gender.lower()]
if education and education != "All":
filtered = [p for p in filtered
if p.get("identity", {}).get("education_level") == education]
filtered = [p for p in filtered
if age_min <= p.get("identity", {}).get("age", 0) <= age_max]
filtered = [p for p in filtered
if system_age_min <= p.get("system_age_days", 0) <= system_age_max]
if search_text:
search_lower = search_text.lower()
filtered = [p for p in filtered
if search_lower in _persona_label(p).lower()
or search_lower in p.get("identity", {}).get("occupation", "").lower()
or search_lower in p.get("identity", {}).get("town", "").lower()]
choices = [_persona_label(p) for p in filtered]
return gr.Dropdown(choices=choices, value=choices[0] if choices else None)
def show_persona_detail(selection: str | None) -> str:
"""Display full persona detail when selected from dropdown."""
if not selection:
return "*Select a persona from the dropdown above.*"
pid = selection.split(" -- ")[0].strip()
persona = _persona_lookup.get(pid)
if not persona:
return f"*Persona {pid} not found.*"
return format_persona_detail(persona)
# ---------------------------------------------------------------------------
# Tab 2: DYNAMICS Similarity Search + quiz import
# ---------------------------------------------------------------------------
def _parse_quiz_scores(raw_input: str) -> dict[str, float] | None:
"""Parse DYNAMICS-8 quiz URL or score string.
Accepts:
Full URL: https://kronaxis.co.uk/results?s=D72Y31N45A60M27I54C44S60
Score only: D72Y31N45A60M27I54C44S60
Returns dict with D/Y/N/A/M/I/C/S keys mapped to 0.00-0.99, or None on failure.
"""
if not raw_input:
return None
text = raw_input.strip()
# Extract score parameter from URL if present
match = re.search(r'[?&]s=([A-Z0-9]+)', text)
if match:
text = match.group(1)
# Also try the raw string directly
pattern = re.match(
r'^D(\d{2})Y(\d{2})N(\d{2})A(\d{2})M(\d{2})I(\d{2})C(\d{2})S(\d{2})$',
text,
)
if not pattern:
return None
dims = "DYNAMICS"
scores = {}
for idx, dim in enumerate(dims):
raw_val = int(pattern.group(idx + 1))
scores[dim] = max(0.0, min(0.99, raw_val / 100.0))
return scores
def import_quiz_scores(quiz_input: str):
"""Parse quiz URL/string and return slider values."""
scores = _parse_quiz_scores(quiz_input)
if scores is None:
return (
gr.Slider(value=0.5), gr.Slider(value=0.5),
gr.Slider(value=0.5), gr.Slider(value=0.5),
gr.Slider(value=0.5), gr.Slider(value=0.5),
gr.Slider(value=0.5), gr.Slider(value=0.5),
"Could not parse quiz scores. Expected format: D72Y31N45A60M27I54C44S60",
)
return (
gr.Slider(value=scores["D"]), gr.Slider(value=scores["Y"]),
gr.Slider(value=scores["N"]), gr.Slider(value=scores["A"]),
gr.Slider(value=scores["M"]), gr.Slider(value=scores["I"]),
gr.Slider(value=scores["C"]), gr.Slider(value=scores["S"]),
f"Imported: D={scores['D']:.2f} Y={scores['Y']:.2f} N={scores['N']:.2f} "
f"A={scores['A']:.2f} M={scores['M']:.2f} I={scores['I']:.2f} "
f"C={scores['C']:.2f} S={scores['S']:.2f}",
)
def find_similar_personas(
d_val: float, y_val: float, n_val: float, a_val: float,
m_val: float, i_val: float, c_val: float, s_val: float,
) -> str:
"""Find the 5 most similar personas to the given DYNAMICS profile."""
if not _personas or _faiss_index is None:
return "*No personas loaded. Dataset not yet available.*"
dynamics = {"D": d_val, "Y": y_val, "N": n_val, "A": a_val,
"M": m_val, "I": i_val, "C": c_val, "S": s_val}
attrs = derive_attributes(dynamics)
query_vec = dynamics_to_vector(dynamics, income_band=attrs["income_band"])
results = search_similar(query_vec, _faiss_index, _personas, k=5)
if not results:
return "*No similar personas found.*"
parts = []
for item in results:
p = item["persona"]
dist = item["distance"]
rank = item["rank"]
parts.append(f"### #{rank} (distance: {dist:.3f})")
parts.append(format_persona_card(p))
parts.append("")
return "\n\n---\n\n".join(parts)
def load_similar_persona(
d_val: float, y_val: float, n_val: float, a_val: float,
m_val: float, i_val: float, c_val: float, s_val: float,
selection: str | None,
) -> str:
"""Load and display a selected similar persona."""
if not selection:
return "*Select a persona from the results above.*"
pid = selection.split(" -- ")[0].strip()
persona = _persona_lookup.get(pid)
if not persona:
return f"*Persona {pid} not found.*"
return format_persona_detail(persona)
# ---------------------------------------------------------------------------
# Tab 3: Stimulus Response with persona loading
# ---------------------------------------------------------------------------
def load_persona_into_demo(selection: str | None):
"""Load a dataset persona into the demo sliders and show a context summary."""
if not selection or selection == "Custom (manual sliders)":
return (0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
0.0, 0.5, "neutral",
"*Using custom DYNAMICS values. Adjust sliders manually.*")
pid = selection.split(" -- ")[0].strip()
persona = _persona_lookup.get(pid)
if not persona:
return (0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
0.0, 0.5, "neutral",
f"*Persona {pid} not found.*")
dyn = persona.get("dynamics_8", {})
ident = persona.get("identity", {})
emo = persona.get("emotional_state", {})
fin = persona.get("financial", {})
# Map resilience to valence (-1 to 1)
resilience = emo.get("resilience_rating", 0.5)
valence = (resilience * 2.0) - 1.0
# Derive dominant emotion from baseline mood
mood = (emo.get("baseline_mood") or "").lower()
if "anxious" in mood or "worry" in mood:
emotion = "anxious"
elif "content" in mood or "calm" in mood or "steady" in mood:
emotion = "content"
elif "excit" in mood or "energetic" in mood:
emotion = "excited"
elif "frustrat" in mood:
emotion = "frustrated"
elif "melanchol" in mood or "sad" in mood:
emotion = "melancholy"
else:
emotion = "neutral"
# Build context card
name = f"{ident.get('first_name', '?')} {ident.get('surname', '')}"
context_parts = [
f"**Loaded: {pid} -- {name}**",
f"{ident.get('age', '?')}, {ident.get('gender', '?').title()}, "
f"{ident.get('town', '?')}, {ident.get('region', '?')}",
f"{ident.get('occupation', '?')} ({ident.get('occupation_sector', '?')})",
]
if fin.get("annual_income"):
context_parts.append(f"Income: \u00a3{fin['annual_income']:,}/year")
if emo.get("baseline_mood"):
context_parts.append(f"Mood: {emo['baseline_mood'][:100]}")
# Top personality traits
deviations = sorted(
((dim, abs(dyn.get(dim, 0.5) - 0.5), dyn.get(dim, 0.5))
for dim in _DIM_NAMES),
key=lambda x: x[1],
reverse=True,
)
traits = []
for dim, _, val in deviations[:3]:
if dim in _DIM_NAMES:
traits.append(f"{'high' if val >= 0.5 else 'low'} {_DIM_NAMES[dim]} ({val:.2f})")
if traits:
context_parts.append(f"Key traits: {', '.join(traits)}")
context_md = "\n\n".join(context_parts)
return (
dyn.get("D", 0.5), dyn.get("Y", 0.5), dyn.get("N", 0.5), dyn.get("A", 0.5),
dyn.get("M", 0.5), dyn.get("I", 0.5), dyn.get("C", 0.5), dyn.get("S", 0.5),
valence, resilience, emotion,
context_md,
)
def run_stimulus_response(
d_val: float, y_val: float, n_val: float, a_val: float,
m_val: float, i_val: float, c_val: float, s_val: float,
stimulus: str,
category: str,
valence: float,
arousal: float,
dominant_emotion: str,
request: gr.Request | None = None,
):
"""Run the stimulus-response inference pipeline."""
session_id = "default"
if request is not None:
session_id = request.session_hash or request.client.host or "default"
if _rate_limited(session_id):
return "*Rate limit reached (10/hour). Try again later.*", "", "", ""
dynamics = {
"D": max(0.0, min(1.0, d_val)),
"Y": max(0.0, min(1.0, y_val)),
"N": max(0.0, min(1.0, n_val)),
"A": max(0.0, min(1.0, a_val)),
"M": max(0.0, min(1.0, m_val)),
"I": max(0.0, min(1.0, i_val)),
"C": max(0.0, min(1.0, c_val)),
"S": max(0.0, min(1.0, s_val)),
}
stimulus = (stimulus or "").strip()
if not stimulus:
return "*Please enter a stimulus.*", "", "", ""
if len(stimulus) > _MAX_STIMULUS_LENGTH:
stimulus = stimulus[:_MAX_STIMULUS_LENGTH]
if _is_blocked(stimulus):
return "*Blocked by content filter. Please rephrase.*", "", "", ""
attrs = derive_attributes(dynamics)
emotional_state = {
"valence": max(-1.0, min(1.0, valence)),
"arousal": max(0.0, min(1.0, arousal)),
"dominant_emotion": dominant_emotion,
}
prompt = build_prompt(
dynamics=dynamics,
income_band=attrs["income_band"],
balance=attrs["current_balance"],
emotional_state=emotional_state,
stimulus=stimulus,
monthly_income=attrs["monthly_income"],
financial_anxiety_label=attrs["financial_anxiety"],
)
result = call_inference(prompt, session_id=session_id)
trace = build_reasoning_trace(
dynamics=dynamics,
response=result,
stimulus=stimulus,
financial_anxiety_label=attrs["financial_anxiety"],
income_band=attrs["income_band"],
balance=attrs["current_balance"],
monthly_income=attrs["monthly_income"],
)
response_text = f"*{result['raw_text']}*" if result["raw_text"] else "*No response.*"
provider = result.get("provider", "unknown")
provider_label = f"Powered by: {provider}"
trace_display = (
f"**Narrative:** {trace.get('narrative', '')}\n\n"
f"**DYNAMICS drivers:** {', '.join(trace.get('dynamics_drivers', []))}\n\n"
f"**Economic driver:** {trace.get('economic_driver', '')}\n\n"
f"**Confidence:** {trace.get('confidence', 0):.0%}"
)
# Similar personas from dataset
similar_display = ""
if _faiss_index is not None:
query_vec = dynamics_to_vector(
dynamics, income_band=attrs["income_band"],
emotional_valence=emotional_state["valence"],
)
similar = search_similar(query_vec, _faiss_index, _personas, k=3)
if similar:
parts = []
for item in similar:
parts.append(f"**#{item['rank']}** (distance: {item['distance']:.3f})")
parts.append(format_persona_card(item["persona"]))
similar_display = "\n\n---\n\n".join(parts)
return response_text, trace_display, similar_display, provider_label
# ---------------------------------------------------------------------------
# Tab 4: Compatibility
# ---------------------------------------------------------------------------
# Dimension weights for compatibility scoring.
# Positive weight = similarity preferred, negative = complementarity preferred.
_COMPAT_WEIGHTS = {
"D": +0.8,
"Y": -0.3,
"N": +0.6,
"A": +0.5,
"M": -0.4,
"I": +0.2,
"C": +0.9,
"S": +0.4,
}
def _compute_compatibility(
profile_a: dict[str, float],
profile_b: dict[str, float],
) -> dict:
"""Compute compatibility between two DYNAMICS-8 profiles.
Returns a dict with overall score, per-dimension breakdown, strengths, and risks.
"""
dim_scores = {}
total_score = 0.0
total_weight = 0.0
for dim in "DYNAMICS":
a_val = profile_a.get(dim, 0.5)
b_val = profile_b.get(dim, 0.5)
w = _COMPAT_WEIGHTS[dim]
diff = abs(a_val - b_val)
if w >= 0:
dim_score = (1.0 - diff) * abs(w)
else:
dim_score = diff * abs(w)
dim_scores[dim] = {
"a": a_val,
"b": b_val,
"diff": diff,
"weight": w,
"score": dim_score,
"type": "similarity" if w >= 0 else "complementarity",
}
total_score += dim_score
total_weight += abs(w)
overall = total_score / total_weight if total_weight > 0 else 0.0
# Strengths and risks
strengths = []
risks = []
d_a, d_b = profile_a.get("D", 0.5), profile_b.get("D", 0.5)
c_a, c_b = profile_a.get("C", 0.5), profile_b.get("C", 0.5)
m_a, m_b = profile_a.get("M", 0.5), profile_b.get("M", 0.5)
s_a, s_b = profile_a.get("S", 0.5), profile_b.get("S", 0.5)
n_a, n_b = profile_a.get("N", 0.5), profile_b.get("N", 0.5)
i_a, i_b = profile_a.get("I", 0.5), profile_b.get("I", 0.5)
y_a, y_b = profile_a.get("Y", 0.5), profile_b.get("Y", 0.5)
# D: Discipline
if d_a > 0.6 and d_b > 0.6:
risks.append("Both score high on Discipline: risk of competing for control")
elif d_a < 0.4 and d_b < 0.4:
risks.append("Both score low on Discipline: risk of lack of structure")
elif (d_a > 0.6 and 0.3 <= d_b <= 0.7) or (d_b > 0.6 and 0.3 <= d_a <= 0.7):
strengths.append("Clear leader/follower dynamic from Discipline difference")
# C: Candour
if c_a > 0.6 and c_b > 0.6:
strengths.append("Mutual trust and transparency from shared high Candour")
if (c_a > 0.6 and c_b < 0.4) or (c_b > 0.6 and c_a < 0.4):
risks.append("Value misalignment on honesty (high/low Candour gap)")
# M: Mercuriality
if (m_a > 0.6 and m_b < 0.4) or (m_b > 0.6 and m_a < 0.4):
strengths.append("Emotional stabiliser dynamic from Mercuriality contrast")
if m_a > 0.6 and m_b > 0.6:
risks.append("Risk of mutual emotional escalation (both high Mercuriality)")
# S: Sociability
if s_a > 0.6 and s_b > 0.6:
strengths.append("Shared social energy from mutual high Sociability")
if (s_a > 0.6 and s_b < 0.4) or (s_b > 0.6 and s_a < 0.4):
risks.append("Social energy mismatch (high/low Sociability gap)")
# N: Novelty
if n_a > 0.6 and n_b > 0.6:
strengths.append("Shared intellectual curiosity from mutual high Novelty")
if (n_a > 0.6 and n_b < 0.4) or (n_b > 0.6 and n_a < 0.4):
risks.append("Boredom asymmetry (high/low Novelty gap)")
# I: Impulsivity
if (i_a > 0.6 and i_b < 0.4) or (i_b > 0.6 and i_a < 0.4):
risks.append("Pace mismatch in decisions (high/low Impulsivity gap)")
# Y: Yielding
if (y_a > 0.6 and y_b < 0.4) or (y_b > 0.6 and y_a < 0.4):
strengths.append("Complementary assertiveness from Yielding contrast")
# Interaction style recommendation
if overall >= 0.7:
style = "Natural alignment. Direct, open communication works well."
elif overall >= 0.5:
style = (
"Moderate compatibility. Structured check-ins help. "
"Address the identified risks through explicit ground rules."
)
elif overall >= 0.3:
style = (
"Significant friction likely. Define clear roles and boundaries. "
"Mediated discussions recommended for contentious topics."
)
else:
style = (
"Low natural compatibility. Requires substantial compromise from both parties. "
"External facilitation recommended. Focus on specific, bounded goals."
)
return {
"overall": round(overall, 3),
"dim_scores": dim_scores,
"strengths": strengths,
"risks": risks,
"style": style,
}
def _get_persona_dynamics(pid: str) -> dict[str, float] | None:
"""Extract DYNAMICS-8 profile from a persona ID."""
persona = _persona_lookup.get(pid)
if not persona:
return None
return persona.get("dynamics_8", {})
def run_compatibility(
persona_a_choice: str,
a_d: float, a_y: float, a_n: float, a_a: float,
a_m: float, a_i: float, a_c: float, a_s: float,
persona_b_choice: str,
b_d: float, b_y: float, b_n: float, b_a: float,
b_m: float, b_i: float, b_c: float, b_s: float,
) -> str:
"""Calculate and format compatibility results."""
# Resolve profile A
if persona_a_choice and persona_a_choice != "Custom (sliders)":
pid_a = persona_a_choice.split(" -- ")[0].strip()
dyn_a = _get_persona_dynamics(pid_a)
if dyn_a is None:
return f"*Persona A ({pid_a}) not found.*"
label_a = persona_a_choice.split(" -- ")[1].split(" (")[0] if " -- " in persona_a_choice else pid_a
else:
dyn_a = {"D": a_d, "Y": a_y, "N": a_n, "A": a_a,
"M": a_m, "I": a_i, "C": a_c, "S": a_s}
label_a = "Custom Profile A"
# Resolve profile B
if persona_b_choice and persona_b_choice != "Custom (sliders)":
pid_b = persona_b_choice.split(" -- ")[0].strip()
dyn_b = _get_persona_dynamics(pid_b)
if dyn_b is None:
return f"*Persona B ({pid_b}) not found.*"
label_b = persona_b_choice.split(" -- ")[1].split(" (")[0] if " -- " in persona_b_choice else pid_b
else:
dyn_b = {"D": b_d, "Y": b_y, "N": b_n, "A": b_a,
"M": b_m, "I": b_i, "C": b_c, "S": b_s}
label_b = "Custom Profile B"
result = _compute_compatibility(dyn_a, dyn_b)
# Format output
overall = result["overall"]
if overall >= 0.7:
rating = "High"
elif overall >= 0.5:
rating = "Moderate"
elif overall >= 0.3:
rating = "Low"
else:
rating = "Very Low"
lines = [
f"## Compatibility: {label_a} & {label_b}",
"",
f"### Overall Score: {overall:.1%} ({rating})",
"",
"---",
"",
"### Per-Dimension Breakdown",
"",
"| Dimension | Person A | Person B | Difference | Weighting | Contribution | Type |",
"|-----------|----------|----------|------------|-----------|--------------|------|",
]
for dim in "DYNAMICS":
ds = result["dim_scores"][dim]
name = _DIM_NAMES[dim]
w_sign = "+" if ds["weight"] >= 0 else ""
lines.append(
f"| **{dim}** {name} | {ds['a']:.2f} | {ds['b']:.2f} | "
f"{ds['diff']:.2f} | {w_sign}{ds['weight']:.1f} | "
f"{ds['score']:.3f} | {ds['type'].title()} |"
)
lines.append("")
lines.append("---")
lines.append("")
if result["strengths"]:
lines.append("### Strengths")
lines.append("")
for s in result["strengths"]:
lines.append(f"- {s}")
lines.append("")
if result["risks"]:
lines.append("### Risks")
lines.append("")
for r in result["risks"]:
lines.append(f"- {r}")
lines.append("")
lines.append("---")
lines.append("")
lines.append("### Recommended Interaction Style")
lines.append("")
lines.append(result["style"])
return "\n".join(lines)
def update_compat_sliders_a(persona_choice: str):
"""When a dataset persona is selected for A, update the sliders."""
if not persona_choice or persona_choice == "Custom (sliders)":
return [gr.Slider(interactive=True)] * 8
pid = persona_choice.split(" -- ")[0].strip()
dyn = _get_persona_dynamics(pid)
if dyn is None:
return [gr.Slider(interactive=True)] * 8
return [
gr.Slider(value=dyn.get("D", 0.5), interactive=False),
gr.Slider(value=dyn.get("Y", 0.5), interactive=False),
gr.Slider(value=dyn.get("N", 0.5), interactive=False),
gr.Slider(value=dyn.get("A", 0.5), interactive=False),
gr.Slider(value=dyn.get("M", 0.5), interactive=False),
gr.Slider(value=dyn.get("I", 0.5), interactive=False),
gr.Slider(value=dyn.get("C", 0.5), interactive=False),
gr.Slider(value=dyn.get("S", 0.5), interactive=False),
]
def update_compat_sliders_b(persona_choice: str):
"""When a dataset persona is selected for B, update the sliders."""
if not persona_choice or persona_choice == "Custom (sliders)":
return [gr.Slider(interactive=True)] * 8
pid = persona_choice.split(" -- ")[0].strip()
dyn = _get_persona_dynamics(pid)
if dyn is None:
return [gr.Slider(interactive=True)] * 8
return [
gr.Slider(value=dyn.get("D", 0.5), interactive=False),
gr.Slider(value=dyn.get("Y", 0.5), interactive=False),
gr.Slider(value=dyn.get("N", 0.5), interactive=False),
gr.Slider(value=dyn.get("A", 0.5), interactive=False),
gr.Slider(value=dyn.get("M", 0.5), interactive=False),
gr.Slider(value=dyn.get("I", 0.5), interactive=False),
gr.Slider(value=dyn.get("C", 0.5), interactive=False),
gr.Slider(value=dyn.get("S", 0.5), interactive=False),
]
# ---------------------------------------------------------------------------
# Tab 5: Validation Results
# ---------------------------------------------------------------------------
def _build_validation_summary_table() -> str:
"""Build a markdown table of all validation hypotheses."""
if not _validation_results:
return "*Validation data not available.*"
lines = [
"| # | Hypothesis | n (high) | n (low) | Mean (high) | Mean (low) | "
"Cohen's d | p (corrected) | Result |",
"|---|-----------|----------|---------|-------------|------------|"
"-----------|---------------|--------|",
]
for idx, h in enumerate(_validation_results, 1):
test_name = h.get("test", "")
n_high = h.get("n_high", 0)
n_low = h.get("n_low", 0)
mean_high = h.get("mean_high", 0.0)
mean_low = h.get("mean_low", 0.0)
d_val = h.get("cohens_d", 0.0)
p_raw = h.get("p_value", 1.0)
# Bonferroni correction (10 tests)
p_corrected = min(1.0, p_raw * 10)
status = h.get("status", "")
direction_ok = h.get("direction_correct", 0)
if status == "OK" and direction_ok and p_corrected < 0.05:
result = "Confirmed"
else:
result = "Not confirmed"
# Format p value
if p_corrected < 0.001:
p_str = f"{p_corrected:.2e}"
else:
p_str = f"{p_corrected:.4f}"
lines.append(
f"| H{idx} | {test_name} | {n_high} | {n_low} | "
f"{mean_high:.4f} | {mean_low:.4f} | {abs(d_val):.3f} | "
f"{p_str} | **{result}** |"
)
return "\n".join(lines)
def _build_validation_chart():
"""Build a matplotlib chart showing high vs low means for confirmed hypotheses."""
try:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
except ImportError:
return None
if not _validation_results:
return None
# Filter to confirmed hypotheses
confirmed = []
for idx, h in enumerate(_validation_results, 1):
p_corrected = min(1.0, h.get("p_value", 1.0) * 10)
if (h.get("status") == "OK"
and h.get("direction_correct")
and p_corrected < 0.05):
confirmed.append((idx, h))
if not confirmed:
return None
fig, axes = plt.subplots(
2, (len(confirmed) + 1) // 2,
figsize=(max(12, len(confirmed) * 2.5), 8),
)
if len(confirmed) == 1:
axes = np.array([axes]).flatten()
else:
axes = axes.flatten()
orange = _ACCENT_COLOUR
grey = "#555555"
for plot_idx, (h_idx, h) in enumerate(confirmed):
ax = axes[plot_idx]
test_name = h.get("test", "")
# Extract short label (after colon)
short = test_name.split(":")[-1].strip() if ":" in test_name else test_name
if len(short) > 25:
short = short[:22] + "..."
mean_high = h.get("mean_high", 0)
mean_low = h.get("mean_low", 0)
d_val = abs(h.get("cohens_d", 0))
bars = ax.bar(
["High", "Low"],
[mean_high, mean_low],
color=[orange, grey],
width=0.5,
)
ax.set_title(f"H{h_idx}: {short}", fontsize=9, fontweight="bold")
ax.set_ylabel("Mean", fontsize=8)
ax.tick_params(labelsize=8)
ax.text(
0.5, 0.95, f"d = {d_val:.2f}",
transform=ax.transAxes,
ha="center", va="top",
fontsize=8, fontstyle="italic",
color="#666666",
)
# Hide unused subplots
for extra_idx in range(len(confirmed), len(axes)):
axes[extra_idx].set_visible(False)
fig.suptitle(
"DYNAMICS-8 Validation: Mean Comparison (High vs Low Groups)",
fontsize=12, fontweight="bold", y=1.02,
)
plt.tight_layout()
return fig
def _validation_explanation() -> str:
"""Return explanatory text about the validation study."""
if not _validation_results:
return ""
confirmed_count = sum(
1 for h in _validation_results
if h.get("status") == "OK"
and h.get("direction_correct")
and min(1.0, h.get("p_value", 1.0) * 10) < 0.05
)
total = len(_validation_results)
lines = [
"### What This Demonstrates",
"",
f"The DYNAMICS-8 framework was validated against {total} pre-registered "
f"hypotheses linking personality dimensions to observable behavioural "
f"outcomes in the synthetic persona dataset. "
f"**{confirmed_count} of {total} hypotheses were confirmed** after "
f"Bonferroni correction (alpha = 0.005 per test).",
"",
"Each hypothesis tests whether personas scoring high (above median) on a "
"specific DYNAMICS dimension differ from those scoring low (below median) "
"on a measurable behavioural variable. Mann-Whitney U tests are used "
"throughout, with Cohen's d as the effect size measure.",
"",
"This internal consistency validation shows that the persona generation "
"pipeline produces psychologically coherent individuals: their personality "
"scores drive their financial, social, and political behaviours in "
"directions consistent with established research literature.",
"",
"Full methodology and literature grounding are described in the research paper.",
]
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Gradio UI
# ---------------------------------------------------------------------------
_CUSTOM_CSS = """
.gradio-container {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
.kx-header {
color: #e8871e;
}
.kx-footer {
border-top: 2px solid #e8871e;
padding-top: 16px;
margin-top: 24px;
}
.provider-label {
font-size: 0.85em;
opacity: 0.7;
}
"""
def create_app() -> gr.Blocks:
"""Build the Gradio application."""
persona_count = len(_personas)
data_status = (
f"**{persona_count} personas loaded** (500 UK, 500 US)."
if persona_count > 0
else "**Dataset not yet available.** Generation in progress."
)
# Build Kronaxis-branded theme
try:
from gradio.themes import Soft, Color
kx_theme = Soft(
primary_hue=Color(
c50="#fef6ee",
c100="#fdecd8",
c200="#f9d4ab",
c300="#f5b874",
c400="#f09a3d",
c500="#e8871e",
c600="#d07014",
c700="#ac5612",
c800="#8a4416",
c900="#713915",
c950="#3d1c08",
name="kronaxis_orange",
),
)
except (ImportError, AttributeError):
kx_theme = gr.themes.Soft() if hasattr(gr, "themes") else None
with gr.Blocks(
title="Kronaxis Imprint Persona Explorer",
theme=kx_theme,
css=_CUSTOM_CSS,
) as app:
gr.Markdown(
"# <span class='kx-header'>Kronaxis</span> Imprint Persona Explorer\n"
"*Browse 1,000 census-weighted synthetic personas (500 UK, 500 US) with "
"up to 187 fields across 11 cognitive simulation categories.*\n\n"
f"{data_status}\n\n"
"Full dataset: [kronaxis/imprint-personas-v2]"
"(https://huggingface.co/datasets/kronaxis/imprint-personas-v2).\n"
)
# ==================================================================
# Tab 1: Explore Personas
# ==================================================================
with gr.Tab("Explore Personas"):
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### Filters")
country_filter = gr.Dropdown(
choices=["All"] + _ALL_COUNTRIES,
value="All",
label="Country",
)
region_filter = gr.Dropdown(
choices=["All"] + _ALL_REGIONS,
value="All",
label="Region",
)
gender_filter = gr.Dropdown(
choices=["All"] + _ALL_GENDERS,
value="All",
label="Gender",
)
education_filter = gr.Dropdown(
choices=["All"] + _ALL_EDUCATION,
value="All",
label="Education Level",
)
age_min = gr.Slider(18, 85, value=18, step=1, label="Age (min)")
age_max = gr.Slider(18, 85, value=85, step=1, label="Age (max)")
system_age_min = gr.Slider(
7, 365, value=7, step=1, label="System Age (min days)")
system_age_max = gr.Slider(
7, 365, value=365, step=1, label="System Age (max days)")
search_box = gr.Textbox(
label="Search",
placeholder="Name, occupation, or town...",
)
filter_btn = gr.Button("Apply Filters", variant="primary")
with gr.Column(scale=3):
# Persona selector
initial_choices = [_persona_label(p) for p in _personas[:50]]
persona_dropdown = gr.Dropdown(
choices=initial_choices,
value=initial_choices[0] if initial_choices else None,
label="Select Persona",
filterable=True,
)
# Full detail display
detail_output = gr.Markdown(
value=(
format_persona_detail(_personas[0])
if _personas
else "*No personas available.*"
),
label="Persona Detail",
)
# Wire up filter -> dropdown
filter_inputs = [
country_filter, region_filter, gender_filter, education_filter,
age_min, age_max, system_age_min, system_age_max,
search_box,
]
filter_btn.click(
fn=filter_persona_list,
inputs=filter_inputs,
outputs=[persona_dropdown],
)
# Wire up dropdown -> detail
persona_dropdown.change(
fn=show_persona_detail,
inputs=[persona_dropdown],
outputs=[detail_output],
)
# ==================================================================
# Tab 2: DYNAMICS Similarity Search
# ==================================================================
with gr.Tab("Find Similar Personas"):
gr.Markdown(
"### DYNAMICS Similarity Search\n"
"Adjust the eight personality dimensions to find the most similar "
"personas in the dataset. Uses FAISS nearest-neighbour search across "
"a 10-dimensional vector (8 DYNAMICS + income band + emotional valence)."
)
# Quiz import
with gr.Accordion("Import from DYNAMICS-8 Quiz", open=False):
gr.Markdown(
"Paste your quiz result URL from "
"[kronaxis.co.uk/quiz](https://kronaxis.co.uk/quiz) or a raw "
"score string (e.g. `D72Y31N45A60M27I54C44S60`)."
)
with gr.Row():
quiz_input = gr.Textbox(
label="Quiz URL or Score String",
placeholder="https://kronaxis.co.uk/results?s=D72Y31N45A60M27I54C44S60",
scale=3,
)
import_btn = gr.Button("Import", variant="primary", scale=1)
import_status = gr.Textbox(
label="Import Status",
interactive=False,
value="",
)
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("**DYNAMICS Dimensions** (0.0 = very low, 1.0 = very high)")
sim_d = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="D: Discipline")
sim_y = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="Y: Yielding")
sim_n = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="N: Novelty")
sim_a = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="A: Acuity")
sim_m = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="M: Mercuriality")
sim_i = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="I: Impulsivity")
sim_c = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="C: Candour")
sim_s = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="S: Sociability")
search_btn = gr.Button("Find Similar", variant="primary")
with gr.Column(scale=2):
similarity_output = gr.Markdown(
value="*Adjust sliders and click 'Find Similar' to search.*",
label="Similar Personas",
)
sim_sliders = [sim_d, sim_y, sim_n, sim_a, sim_m, sim_i, sim_c, sim_s]
# Wire quiz import -> sliders + status
import_btn.click(
fn=import_quiz_scores,
inputs=[quiz_input],
outputs=sim_sliders + [import_status],
)
search_btn.click(
fn=find_similar_personas,
inputs=sim_sliders,
outputs=[similarity_output],
)
# ==================================================================
# Tab 3: Stimulus Response Demo
# ==================================================================
with gr.Tab("Live Stimulus Demo"):
_backend_status = get_backend_status()
_provider_desc = get_available_provider_label()
_backend_note = f"*Active backend: {_provider_desc}*"
if not _backend_status["local"] and not _backend_status["gemini"]:
_backend_note = (
"*Stimulus response requires a running inference backend. "
"Set GEMINI_API_KEY to enable cloud inference, or set "
"LOCAL_INFERENCE_URL for a local model.*"
)
gr.Markdown(
"### Stimulus Response Simulation\n"
"Load a persona from the dataset (or build one manually), present "
"a stimulus, and observe the simulated response with reasoning trace.\n\n"
+ _backend_note
)
# Persona loader
demo_persona_choices = (
["Custom (manual sliders)"]
+ [_persona_label(p) for p in _personas]
)
demo_persona_dropdown = gr.Dropdown(
choices=demo_persona_choices,
value="Custom (manual sliders)",
label="Load a Persona",
filterable=True,
)
persona_context = gr.Markdown(
value="*Select a persona above to auto-fill, or adjust sliders manually.*",
)
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("**Persona DYNAMICS**")
demo_d = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="D: Discipline")
demo_y = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="Y: Yielding")
demo_n = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="N: Novelty")
demo_a = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="A: Acuity")
demo_m = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="M: Mercuriality")
demo_i = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="I: Impulsivity")
demo_c = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="C: Candour")
demo_s = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="S: Sociability")
with gr.Accordion("Emotional State", open=False):
valence_slider = gr.Slider(
-1.0, 1.0, value=0.0, step=0.05,
label="Valence (-1 negative, +1 positive)",
)
arousal_slider = gr.Slider(
0.0, 1.0, value=0.5, step=0.05, label="Arousal",
)
emotion_dropdown = gr.Dropdown(
choices=["neutral", "anxious", "content", "excited",
"frustrated", "melancholy"],
value="neutral",
label="Dominant Emotion",
)
with gr.Column(scale=1):
gr.Markdown("**Stimulus**")
stimulus_box = gr.Textbox(
label="Stimulus",
placeholder="Describe a product, offer, situation, or question...",
lines=4,
)
category_dropdown = gr.Dropdown(
choices=[
"Purchase decision", "Subscription offer",
"Social interaction", "Financial stress",
"Brand preference", "Custom (free text)",
],
value="Custom (free text)",
label="Category",
)
submit_btn = gr.Button("Submit", variant="primary")
with gr.Column(scale=1):
gr.Markdown("**Response**")
response_output = gr.Markdown(
value="*Submit a stimulus to see the response.*",
)
provider_output = gr.Markdown(value="", elem_classes=["provider-label"])
gr.Markdown("**Reasoning Trace**")
trace_output = gr.Markdown(value="")
gr.Markdown("**Similar Personas in Dataset**")
similar_output = gr.Markdown(value="")
# Wire persona loader -> sliders + emotional state + context card
demo_sliders = [demo_d, demo_y, demo_n, demo_a,
demo_m, demo_i, demo_c, demo_s]
demo_persona_dropdown.change(
fn=load_persona_into_demo,
inputs=[demo_persona_dropdown],
outputs=demo_sliders + [
valence_slider, arousal_slider, emotion_dropdown,
persona_context,
],
)
# Wire submit -> inference
submit_btn.click(
fn=run_stimulus_response,
inputs=demo_sliders + [
stimulus_box, category_dropdown,
valence_slider, arousal_slider, emotion_dropdown,
],
outputs=[response_output, trace_output, similar_output, provider_output],
)
# ==================================================================
# Tab 4: Compatibility
# ==================================================================
with gr.Tab("Compatibility"):
gr.Markdown(
"### DYNAMICS-8 Compatibility Analysis\n"
"Compare two DYNAMICS-8 profiles to assess compatibility. Select "
"personas from the dataset or build custom profiles with sliders. "
"The algorithm uses weighted dimension scoring: positive weights "
"reward similarity, negative weights reward complementarity."
)
compat_persona_choices = (
["Custom (sliders)"]
+ [_persona_label(p) for p in _personas]
)
with gr.Row():
# Person A
with gr.Column(scale=1):
gr.Markdown("#### Person A")
compat_a_dropdown = gr.Dropdown(
choices=compat_persona_choices,
value="Custom (sliders)",
label="Select Persona A",
filterable=True,
)
ca_d = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="D: Discipline")
ca_y = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="Y: Yielding")
ca_n = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="N: Novelty")
ca_a = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="A: Acuity")
ca_m = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="M: Mercuriality")
ca_i = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="I: Impulsivity")
ca_c = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="C: Candour")
ca_s = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="S: Sociability")
# Person B
with gr.Column(scale=1):
gr.Markdown("#### Person B")
compat_b_dropdown = gr.Dropdown(
choices=compat_persona_choices,
value="Custom (sliders)",
label="Select Persona B",
filterable=True,
)
cb_d = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="D: Discipline")
cb_y = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="Y: Yielding")
cb_n = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="N: Novelty")
cb_a = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="A: Acuity")
cb_m = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="M: Mercuriality")
cb_i = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="I: Impulsivity")
cb_c = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="C: Candour")
cb_s = gr.Slider(0.0, 1.0, value=0.5, step=0.01, label="S: Sociability")
compat_btn = gr.Button("Calculate Compatibility", variant="primary")
compat_output = gr.Markdown(
value="*Select two profiles and click 'Calculate Compatibility'.*",
)
ca_sliders = [ca_d, ca_y, ca_n, ca_a, ca_m, ca_i, ca_c, ca_s]
cb_sliders = [cb_d, cb_y, cb_n, cb_a, cb_m, cb_i, cb_c, cb_s]
# Wire persona selectors to update sliders
compat_a_dropdown.change(
fn=update_compat_sliders_a,
inputs=[compat_a_dropdown],
outputs=ca_sliders,
)
compat_b_dropdown.change(
fn=update_compat_sliders_b,
inputs=[compat_b_dropdown],
outputs=cb_sliders,
)
# Wire calculate button
compat_btn.click(
fn=run_compatibility,
inputs=[compat_a_dropdown] + ca_sliders + [compat_b_dropdown] + cb_sliders,
outputs=[compat_output],
)
# ==================================================================
# Tab 5: Validation Results
# ==================================================================
with gr.Tab("Validation"):
gr.Markdown(
"### DYNAMICS-8 Validation Results\n"
"Pre-registered hypothesis tests demonstrating internal consistency "
"of the persona dataset. Each test compares personas scoring above "
"and below the median on a given dimension against a measurable "
"behavioural outcome."
)
gr.Markdown(_build_validation_summary_table())
gr.Markdown("")
# Chart
validation_chart = _build_validation_chart()
if validation_chart is not None:
gr.Plot(value=validation_chart, label="Validation Chart")
gr.Markdown(_validation_explanation())
# ==================================================================
# Footer
# ==================================================================
gr.Markdown(
"<div class='kx-footer'>\n\n"
"---\n"
"**Built by [Kronaxis](https://kronaxis.co.uk).** "
"UK AI company building cognitive simulation infrastructure.\n\n"
"This demo is a public demonstration of the cognitive depth behind "
"[Panel Studio](https://kronaxis.co.uk/panel-studio), "
"where these personas live simulated lives with personality-driven decisions, "
"three-tier memory, lifecycle events, and causal reasoning traces.\n\n"
"**1,000 synthetic personas** (500 UK, 500 US), census-weighted from "
"ONS 2021 and US Census data.\n\n"
"**Research paper:** DYNAMICS-8: An Eight-Dimension Personality Framework "
"for Computational Behavioural Simulation (arXiv, forthcoming)\n\n"
"**Take the DYNAMICS-8 Quiz:** "
"[kronaxis.co.uk/quiz](https://kronaxis.co.uk/quiz)\n\n"
"[Persona Dataset](https://huggingface.co/datasets/kronaxis/imprint-personas-v2) "
"| [Research](https://kronaxis.co.uk/research) "
"| [Licensing](https://kronaxis.co.uk/licensing)\n\n"
"**Licence:** Apache 2.0 (demo code). Dataset: CC BY-NC 4.0. "
"All personas are fully synthetic.\n\n"
"</div>"
)
return app
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
demo = create_app()
demo.queue(default_concurrency_limit=5)
demo.launch(server_name="0.0.0.0", server_port=7860)