Deploy LifeChoice Simulator from Codex-built repository
Browse files- app.py +65 -9
- docs/lifechoice-demo.mp4 +2 -2
- docs/screenshots/simulation.png +2 -2
- lifechoice_engine.py +131 -20
app.py
CHANGED
|
@@ -16,8 +16,10 @@ from lifechoice_engine import (
|
|
| 16 |
SimulationSession,
|
| 17 |
choose,
|
| 18 |
current_node,
|
|
|
|
| 19 |
environment_image,
|
| 20 |
environment_state,
|
|
|
|
| 21 |
split_dilemma,
|
| 22 |
start_session,
|
| 23 |
)
|
|
@@ -280,6 +282,29 @@ footer { display: none !important; }
|
|
| 280 |
transform: translate(-50%, -50%);
|
| 281 |
z-index: 4;
|
| 282 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
.cascade-banner b { display: block; font: 900 .7rem ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; }
|
| 284 |
.cascade-banner span { font-size: .76rem; }
|
| 285 |
.report-card {
|
|
@@ -336,6 +361,7 @@ footer { display: none !important; }
|
|
| 336 |
}
|
| 337 |
#reset-button:hover { border-color: rgba(255,200,87,.45) !important; color: var(--gold) !important; }
|
| 338 |
@keyframes slide-in { from { opacity: 0; transform: translate(-50%, -42%); } to { opacity: 1; transform: translate(-50%, -50%); } }
|
|
|
|
| 339 |
@media (max-width: 760px) {
|
| 340 |
.gradio-container { padding: 12px !important; }
|
| 341 |
.app-header { align-items: flex-start; flex-direction: column; }
|
|
@@ -373,9 +399,20 @@ def begin(dilemma: str, chosen_key: str, calibration: str, persona: str):
|
|
| 373 |
raise gr.Error("Enter a real dilemma first.")
|
| 374 |
if not calibration.strip():
|
| 375 |
raise gr.Error("Add one concrete pressure, constraint, or proof point.")
|
| 376 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
node = current_node(session)
|
| 378 |
-
|
| 379 |
session,
|
| 380 |
game_html(session, node),
|
| 381 |
gr.Radio(choices=_choice_options(node), value=None, label="Choose your move"),
|
|
@@ -383,8 +420,10 @@ def begin(dilemma: str, chosen_key: str, calibration: str, persona: str):
|
|
| 383 |
gr.Group(visible=False),
|
| 384 |
gr.Column(visible=True),
|
| 385 |
gr.Group(visible=False),
|
| 386 |
-
gr.
|
|
|
|
| 387 |
)
|
|
|
|
| 388 |
|
| 389 |
|
| 390 |
def make_choice(session: SimulationSession, choice_value: str | None, custom_choice: str):
|
|
@@ -437,10 +476,25 @@ def reset():
|
|
| 437 |
gr.Group(visible=False),
|
| 438 |
gr.Column(visible=False),
|
| 439 |
gr.Group(visible=True),
|
|
|
|
| 440 |
gr.Button(interactive=True),
|
| 441 |
)
|
| 442 |
|
| 443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
def game_html(
|
| 445 |
session: SimulationSession,
|
| 446 |
node: dict[str, Any] | None = None,
|
|
@@ -449,10 +503,11 @@ def game_html(
|
|
| 449 |
report: dict[str, Any] | None = None,
|
| 450 |
) -> str:
|
| 451 |
state = environment_state(session.world_state)
|
|
|
|
| 452 |
background = image_data_uri(environment_image(session))
|
| 453 |
sprite = image_data_uri(str(ROOT / "assets" / "characters" / "character.png"))
|
| 454 |
portrait = image_data_uri(str(PERSONA_ASSETS[session.persona]))
|
| 455 |
-
sprite_position = {"
|
| 456 |
node = node or {
|
| 457 |
"month_label": "Future report",
|
| 458 |
"node_theme": "reflection",
|
|
@@ -501,7 +556,7 @@ def game_html(
|
|
| 501 |
<div class="stage-bottom">
|
| 502 |
{persona_markup}
|
| 503 |
<div class="player-wrap">
|
| 504 |
-
<span class="state-pill">World: {state}</span>
|
| 505 |
<div class="player-sprite" style="background-image:url('{sprite}');background-position:{sprite_position} 0"></div>
|
| 506 |
</div>
|
| 507 |
</div>
|
|
@@ -654,8 +709,9 @@ with gr.Blocks(css=CSS, theme=theme, title="LifeChoice Simulator") as demo:
|
|
| 654 |
begin_btn.click(
|
| 655 |
begin,
|
| 656 |
[dilemma, path_choice, calibration, persona],
|
| 657 |
-
[session_state, game_board, choices, custom, report_group, play_column, setup_group, choose_btn],
|
| 658 |
-
|
|
|
|
| 659 |
choose_btn.click(
|
| 660 |
make_choice,
|
| 661 |
[session_state, choices, custom],
|
|
@@ -663,9 +719,9 @@ with gr.Blocks(css=CSS, theme=theme, title="LifeChoice Simulator") as demo:
|
|
| 663 |
)
|
| 664 |
reset_btn.click(
|
| 665 |
reset,
|
| 666 |
-
outputs=[session_state, game_board, choices, custom, report_group, play_column, setup_group, choose_btn],
|
| 667 |
show_progress="hidden",
|
| 668 |
-
)
|
| 669 |
|
| 670 |
if __name__ == "__main__":
|
| 671 |
demo.queue(default_concurrency_limit=8).launch()
|
|
|
|
| 16 |
SimulationSession,
|
| 17 |
choose,
|
| 18 |
current_node,
|
| 19 |
+
character_expression,
|
| 20 |
environment_image,
|
| 21 |
environment_state,
|
| 22 |
+
prefetch_node,
|
| 23 |
split_dilemma,
|
| 24 |
start_session,
|
| 25 |
)
|
|
|
|
| 282 |
transform: translate(-50%, -50%);
|
| 283 |
z-index: 4;
|
| 284 |
}
|
| 285 |
+
.loading-stage {
|
| 286 |
+
align-items: center;
|
| 287 |
+
background:
|
| 288 |
+
radial-gradient(circle at 50% 35%, rgba(92,225,230,.10), transparent 30%),
|
| 289 |
+
linear-gradient(145deg, #11182a, #070a12);
|
| 290 |
+
display: flex;
|
| 291 |
+
flex-direction: column;
|
| 292 |
+
gap: 14px;
|
| 293 |
+
justify-content: center;
|
| 294 |
+
min-height: 530px;
|
| 295 |
+
text-align: center;
|
| 296 |
+
}
|
| 297 |
+
.loading-orbit {
|
| 298 |
+
animation: orbit 1.05s linear infinite;
|
| 299 |
+
border: 3px solid rgba(255,255,255,.11);
|
| 300 |
+
border-radius: 50%;
|
| 301 |
+
border-top-color: var(--gold);
|
| 302 |
+
height: 52px;
|
| 303 |
+
width: 52px;
|
| 304 |
+
}
|
| 305 |
+
.loading-stage h2 { color: #fff !important; font: 1.7rem "Arial Black", Impact, sans-serif; margin: 0; text-transform: uppercase; }
|
| 306 |
+
.loading-stage p { color: var(--muted) !important; margin: 0; max-width: 460px; }
|
| 307 |
+
.loading-steps { color: var(--cyan); font: 700 .67rem ui-monospace, monospace; letter-spacing: .09em; text-transform: uppercase; }
|
| 308 |
.cascade-banner b { display: block; font: 900 .7rem ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; }
|
| 309 |
.cascade-banner span { font-size: .76rem; }
|
| 310 |
.report-card {
|
|
|
|
| 361 |
}
|
| 362 |
#reset-button:hover { border-color: rgba(255,200,87,.45) !important; color: var(--gold) !important; }
|
| 363 |
@keyframes slide-in { from { opacity: 0; transform: translate(-50%, -42%); } to { opacity: 1; transform: translate(-50%, -50%); } }
|
| 364 |
+
@keyframes orbit { to { transform: rotate(360deg); } }
|
| 365 |
@media (max-width: 760px) {
|
| 366 |
.gradio-container { padding: 12px !important; }
|
| 367 |
.app-header { align-items: flex-start; flex-direction: column; }
|
|
|
|
| 399 |
raise gr.Error("Enter a real dilemma first.")
|
| 400 |
if not calibration.strip():
|
| 401 |
raise gr.Error("Add one concrete pressure, constraint, or proof point.")
|
| 402 |
+
yield (
|
| 403 |
+
None,
|
| 404 |
+
loading_html(dilemma, calibration),
|
| 405 |
+
gr.Radio(choices=[], value=None),
|
| 406 |
+
"",
|
| 407 |
+
gr.Group(visible=False),
|
| 408 |
+
gr.Column(visible=False),
|
| 409 |
+
gr.Group(visible=False),
|
| 410 |
+
gr.Group(visible=True),
|
| 411 |
+
gr.Button(interactive=False, value="Building your world..."),
|
| 412 |
+
)
|
| 413 |
+
session = start_session(dilemma, chosen_key or "A", calibration, persona, prefetch=False)
|
| 414 |
node = current_node(session)
|
| 415 |
+
yield (
|
| 416 |
session,
|
| 417 |
game_html(session, node),
|
| 418 |
gr.Radio(choices=_choice_options(node), value=None, label="Choose your move"),
|
|
|
|
| 420 |
gr.Group(visible=False),
|
| 421 |
gr.Column(visible=True),
|
| 422 |
gr.Group(visible=False),
|
| 423 |
+
gr.Group(visible=True),
|
| 424 |
+
gr.Button(interactive=True, value="Commit choice"),
|
| 425 |
)
|
| 426 |
+
prefetch_node(session, 1)
|
| 427 |
|
| 428 |
|
| 429 |
def make_choice(session: SimulationSession, choice_value: str | None, custom_choice: str):
|
|
|
|
| 476 |
gr.Group(visible=False),
|
| 477 |
gr.Column(visible=False),
|
| 478 |
gr.Group(visible=True),
|
| 479 |
+
gr.Group(visible=False),
|
| 480 |
gr.Button(interactive=True),
|
| 481 |
)
|
| 482 |
|
| 483 |
|
| 484 |
+
def loading_html(dilemma: str, calibration: str) -> str:
|
| 485 |
+
path_a, path_b = split_dilemma(dilemma)
|
| 486 |
+
detail = " ".join(calibration.split())[:150]
|
| 487 |
+
return f"""
|
| 488 |
+
<div class="game-frame loading-stage">
|
| 489 |
+
<div class="loading-orbit"></div>
|
| 490 |
+
<div class="eyebrow">Building a personalized causal world</div>
|
| 491 |
+
<h2>{html.escape(path_a)} / {html.escape(path_b)}</h2>
|
| 492 |
+
<p>Connecting your real constraint to the opening scene: {html.escape(detail)}</p>
|
| 493 |
+
<div class="loading-steps">Calibrating state / placing consequences / preparing choices</div>
|
| 494 |
+
</div>
|
| 495 |
+
"""
|
| 496 |
+
|
| 497 |
+
|
| 498 |
def game_html(
|
| 499 |
session: SimulationSession,
|
| 500 |
node: dict[str, Any] | None = None,
|
|
|
|
| 503 |
report: dict[str, Any] | None = None,
|
| 504 |
) -> str:
|
| 505 |
state = environment_state(session.world_state)
|
| 506 |
+
expression = character_expression(session.world_state)
|
| 507 |
background = image_data_uri(environment_image(session))
|
| 508 |
sprite = image_data_uri(str(ROOT / "assets" / "characters" / "character.png"))
|
| 509 |
portrait = image_data_uri(str(PERSONA_ASSETS[session.persona]))
|
| 510 |
+
sprite_position = {"neutral": "0%", "stressed": "33.333%", "confident": "66.666%"}[expression]
|
| 511 |
node = node or {
|
| 512 |
"month_label": "Future report",
|
| 513 |
"node_theme": "reflection",
|
|
|
|
| 556 |
<div class="stage-bottom">
|
| 557 |
{persona_markup}
|
| 558 |
<div class="player-wrap">
|
| 559 |
+
<span class="state-pill">World: {state} / You: {expression}</span>
|
| 560 |
<div class="player-sprite" style="background-image:url('{sprite}');background-position:{sprite_position} 0"></div>
|
| 561 |
</div>
|
| 562 |
</div>
|
|
|
|
| 709 |
begin_btn.click(
|
| 710 |
begin,
|
| 711 |
[dilemma, path_choice, calibration, persona],
|
| 712 |
+
[session_state, game_board, choices, custom, report_group, play_column, setup_group, game_group, choose_btn],
|
| 713 |
+
show_progress="hidden",
|
| 714 |
+
)
|
| 715 |
choose_btn.click(
|
| 716 |
make_choice,
|
| 717 |
[session_state, choices, custom],
|
|
|
|
| 719 |
)
|
| 720 |
reset_btn.click(
|
| 721 |
reset,
|
| 722 |
+
outputs=[session_state, game_board, choices, custom, report_group, play_column, setup_group, game_group, choose_btn],
|
| 723 |
show_progress="hidden",
|
| 724 |
+
)
|
| 725 |
|
| 726 |
if __name__ == "__main__":
|
| 727 |
demo.queue(default_concurrency_limit=8).launch()
|
docs/lifechoice-demo.mp4
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9e42b89611a55ae62c52f809ffb498c3dd2b237f50540eef3a805bd4b7ea6b78
|
| 3 |
+
size 694479
|
docs/screenshots/simulation.png
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
lifechoice_engine.py
CHANGED
|
@@ -67,6 +67,7 @@ def start_session(
|
|
| 67 |
chosen_key: str,
|
| 68 |
calibration: str,
|
| 69 |
persona: str,
|
|
|
|
| 70 |
) -> SimulationSession:
|
| 71 |
path_a, path_b = split_dilemma(dilemma)
|
| 72 |
chosen, unchosen = (path_b, path_a) if chosen_key == "B" else (path_a, path_b)
|
|
@@ -81,7 +82,8 @@ def start_session(
|
|
| 81 |
characters=_characters(chosen, unchosen),
|
| 82 |
)
|
| 83 |
session.generated_nodes[0] = build_node(session, 0, use_llm=False)
|
| 84 |
-
|
|
|
|
| 85 |
return session
|
| 86 |
|
| 87 |
|
|
@@ -176,22 +178,23 @@ def deterministic_node(context: dict[str, Any], index: int) -> dict[str, Any]:
|
|
| 176 |
unchosen = context["unchosen_path"]
|
| 177 |
state = context["world_state"]
|
| 178 |
pressure = state_facts(state)
|
|
|
|
| 179 |
prior = context.get("recent_choices", [])
|
| 180 |
prior_text = prior[-1]["choice"] if prior else f"step into {chosen}"
|
| 181 |
facts = context.get("facts") or ["your first commitment"]
|
| 182 |
fact = facts[-1]
|
| 183 |
|
| 184 |
scenarios = (
|
| 185 |
-
f"
|
| 186 |
-
f"
|
| 187 |
-
f"
|
| 188 |
-
f"
|
| 189 |
-
f"
|
| 190 |
-
f"
|
| 191 |
-
f"
|
| 192 |
-
f"Ten months in,
|
| 193 |
)
|
| 194 |
-
choices = _choice_templates(chosen, unchosen, index)
|
| 195 |
return {
|
| 196 |
"month_label": MONTHS[index],
|
| 197 |
"node_theme": THEMES[index],
|
|
@@ -201,22 +204,27 @@ def deterministic_node(context: dict[str, Any], index: int) -> dict[str, Any]:
|
|
| 201 |
}
|
| 202 |
|
| 203 |
|
| 204 |
-
def _choice_templates(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
templates = (
|
| 206 |
(
|
| 207 |
-
(
|
| 208 |
-
(
|
| 209 |
-
(
|
| 210 |
),
|
| 211 |
(
|
| 212 |
-
("Cut optional spending and protect time for
|
| 213 |
-
("
|
| 214 |
-
(f"Pause {chosen} until
|
| 215 |
),
|
| 216 |
(
|
| 217 |
-
("Share unfinished
|
| 218 |
-
("
|
| 219 |
-
(f"Ask someone on {unchosen}
|
| 220 |
),
|
| 221 |
(
|
| 222 |
("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"]),
|
|
@@ -258,6 +266,90 @@ def _choice_templates(chosen: str, unchosen: str, index: int) -> list[dict[str,
|
|
| 258 |
return result
|
| 259 |
|
| 260 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
def validate_node(data: Any, fallback: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
| 262 |
if not isinstance(data, dict) or not isinstance(data.get("choices"), list) or len(data["choices"]) != 3:
|
| 263 |
return fallback
|
|
@@ -395,6 +487,14 @@ def environment_state(state: dict[str, int]) -> str:
|
|
| 395 |
return "stable"
|
| 396 |
|
| 397 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
def infer_environment(path: str) -> str:
|
| 399 |
text = path.lower()
|
| 400 |
mapping = (
|
|
@@ -450,6 +550,8 @@ def _consume_or_build(session: SimulationSession, index: int) -> dict[str, Any]:
|
|
| 450 |
|
| 451 |
def _generate_node_with_hf(context: dict[str, Any], index: int) -> Any:
|
| 452 |
token = os.getenv("HF_TOKEN")
|
|
|
|
|
|
|
| 453 |
try:
|
| 454 |
from huggingface_hub import InferenceClient
|
| 455 |
|
|
@@ -460,6 +562,11 @@ def _generate_node_with_hf(context: dict[str, Any], index: int) -> Any:
|
|
| 460 |
"Exactly three materially different choices.",
|
| 461 |
"Each choice must create a fact and use all five integer delta keys from -15 to 15.",
|
| 462 |
"Respect metric constraints and causal facts. Never contradict closed options.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
"No advice, diagnosis, certainty, or prediction.",
|
| 464 |
],
|
| 465 |
"month": MONTHS[index],
|
|
@@ -555,5 +662,9 @@ def _label(value: str) -> str:
|
|
| 555 |
return " ".join(word if word.isupper() else word.capitalize() for word in cleaned.strip(" ?.,").split())
|
| 556 |
|
| 557 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
def _clamp(value: int) -> int:
|
| 559 |
return max(0, min(100, int(value)))
|
|
|
|
| 67 |
chosen_key: str,
|
| 68 |
calibration: str,
|
| 69 |
persona: str,
|
| 70 |
+
prefetch: bool = True,
|
| 71 |
) -> SimulationSession:
|
| 72 |
path_a, path_b = split_dilemma(dilemma)
|
| 73 |
chosen, unchosen = (path_b, path_a) if chosen_key == "B" else (path_a, path_b)
|
|
|
|
| 82 |
characters=_characters(chosen, unchosen),
|
| 83 |
)
|
| 84 |
session.generated_nodes[0] = build_node(session, 0, use_llm=False)
|
| 85 |
+
if prefetch:
|
| 86 |
+
prefetch_node(session, 1)
|
| 87 |
return session
|
| 88 |
|
| 89 |
|
|
|
|
| 178 |
unchosen = context["unchosen_path"]
|
| 179 |
state = context["world_state"]
|
| 180 |
pressure = state_facts(state)
|
| 181 |
+
profile = personalization_profile(context)
|
| 182 |
prior = context.get("recent_choices", [])
|
| 183 |
prior_text = prior[-1]["choice"] if prior else f"step into {chosen}"
|
| 184 |
facts = context.get("facts") or ["your first commitment"]
|
| 185 |
fact = facts[-1]
|
| 186 |
|
| 187 |
scenarios = (
|
| 188 |
+
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']}.",
|
| 189 |
+
f"{profile['money_event']} Your earlier decision to {prior_text.lower()} now affects both your calendar and your bank balance. {pressure[0]}",
|
| 190 |
+
f"{profile['comparison_event']} Your progress on {chosen} is real but less visible. {pressure[1]}",
|
| 191 |
+
f"{profile['identity_event']} The offer protects one part of your life while weakening the reason you chose {chosen}. {pressure[0]}",
|
| 192 |
+
f"{profile['family_event']} They want evidence such as {profile['milestone']}, not another promise. The conversation is also shaped by {fact}.",
|
| 193 |
+
f"{profile['return_event']} Accepting it now means honoring obligations created by your earlier choices. {pressure[0]}",
|
| 194 |
+
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]}",
|
| 195 |
+
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.",
|
| 196 |
)
|
| 197 |
+
choices = _choice_templates(chosen, unchosen, index, profile)
|
| 198 |
return {
|
| 199 |
"month_label": MONTHS[index],
|
| 200 |
"node_theme": THEMES[index],
|
|
|
|
| 204 |
}
|
| 205 |
|
| 206 |
|
| 207 |
+
def _choice_templates(
|
| 208 |
+
chosen: str,
|
| 209 |
+
unchosen: str,
|
| 210 |
+
index: int,
|
| 211 |
+
profile: dict[str, str],
|
| 212 |
+
) -> list[dict[str, Any]]:
|
| 213 |
templates = (
|
| 214 |
(
|
| 215 |
+
(profile["opening_bold"], _delta(-7, 12, 5, 9, -3), ["publicly_committed"], ["prove_progress"], []),
|
| 216 |
+
(profile["opening_balanced"], _delta(5, 5, 1, 2, 3), ["built_a_safety_floor"], ["maintain_two_tracks"], []),
|
| 217 |
+
(profile["opening_safe"], _delta(6, -7, -3, 7, 4), ["delayed_commitment"], ["make_a_deadline"], []),
|
| 218 |
),
|
| 219 |
(
|
| 220 |
+
(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"], []),
|
| 221 |
+
(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"], []),
|
| 222 |
+
(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"]),
|
| 223 |
),
|
| 224 |
(
|
| 225 |
+
(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"], []),
|
| 226 |
+
(f"Keep the {profile['work_sample']} private until it reaches the milestone you defined", _delta(1, 8, -5, 2, 0), ["chose_private_progress"], [], []),
|
| 227 |
+
(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"], []),
|
| 228 |
),
|
| 229 |
(
|
| 230 |
("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"]),
|
|
|
|
| 266 |
return result
|
| 267 |
|
| 268 |
|
| 269 |
+
def personalization_profile(context: dict[str, Any]) -> dict[str, str]:
|
| 270 |
+
chosen = context["chosen_path"]
|
| 271 |
+
unchosen = context["unchosen_path"]
|
| 272 |
+
calibration = " ".join(str(context.get("calibration", "")).split())
|
| 273 |
+
text = f"{chosen} {unchosen} {calibration}".lower()
|
| 274 |
+
|
| 275 |
+
if any(word in text for word in ("mtech", "masters", "research", "professor", "phd")):
|
| 276 |
+
professor = "the professor who showed interest" if "professor" in text else "a potential research mentor"
|
| 277 |
+
return {
|
| 278 |
+
"opening": f"{professor.capitalize()} asks whether you can commit to a defined project this semester.",
|
| 279 |
+
"proof": "a research direction and mentor support",
|
| 280 |
+
"alternative_pull": "an immediate salary and a clearer timeline",
|
| 281 |
+
"money_event": "Fees, living costs, and the delay before a dependable stipend become concrete.",
|
| 282 |
+
"comparison_event": f"A college peer shares a joining date and salary update from {unchosen}.",
|
| 283 |
+
"identity_event": "A placement opportunity arrives just as the research begins to feel like your own work.",
|
| 284 |
+
"family_event": "Your family asks how long they must wait before this path contributes income.",
|
| 285 |
+
"return_event": f"The professor offers a larger role, but it overlaps with interviews and income plans for {unchosen}.",
|
| 286 |
+
"milestone": "a funded project, publication milestone, or credible placement outcome",
|
| 287 |
+
"safety_floor": "a fees-and-living-cost plan with a firm review date",
|
| 288 |
+
"income_floor": "paid tutoring, assistantship work, or a limited freelance commitment",
|
| 289 |
+
"work_sample": "research proposal or early project result",
|
| 290 |
+
"opening_bold": "Accept the research project, but ask for scope, stipend timing, and a three-month milestone",
|
| 291 |
+
"opening_balanced": f"Commit to a smaller research milestone while continuing selected {unchosen} interviews",
|
| 292 |
+
"opening_safe": f"Take the {unchosen} route now and ask the professor to keep a limited research role open",
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
if any(word in text for word in ("artist", "design", "music", "writer", "creative", "film")):
|
| 296 |
+
return {
|
| 297 |
+
"opening": "A real client or showcase opportunity asks for a committed delivery date.",
|
| 298 |
+
"proof": "your portfolio and response from real people",
|
| 299 |
+
"alternative_pull": "a predictable salary and daily structure",
|
| 300 |
+
"money_event": "Rent, software costs, and an uneven client pipeline arrive in the same month.",
|
| 301 |
+
"comparison_event": f"A friend on {unchosen} posts a promotion while your strongest work is unfinished.",
|
| 302 |
+
"identity_event": "A commercial brief pays well but pushes your work away from the style you wanted to build.",
|
| 303 |
+
"family_event": "Your family asks whether the next client and payment are actually confirmed.",
|
| 304 |
+
"return_event": "A previous client returns with a larger project, a short deadline, and restrictive terms.",
|
| 305 |
+
"milestone": "three paying clients or a portfolio that consistently creates leads",
|
| 306 |
+
"safety_floor": "six months of runway and a monthly client target",
|
| 307 |
+
"income_floor": "a retainer client or part-time design contract",
|
| 308 |
+
"work_sample": "portfolio piece",
|
| 309 |
+
"opening_bold": "Accept the project, quote a real price, and publish the finished work as portfolio proof",
|
| 310 |
+
"opening_balanced": "Negotiate a smaller paid scope while keeping a limited stable-work schedule",
|
| 311 |
+
"opening_safe": f"Prioritize {unchosen} and reserve fixed weekly hours for one serious portfolio project",
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
if any(word in text for word in ("startup", "founder", "business", "revenue", "users")):
|
| 315 |
+
return {
|
| 316 |
+
"opening": "A potential customer asks for a feature and a delivery date before agreeing to pay.",
|
| 317 |
+
"proof": "customer use and repeatable revenue",
|
| 318 |
+
"alternative_pull": "salary, benefits, and predictable hours",
|
| 319 |
+
"money_event": "Runway shortens while customer requests expand beyond the original product.",
|
| 320 |
+
"comparison_event": f"A peer accepts a strong {unchosen} offer while your traction remains uneven.",
|
| 321 |
+
"identity_event": "An investor-friendly pivot could improve growth but weakens the problem you wanted to solve.",
|
| 322 |
+
"family_event": "Your family asks how much runway remains and what result would make you stop.",
|
| 323 |
+
"return_event": "An early customer returns with revenue, but demands priority and custom work.",
|
| 324 |
+
"milestone": "repeat customers and a monthly revenue target",
|
| 325 |
+
"safety_floor": "a runway threshold and a written stop-or-pivot date",
|
| 326 |
+
"income_floor": "consulting work capped at two days per week",
|
| 327 |
+
"work_sample": "customer pilot",
|
| 328 |
+
"opening_bold": "Commit to the pilot and define payment, scope, and a four-week success metric",
|
| 329 |
+
"opening_balanced": "Run the pilot while preserving two days of paid consulting each week",
|
| 330 |
+
"opening_safe": f"Take {unchosen} and test the product with customers on a fixed evening schedule",
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
concrete = calibration.rstrip(".") or f"you have a real reason to consider {chosen}"
|
| 334 |
+
return {
|
| 335 |
+
"opening": f"A concrete opportunity forces the decision forward: {concrete}.",
|
| 336 |
+
"proof": f"whether {chosen} works under your real constraint",
|
| 337 |
+
"alternative_pull": f"the protections you associate with {unchosen}",
|
| 338 |
+
"money_event": "The first deadline and financial consequence arrive together.",
|
| 339 |
+
"comparison_event": f"Someone close to you shows visible progress on {unchosen}.",
|
| 340 |
+
"identity_event": f"An easier option appears, but it changes what {chosen} would mean in daily life.",
|
| 341 |
+
"family_event": f"People affected by the decision ask for a concrete plan for {chosen}.",
|
| 342 |
+
"return_event": "The first opportunity returns with better upside and stricter conditions.",
|
| 343 |
+
"milestone": "a measurable result with a deadline",
|
| 344 |
+
"safety_floor": "a money, time, and health boundary",
|
| 345 |
+
"income_floor": "limited paid work",
|
| 346 |
+
"work_sample": "work-in-progress result",
|
| 347 |
+
"opening_bold": f"Accept the first {chosen} opportunity and define a measurable result before committing further",
|
| 348 |
+
"opening_balanced": f"Test {chosen} for three months while preserving a specific fallback on {unchosen}",
|
| 349 |
+
"opening_safe": f"Choose {unchosen} now and keep one bounded experiment on {chosen}",
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
|
| 353 |
def validate_node(data: Any, fallback: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
| 354 |
if not isinstance(data, dict) or not isinstance(data.get("choices"), list) or len(data["choices"]) != 3:
|
| 355 |
return fallback
|
|
|
|
| 487 |
return "stable"
|
| 488 |
|
| 489 |
|
| 490 |
+
def character_expression(state: dict[str, int]) -> str:
|
| 491 |
+
if state["stress"] > 70:
|
| 492 |
+
return "stressed"
|
| 493 |
+
if state["creative_fulfillment"] > 70 and state["stress"] < 40:
|
| 494 |
+
return "confident"
|
| 495 |
+
return "neutral"
|
| 496 |
+
|
| 497 |
+
|
| 498 |
def infer_environment(path: str) -> str:
|
| 499 |
text = path.lower()
|
| 500 |
mapping = (
|
|
|
|
| 550 |
|
| 551 |
def _generate_node_with_hf(context: dict[str, Any], index: int) -> Any:
|
| 552 |
token = os.getenv("HF_TOKEN")
|
| 553 |
+
if not token:
|
| 554 |
+
return None
|
| 555 |
try:
|
| 556 |
from huggingface_hub import InferenceClient
|
| 557 |
|
|
|
|
| 562 |
"Exactly three materially different choices.",
|
| 563 |
"Each choice must create a fact and use all five integer delta keys from -15 to 15.",
|
| 564 |
"Respect metric constraints and causal facts. Never contradict closed options.",
|
| 565 |
+
"Write in second person and keep the scenario under 320 characters.",
|
| 566 |
+
"Mention at least one concrete detail from calibration, current state, or recent history.",
|
| 567 |
+
"Choices must be specific actions with a concrete object, person, amount, deadline, or milestone.",
|
| 568 |
+
"Every choice must expose a real tradeoff with the unchosen path; no generic advice.",
|
| 569 |
+
"Ground relevant career dilemmas in realistic Indian student or young-professional details.",
|
| 570 |
"No advice, diagnosis, certainty, or prediction.",
|
| 571 |
],
|
| 572 |
"month": MONTHS[index],
|
|
|
|
| 662 |
return " ".join(word if word.isupper() else word.capitalize() for word in cleaned.strip(" ?.,").split())
|
| 663 |
|
| 664 |
|
| 665 |
+
def _lower_first(value: str) -> str:
|
| 666 |
+
return value[:1].lower() + value[1:] if value else value
|
| 667 |
+
|
| 668 |
+
|
| 669 |
def _clamp(value: int) -> int:
|
| 670 |
return max(0, min(100, int(value)))
|