| from __future__ import annotations |
|
|
| import copy |
| import json |
| import os |
| import re |
| import threading |
| from concurrent.futures import Future, ThreadPoolExecutor |
| from dataclasses import dataclass, field |
| from pathlib import Path |
| from typing import Any |
|
|
| MODEL_ID = "Qwen/Qwen2.5-7B-Instruct" |
| MODEL_PARAMETERS_B = 7 |
| METRICS = ( |
| "financial_security", |
| "creative_fulfillment", |
| "social_validation", |
| "stress", |
| "family_satisfaction", |
| ) |
| MONTHS = ("Month 1", "Month 2", "Month 3", "Month 4", "Month 5", "Month 6", "Month 8", "Month 10") |
| THEMES = ( |
| "first commitment", |
| "money pressure", |
| "social comparison", |
| "identity friction", |
| "family pressure", |
| "delayed consequence", |
| "survival versus meaning", |
| "final reckoning", |
| ) |
| EXECUTOR = ThreadPoolExecutor(max_workers=4, thread_name_prefix="lifechoice") |
|
|
|
|
| @dataclass |
| class SimulationSession: |
| dilemma: str |
| chosen_path: str |
| unchosen_path: str |
| calibration: str |
| persona: str |
| environment_category: str |
| world_state: dict[str, int] |
| characters: dict[str, dict[str, str]] |
| current_node: int = 0 |
| history: list[dict[str, Any]] = field(default_factory=list) |
| facts: list[str] = field(default_factory=list) |
| obligations: list[str] = field(default_factory=list) |
| closed_options: list[str] = field(default_factory=list) |
| generated_nodes: dict[int, dict[str, Any]] = field(default_factory=dict) |
| pending_nodes: dict[int, Future] = field(default_factory=dict, repr=False) |
| lock: threading.Lock = field(default_factory=threading.Lock, repr=False) |
|
|
|
|
| def split_dilemma(dilemma: str) -> tuple[str, str]: |
| text = " ".join(dilemma.strip().split()) |
| for pattern in (r"\s+vs\.?\s+", r"\s+versus\s+", r"\s*/\s*", r"\s+or\s+"): |
| parts = re.split(pattern, text, maxsplit=1, flags=re.IGNORECASE) |
| if len(parts) == 2 and all(parts): |
| return _label(parts[0]), _label(parts[1]) |
| return _label(text) or "Path A", "Safer Alternative" |
|
|
|
|
| def start_session( |
| dilemma: str, |
| chosen_key: str, |
| calibration: str, |
| persona: str, |
| prefetch: bool = True, |
| ) -> SimulationSession: |
| path_a, path_b = split_dilemma(dilemma) |
| chosen, unchosen = (path_b, path_a) if chosen_key == "B" else (path_a, path_b) |
| session = SimulationSession( |
| dilemma=dilemma.strip(), |
| chosen_path=chosen, |
| unchosen_path=unchosen, |
| calibration=calibration.strip(), |
| persona=persona, |
| environment_category=infer_environment(chosen), |
| world_state=derive_initial_world_state(dilemma, calibration), |
| characters=_characters(chosen, unchosen), |
| ) |
| session.generated_nodes[0] = build_node(session, 0, use_llm=False) |
| if prefetch: |
| prefetch_node(session, 1) |
| return session |
|
|
|
|
| def choose(session: SimulationSession, choice_index: int, custom_text: str = "") -> dict[str, Any]: |
| node = current_node(session) |
| if custom_text.strip(): |
| choice = _custom_choice(node, custom_text.strip()) |
| choice_index = -1 |
| else: |
| choice_index = max(0, min(choice_index, len(node["choices"]) - 1)) |
| choice = node["choices"][choice_index] |
|
|
| for key in METRICS: |
| session.world_state[key] = _clamp(session.world_state[key] + int(choice["delta"].get(key, 0))) |
|
|
| record = { |
| "node_index": session.current_node, |
| "month_label": node["month_label"], |
| "theme": node["node_theme"], |
| "scenario": node["scenario"], |
| "choice_index": choice_index, |
| "choice_text": choice["text"], |
| "delta": copy.deepcopy(choice["delta"]), |
| "world_state_after": copy.deepcopy(session.world_state), |
| } |
| session.history.append(record) |
| _merge_unique(session.facts, choice.get("facts_created", []), limit=10) |
| _merge_unique(session.obligations, choice.get("obligations", []), limit=6) |
| _merge_unique(session.closed_options, choice.get("closed_options", []), limit=6) |
| session.current_node += 1 |
|
|
| reaction = persona_reaction(session, record) |
| cascade = cascade_for(session, record) |
| if session.current_node >= len(MONTHS): |
| return {"complete": True, "record": record, "reaction": reaction, "cascade": cascade, "report": build_report(session)} |
|
|
| next_node = _consume_or_build(session, session.current_node) |
| prefetch_node(session, session.current_node + 1) |
| return { |
| "complete": False, |
| "record": record, |
| "reaction": reaction, |
| "cascade": cascade, |
| "node": next_node, |
| } |
|
|
|
|
| def current_node(session: SimulationSession) -> dict[str, Any]: |
| return session.generated_nodes[session.current_node] |
|
|
|
|
| def prefetch_node(session: SimulationSession, index: int) -> None: |
| if index >= len(MONTHS) or index in session.generated_nodes or index in session.pending_nodes: |
| return |
| snapshot = compact_context(session) |
| session.pending_nodes[index] = EXECUTOR.submit(build_node_from_context, snapshot, index, True) |
|
|
|
|
| def compact_context(session: SimulationSession) -> dict[str, Any]: |
| return { |
| "dilemma": session.dilemma, |
| "chosen_path": session.chosen_path, |
| "unchosen_path": session.unchosen_path, |
| "calibration": session.calibration[:360], |
| "persona": session.persona, |
| "world_state": copy.deepcopy(session.world_state), |
| "characters": copy.deepcopy(session.characters), |
| "facts": session.facts[-8:], |
| "obligations": session.obligations[-5:], |
| "closed_options": session.closed_options[-5:], |
| "recent_choices": [ |
| {"month": item["month_label"], "choice": item["choice_text"]} |
| for item in session.history[-3:] |
| ], |
| } |
|
|
|
|
| def build_node(session: SimulationSession, index: int, use_llm: bool = True) -> dict[str, Any]: |
| return build_node_from_context(compact_context(session), index, use_llm) |
|
|
|
|
| def build_node_from_context(context: dict[str, Any], index: int, use_llm: bool = True) -> dict[str, Any]: |
| fallback = deterministic_node(context, index) |
| if not use_llm: |
| return fallback |
| generated = _generate_node_with_hf(context, index) |
| return validate_node(generated, fallback, context) |
|
|
|
|
| def deterministic_node(context: dict[str, Any], index: int) -> dict[str, Any]: |
| chosen = context["chosen_path"] |
| unchosen = context["unchosen_path"] |
| state = context["world_state"] |
| pressure = state_facts(state) |
| profile = personalization_profile(context) |
| prior = context.get("recent_choices", []) |
| prior_text = prior[-1]["choice"] if prior else f"step into {chosen}" |
| facts = context.get("facts") or ["your first commitment"] |
| fact = facts[-1] |
|
|
| scenarios = ( |
| f"{profile['opening']} You said this is real because {_lower_first(context['calibration'].rstrip('.'))}. Choosing it makes {profile['proof']} visible, while {unchosen} still offers {profile['alternative_pull']}.", |
| f"{profile['money_event']} Your earlier decision to {prior_text.lower()} now affects both your calendar and your bank balance. {pressure[0]}", |
| f"{profile['comparison_event']} Your progress on {chosen} is real but less visible. {pressure[1]}", |
| f"{profile['identity_event']} The offer protects one part of your life while weakening the reason you chose {chosen}. {pressure[0]}", |
| f"{profile['family_event']} They want evidence such as {profile['milestone']}, not another promise. The conversation is also shaped by {fact}.", |
| f"{profile['return_event']} Accepting it now means honoring obligations created by your earlier choices. {pressure[0]}", |
| f"The question is no longer simply {chosen} versus {unchosen}. You need a structure built around {profile['safety_floor']} that keeps the chosen path livable. {pressure[1]}", |
| f"Ten months in, you have evidence from {profile['milestone']} and the costs recorded in your choices. Decide whether {chosen} can carry meaning, money, health, and relationships together.", |
| ) |
| choices = _choice_templates(chosen, unchosen, index, profile) |
| return { |
| "month_label": MONTHS[index], |
| "node_theme": THEMES[index], |
| "scenario": scenarios[index], |
| "choices": choices, |
| "generation_source": "deterministic", |
| } |
|
|
|
|
| def _choice_templates( |
| chosen: str, |
| unchosen: str, |
| index: int, |
| profile: dict[str, str], |
| ) -> list[dict[str, Any]]: |
| templates = ( |
| ( |
| (profile["opening_bold"], _delta(-7, 12, 5, 9, -3), ["publicly_committed"], ["prove_progress"], []), |
| (profile["opening_balanced"], _delta(5, 5, 1, 2, 3), ["built_a_safety_floor"], ["maintain_two_tracks"], []), |
| (profile["opening_safe"], _delta(6, -7, -3, 7, 4), ["delayed_commitment"], ["make_a_deadline"], []), |
| ), |
| ( |
| (f"Cut optional spending for three months and protect weekly time for {profile['milestone']}", _delta(-4, 8, -2, 7, -4), ["protected_the_bold_path"], ["tight_budget"], []), |
| (f"Use {profile['income_floor']} to support yourself, but reduce the pace of {chosen}", _delta(10, -3, 3, 5, 4), ["added_paid_work"], ["less_time_for_core_path"], []), |
| (f"Pause {chosen} until you have {profile['safety_floor']}, even if {profile['proof']} loses momentum", _delta(12, -11, 4, -5, 7), ["paused_for_money"], [], ["current_momentum"]), |
| ), |
| ( |
| (f"Share an unfinished {profile['work_sample']} and ask for feedback instead of hiding behind comparison", _delta(-1, 6, 11, 8, -1), ["became_visible"], ["respond_to_feedback"], []), |
| (f"Keep the {profile['work_sample']} private until it reaches the milestone you defined", _delta(1, 8, -5, 2, 0), ["chose_private_progress"], [], []), |
| (f"Ask someone already on {unchosen} to compare daily work, money, and growth honestly", _delta(3, 1, 5, 4, 3), ["sought_cross_path_advice"], ["face_comparison"], []), |
| ), |
| ( |
| ("Reject the easier offer because it changes what the work means to you", _delta(-10, 13, -2, 8, -3), ["protected_identity"], ["replace_lost_income"], ["easy_offer"]), |
| ("Accept the compromise and use the stability to recover", _delta(13, -7, 5, -6, 7), ["accepted_compromise"], [], []), |
| ("Negotiate narrower terms that preserve both income and ownership", _delta(6, 6, 3, 4, 2), ["negotiated_middle_path"], ["deliver_negotiated_terms"], []), |
| ), |
| ( |
| ("Show your family the real plan, including risks and deadlines", _delta(2, 2, 3, -3, 11), ["shared_full_plan"], ["report_progress"], []), |
| ("Hide the uncertainty until results become visible", _delta(0, 4, -2, 9, -9), ["hid_the_messy_parts"], ["maintain_the_story"], []), |
| ("Let family concern change the next move toward stability", _delta(8, -8, 5, -3, 12), ["family_changed_direction"], [], ["unrestricted_risk"]), |
| ), |
| ( |
| ("Accept the returning opportunity and absorb its delayed cost", _delta(-11, 13, 9, 12, -4), ["accepted_delayed_payoff"], ["pay_delayed_cost"], []), |
| ("Decline because the earlier choice made the cost unsustainable", _delta(8, -10, -2, -7, 6), ["declined_delayed_payoff"], [], ["returning_opportunity"]), |
| ("Redesign the opportunity around the obligations you already created", _delta(4, 7, 5, 3, 4), ["redesigned_consequence"], ["honor_existing_obligations"], []), |
| ), |
| ( |
| ("Build a financial and time floor around the meaningful work", _delta(12, 6, 2, -9, 5), ["built_sustainable_structure"], ["follow_weekly_limits"], []), |
| ("Push harder for visible progress and accept another intense season", _delta(-6, 10, 8, 14, -5), ["chose_intensity"], ["recover_after_push"], []), |
| (f"Return to {unchosen} as the main path and keep this work secondary", _delta(14, -12, 7, -5, 9), ["returned_to_alternative"], [], ["chosen_path_as_primary"]), |
| ), |
| ( |
| ("Commit to the chosen path with explicit money, health, and relationship boundaries", _delta(8, 11, 6, -10, 7), ["committed_with_boundaries"], [], []), |
| (f"Choose {unchosen} and preserve the meaningful parts as a serious side practice", _delta(14, -7, 8, -7, 10), ["chose_stability_with_meaning"], [], ["chosen_path_as_primary"]), |
| ("Run a measured three-month experiment before using a permanent label", _delta(5, 5, 1, -2, 2), ["chose_measured_experiment"], ["define_success_metrics"], []), |
| ), |
| ) |
| result = [] |
| for text, delta, facts, obligations, closed in templates[index]: |
| result.append( |
| { |
| "text": text, |
| "delta": delta, |
| "facts_created": facts, |
| "obligations": obligations, |
| "closed_options": closed, |
| } |
| ) |
| return result |
|
|
|
|
| def personalization_profile(context: dict[str, Any]) -> dict[str, str]: |
| chosen = context["chosen_path"] |
| unchosen = context["unchosen_path"] |
| calibration = " ".join(str(context.get("calibration", "")).split()) |
| text = f"{chosen} {unchosen} {calibration}".lower() |
|
|
| if any(word in text for word in ("mtech", "masters", "research", "professor", "phd")): |
| professor = "the professor who showed interest" if "professor" in text else "a potential research mentor" |
| return { |
| "opening": f"{professor.capitalize()} asks whether you can commit to a defined project this semester.", |
| "proof": "a research direction and mentor support", |
| "alternative_pull": "an immediate salary and a clearer timeline", |
| "money_event": "Fees, living costs, and the delay before a dependable stipend become concrete.", |
| "comparison_event": f"A college peer shares a joining date and salary update from {unchosen}.", |
| "identity_event": "A placement opportunity arrives just as the research begins to feel like your own work.", |
| "family_event": "Your family asks how long they must wait before this path contributes income.", |
| "return_event": f"The professor offers a larger role, but it overlaps with interviews and income plans for {unchosen}.", |
| "milestone": "a funded project, publication milestone, or credible placement outcome", |
| "safety_floor": "a fees-and-living-cost plan with a firm review date", |
| "income_floor": "paid tutoring, assistantship work, or a limited freelance commitment", |
| "work_sample": "research proposal or early project result", |
| "opening_bold": "Accept the research project, but ask for scope, stipend timing, and a three-month milestone", |
| "opening_balanced": f"Commit to a smaller research milestone while continuing selected {unchosen} interviews", |
| "opening_safe": f"Take the {unchosen} route now and ask the professor to keep a limited research role open", |
| } |
|
|
| if any(word in text for word in ("artist", "design", "music", "writer", "creative", "film")): |
| return { |
| "opening": "A real client or showcase opportunity asks for a committed delivery date.", |
| "proof": "your portfolio and response from real people", |
| "alternative_pull": "a predictable salary and daily structure", |
| "money_event": "Rent, software costs, and an uneven client pipeline arrive in the same month.", |
| "comparison_event": f"A friend on {unchosen} posts a promotion while your strongest work is unfinished.", |
| "identity_event": "A commercial brief pays well but pushes your work away from the style you wanted to build.", |
| "family_event": "Your family asks whether the next client and payment are actually confirmed.", |
| "return_event": "A previous client returns with a larger project, a short deadline, and restrictive terms.", |
| "milestone": "three paying clients or a portfolio that consistently creates leads", |
| "safety_floor": "six months of runway and a monthly client target", |
| "income_floor": "a retainer client or part-time design contract", |
| "work_sample": "portfolio piece", |
| "opening_bold": "Accept the project, quote a real price, and publish the finished work as portfolio proof", |
| "opening_balanced": "Negotiate a smaller paid scope while keeping a limited stable-work schedule", |
| "opening_safe": f"Prioritize {unchosen} and reserve fixed weekly hours for one serious portfolio project", |
| } |
|
|
| if any(word in text for word in ("startup", "founder", "business", "revenue", "users")): |
| return { |
| "opening": "A potential customer asks for a feature and a delivery date before agreeing to pay.", |
| "proof": "customer use and repeatable revenue", |
| "alternative_pull": "salary, benefits, and predictable hours", |
| "money_event": "Runway shortens while customer requests expand beyond the original product.", |
| "comparison_event": f"A peer accepts a strong {unchosen} offer while your traction remains uneven.", |
| "identity_event": "An investor-friendly pivot could improve growth but weakens the problem you wanted to solve.", |
| "family_event": "Your family asks how much runway remains and what result would make you stop.", |
| "return_event": "An early customer returns with revenue, but demands priority and custom work.", |
| "milestone": "repeat customers and a monthly revenue target", |
| "safety_floor": "a runway threshold and a written stop-or-pivot date", |
| "income_floor": "consulting work capped at two days per week", |
| "work_sample": "customer pilot", |
| "opening_bold": "Commit to the pilot and define payment, scope, and a four-week success metric", |
| "opening_balanced": "Run the pilot while preserving two days of paid consulting each week", |
| "opening_safe": f"Take {unchosen} and test the product with customers on a fixed evening schedule", |
| } |
|
|
| concrete = calibration.rstrip(".") or f"you have a real reason to consider {chosen}" |
| return { |
| "opening": f"A concrete opportunity forces the decision forward: {concrete}.", |
| "proof": f"whether {chosen} works under your real constraint", |
| "alternative_pull": f"the protections you associate with {unchosen}", |
| "money_event": "The first deadline and financial consequence arrive together.", |
| "comparison_event": f"Someone close to you shows visible progress on {unchosen}.", |
| "identity_event": f"An easier option appears, but it changes what {chosen} would mean in daily life.", |
| "family_event": f"People affected by the decision ask for a concrete plan for {chosen}.", |
| "return_event": "The first opportunity returns with better upside and stricter conditions.", |
| "milestone": "a measurable result with a deadline", |
| "safety_floor": "a money, time, and health boundary", |
| "income_floor": "limited paid work", |
| "work_sample": "work-in-progress result", |
| "opening_bold": f"Accept the first {chosen} opportunity and define a measurable result before committing further", |
| "opening_balanced": f"Test {chosen} for three months while preserving a specific fallback on {unchosen}", |
| "opening_safe": f"Choose {unchosen} now and keep one bounded experiment on {chosen}", |
| } |
|
|
|
|
| def validate_node(data: Any, fallback: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]: |
| if not isinstance(data, dict) or not isinstance(data.get("choices"), list) or len(data["choices"]) != 3: |
| return fallback |
| scenario = " ".join(str(data.get("scenario", "")).split())[:420] |
| if len(scenario) < 50 or not narrative_respects_state(scenario, context["world_state"]): |
| return fallback |
| choices = [] |
| for index, raw in enumerate(data["choices"]): |
| if not isinstance(raw, dict) or len(str(raw.get("text", ""))) < 12: |
| return fallback |
| base = fallback["choices"][index] |
| choices.append( |
| { |
| "text": " ".join(str(raw["text"]).split())[:150], |
| "delta": _normalize_delta(raw.get("delta")), |
| "facts_created": _clean_list(raw.get("facts_created")) or base["facts_created"], |
| "obligations": _clean_list(raw.get("obligations")), |
| "closed_options": _clean_list(raw.get("closed_options")), |
| } |
| ) |
| return {**fallback, "scenario": scenario, "choices": choices, "generation_source": "hf_llm"} |
|
|
|
|
| def narrative_respects_state(text: str, state: dict[str, int]) -> bool: |
| lower = text.lower() |
| if state["stress"] >= 80 and not any(word in lower for word in ("stress", "tired", "exhaust", "pressure", "overwhelm", "sleep")): |
| return False |
| if state["financial_security"] <= 25 and not any(word in lower for word in ("money", "rent", "bill", "saving", "income", "cost", "financial")): |
| return False |
| if state["family_satisfaction"] <= 25 and not any(word in lower for word in ("family", "home", "parent", "trust", "conversation")): |
| return False |
| return True |
|
|
|
|
| def state_facts(state: dict[str, int]) -> list[str]: |
| facts = [] |
| if state["financial_security"] <= 25: |
| facts.append("Money is now urgent enough to constrain the available choices.") |
| elif state["financial_security"] >= 75: |
| facts.append("Savings provide room to choose without immediate panic.") |
| if state["stress"] >= 80: |
| facts.append("Exhaustion is visible and cannot be treated as background noise.") |
| elif state["stress"] <= 30: |
| facts.append("You have enough emotional room to think beyond survival.") |
| if state["family_satisfaction"] <= 30: |
| facts.append("Trust at home is strained and affects the next decision.") |
| if state["creative_fulfillment"] >= 75: |
| facts.append("The work feels meaningful enough that walking away has a real emotional cost.") |
| if state["social_validation"] <= 30: |
| facts.append("Progress is difficult to explain publicly, increasing isolation.") |
| while len(facts) < 2: |
| facts.append("The trade-off remains real: every available move protects one value by spending another.") |
| return facts[:2] |
|
|
|
|
| def persona_reaction(session: SimulationSession, record: dict[str, Any]) -> str: |
| index = record["node_index"] |
| if index not in {0, 1, 3, 5, 7} and not _threshold_crossed(record): |
| return "" |
| choice = record["choice_text"] |
| state = session.world_state |
| if session.persona == "Inner Voice": |
| return f"I chose to {choice.lower()}, and I can feel which cost I am pretending not to notice." |
| if session.persona == "Friend": |
| return f"I understand why you chose to {choice.lower()}. Just do not confuse momentum with proof that the pressure is sustainable." |
| if session.persona == "Mentor": |
| return f"What does choosing to {choice.lower()} protect that the safer option cannot, and what obligation did it create?" |
| if session.persona == "Partner": |
| return f"I support the reason behind this choice, but stress is at {state['stress']}; our shared plan has to include that cost." |
| return f"I see why you chose to {choice.lower()}, but stability and family trust are part of the decision too, not enemies of it." |
|
|
|
|
| def cascade_for(session: SimulationSession, record: dict[str, Any]) -> dict[str, str] | None: |
| index = record["node_index"] |
| if index not in {2, 5, 7} or not session.history: |
| return None |
| source_index = 0 if index in {2, 5} else min(3, len(session.history) - 1) |
| source = session.history[source_index] |
| labels = {2: "A small echo", 5: "The delayed consequence", 7: "The final payoff"} |
| return { |
| "label": labels[index], |
| "memory": f"In {source['month_label']}, you chose: {source['choice_text']}", |
| "effect": "That decision changed the constraints and obligations shaping this moment.", |
| } |
|
|
|
|
| def build_report(session: SimulationSession) -> dict[str, Any]: |
| state = session.world_state |
| risk_count = sum(1 for item in session.history if item["delta"].get("stress", 0) >= 7) |
| structure_count = sum(1 for item in session.history if item["delta"].get("financial_security", 0) >= 5) |
| meaning_count = sum(1 for item in session.history if item["delta"].get("creative_fulfillment", 0) >= 5) |
| if meaning_count > structure_count: |
| archetype = "Meaning Builder" |
| elif structure_count > meaning_count: |
| archetype = "Practical Architect" |
| else: |
| archetype = "Measured Explorer" |
| return { |
| "archetype": archetype, |
| "summary": ( |
| f"You made {meaning_count} meaning-protecting moves, {structure_count} stabilizing moves, " |
| f"and {risk_count} high-pressure moves while testing {session.chosen_path}." |
| ), |
| "honest_mirror": ( |
| f"Your choices show that {session.chosen_path} is attractive only when it can become livable. " |
| f"You did not simply choose courage or safety; you repeatedly negotiated between identity, money, and relationships. " |
| f"The causal record matters: {', '.join(session.facts[-3:]) or 'you kept options open'}. " |
| f"Your next real-world experiment should test those obligations without treating this simulation as a prediction." |
| ), |
| "world_state": copy.deepcopy(state), |
| "facts": session.facts, |
| "obligations": session.obligations, |
| "model": MODEL_ID, |
| } |
|
|
|
|
| def environment_image(session: SimulationSession) -> str: |
| state = environment_state(session.world_state) |
| path = Path(__file__).parent / "assets" / "environments" |
| return str(path / f"{session.environment_category}_{state}.png") |
|
|
|
|
| def environment_state(state: dict[str, int]) -> str: |
| wellbeing = ( |
| state["financial_security"] |
| + state["creative_fulfillment"] |
| + state["social_validation"] |
| + state["family_satisfaction"] |
| + (100 - state["stress"]) |
| ) / 5 |
| if state["stress"] >= 82 or state["financial_security"] <= 18 or wellbeing < 38: |
| return "struggling" |
| if wellbeing >= 67 and state["stress"] <= 55: |
| return "thriving" |
| return "stable" |
|
|
|
|
| def character_expression(state: dict[str, int]) -> str: |
| if state["stress"] > 70: |
| return "stressed" |
| if state["creative_fulfillment"] > 70 and state["stress"] < 40: |
| return "confident" |
| return "neutral" |
|
|
|
|
| def infer_environment(path: str) -> str: |
| text = path.lower() |
| mapping = ( |
| (("artist", "art", "music", "design", "film", "creative", "writer"), "studio_creative"), |
| (("startup", "founder", "business"), "startup_chaotic"), |
| (("phd", "mtech", "college", "study", "masters", "research"), "campus_academic"), |
| (("doctor", "medical", "neet", "hospital"), "medical_clinical"), |
| (("corporate", "manager", "consulting", "mba"), "corporate"), |
| ) |
| for words, category in mapping: |
| if any(word in text for word in words): |
| return category |
| return "tech_office" |
|
|
|
|
| def derive_initial_world_state(dilemma: str, calibration: str) -> dict[str, int]: |
| state = { |
| "financial_security": 58, |
| "creative_fulfillment": 58, |
| "social_validation": 50, |
| "stress": 42, |
| "family_satisfaction": 50, |
| } |
| text = f"{dilemma} {calibration}".lower() |
| if any(word in text for word in ("rent", "loan", "salary", "money", "saving", "fees")): |
| state["financial_security"] -= 13 |
| state["stress"] += 9 |
| if any(word in text for word in ("family", "parent", "approval", "relatives")): |
| state["family_satisfaction"] -= 11 |
| state["stress"] += 6 |
| if any(word in text for word in ("passion", "meaning", "creative", "research", "freedom")): |
| state["creative_fulfillment"] += 10 |
| if any(word in text for word in ("offer", "admitted", "selected", "client", "revenue")): |
| state["social_validation"] += 8 |
| state["stress"] -= 3 |
| return {key: _clamp(value) for key, value in state.items()} |
|
|
|
|
| def _consume_or_build(session: SimulationSession, index: int) -> dict[str, Any]: |
| fallback = build_node(session, index, use_llm=False) |
| context = compact_context(session) |
| future = session.pending_nodes.pop(index, None) |
| if future and future.done(): |
| try: |
| node = validate_node(future.result(), fallback, context) |
| except Exception: |
| node = fallback |
| else: |
| node = fallback |
| session.generated_nodes[index] = node |
| return node |
|
|
|
|
| def _generate_node_with_hf(context: dict[str, Any], index: int) -> Any: |
| token = os.getenv("HF_TOKEN") |
| if not token: |
| return None |
| try: |
| from huggingface_hub import InferenceClient |
|
|
| client = InferenceClient(model=MODEL_ID, token=token, timeout=12) |
| prompt = { |
| "task": "Generate one LifeChoice Simulator decision node as strict JSON.", |
| "rules": [ |
| "Exactly three materially different choices.", |
| "Each choice must create a fact and use all five integer delta keys from -15 to 15.", |
| "Respect metric constraints and causal facts. Never contradict closed options.", |
| "Write in second person and keep the scenario under 320 characters.", |
| "Mention at least one concrete detail from calibration, current state, or recent history.", |
| "Choices must be specific actions with a concrete object, person, amount, deadline, or milestone.", |
| "Every choice must expose a real tradeoff with the unchosen path; no generic advice.", |
| "Ground relevant career dilemmas in realistic Indian student or young-professional details.", |
| "No advice, diagnosis, certainty, or prediction.", |
| ], |
| "month": MONTHS[index], |
| "theme": THEMES[index], |
| "context": context, |
| "schema": { |
| "scenario": "string", |
| "choices": [ |
| { |
| "text": "string", |
| "delta": {key: 0 for key in METRICS}, |
| "facts_created": ["string"], |
| "obligations": ["string"], |
| "closed_options": ["string"], |
| } |
| ], |
| }, |
| } |
| response = client.chat_completion( |
| messages=[{"role": "user", "content": json.dumps(prompt, ensure_ascii=True)}], |
| max_tokens=850, |
| temperature=0.55, |
| ) |
| text = response.choices[0].message.content |
| match = re.search(r"\{.*\}", text, flags=re.DOTALL) |
| return json.loads(match.group(0)) if match else None |
| except Exception: |
| return None |
|
|
|
|
| def _custom_choice(node: dict[str, Any], text: str) -> dict[str, Any]: |
| totals = {key: 0 for key in METRICS} |
| for choice in node["choices"]: |
| for key in METRICS: |
| totals[key] += int(choice["delta"].get(key, 0)) |
| return { |
| "text": text[:150], |
| "delta": {key: round(value / len(node["choices"])) for key, value in totals.items()}, |
| "facts_created": [f"custom_choice_{_slug(text)[:40]}"], |
| "obligations": ["review_custom_choice_consequences"], |
| "closed_options": [], |
| } |
|
|
|
|
| def _characters(chosen: str, unchosen: str) -> dict[str, dict[str, str]]: |
| return { |
| "family": {"name": "Family", "context": f"Wants {chosen} to have a credible safety floor."}, |
| "friend": {"name": "Friend", "context": f"Chose {unchosen} and provides a living comparison."}, |
| "mentor": {"name": "Mentor", "context": f"Five years ahead on {chosen}, including its difficult middle."}, |
| } |
|
|
|
|
| def _threshold_crossed(record: dict[str, Any]) -> bool: |
| state = record["world_state_after"] |
| return state["stress"] >= 80 or state["financial_security"] <= 20 or state["family_satisfaction"] <= 25 |
|
|
|
|
| def _delta(financial: int, creative: int, social: int, stress: int, family: int) -> dict[str, int]: |
| return dict( |
| financial_security=financial, |
| creative_fulfillment=creative, |
| social_validation=social, |
| stress=stress, |
| family_satisfaction=family, |
| ) |
|
|
|
|
| def _normalize_delta(value: Any) -> dict[str, int]: |
| if not isinstance(value, dict): |
| value = {} |
| return {key: max(-15, min(15, int(value.get(key, 0)))) for key in METRICS} |
|
|
|
|
| def _clean_list(value: Any) -> list[str]: |
| if not isinstance(value, list): |
| return [] |
| return [_slug(str(item))[:60] for item in value[:3] if str(item).strip()] |
|
|
|
|
| def _merge_unique(target: list[str], values: list[str], limit: int) -> None: |
| for value in values: |
| if value and value not in target: |
| target.append(value) |
| del target[:-limit] |
|
|
|
|
| def _slug(value: str) -> str: |
| return re.sub(r"[^a-z0-9]+", "_", value.lower()).strip("_") |
|
|
|
|
| def _label(value: str) -> str: |
| cleaned = re.sub(r"^(?:i am |i'm |should i |choose |between )", "", value.strip(), flags=re.IGNORECASE) |
| return " ".join(word if word.isupper() else word.capitalize() for word in cleaned.strip(" ?.,").split()) |
|
|
|
|
| def _lower_first(value: str) -> str: |
| return value[:1].lower() + value[1:] if value else value |
|
|
|
|
| def _clamp(value: int) -> int: |
| return max(0, min(100, int(value))) |
|
|