Spaces:
Sleeping
Sleeping
| """ | |
| 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) | |