""" 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( "# Kronaxis 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( "
" ) 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)