#!/usr/bin/env python3 """Build the static, clickable GitHub Pages demo. Seeds a THROWAWAY database with the fake "Alex Demo" persona (a mid-level software developer, everything in USD), enriches it so every module has a coherent story — net worth growing for 30 months, the mortgage falling in step, a career analysis, a market barometer, fake performance-marketing reports, an advanced RSU deep-dive — snapshots every GET endpoint to a JSON file, and assembles `dist/`: the real frontend plus those snapshots. api.js serves GETs from the baked files when window.KIST_STATIC_DEMO is set, and writes become a friendly toast. The only real data: public market quotes (Yahoo, keyless) and the maintainer's public GitHub activity (both visible to anyone anyway). Yahoo/GitHub calls are best-effort — offline the demo simply has thinner charts. Usage: python3 demo/build_demo.py [--out dist] """ import json import os import random import re import shutil import subprocess import sys import tempfile import urllib.request from datetime import date, datetime, timedelta, timezone from pathlib import Path ROOT = Path(__file__).resolve().parent.parent OUT = Path(sys.argv[sys.argv.index("--out") + 1] if "--out" in sys.argv else ROOT / "dist") RNG = random.Random(42) # deterministic fake history TODAY = date.today() # --- throwaway data dir, seeded with the sample persona --- data_dir = Path(tempfile.mkdtemp(prefix="kist-demo-")) os.environ["FINANCE_PROJECT_DIR"] = str(data_dir) subprocess.run([sys.executable, str(ROOT / "seed.py")], check=True, env={**os.environ}, cwd=ROOT) sys.path.insert(0, str(ROOT / "server")) import config # noqa: E402 config.setup() import engine_bridge as eb # noqa: E402 import ai_learning # noqa: E402 import planner # noqa: E402 import market # noqa: E402 # watchlist + risk-radar components + FX pairs the currency view needs TICKERS = ["AAPL", "MSFT", "VWCE.DE", "EURPLN=X", "USDPLN=X", "EURUSD=X", "^VIX", "GC=F", "CL=F"] def best_effort(label, fn, *a, **kw): try: return fn(*a, **kw) except Exception as e: print(f" (skipped {label}: {str(e)[:80]})") return None def month_iso(months_ago): y, m = TODAY.year, TODAY.month - months_ago while m <= 0: y, m = y - 1, m + 12 return f"{y:04d}-{m:02d}" # ================= enrich: a coherent 30-month story, all in USD ============== print("Enriching demo data…") planner.save_app_config({"wizard_completed": True, "base_currency": "USD", "modules": {"debts": True, "taxes": True, "markets": True, "rsu": True, "business": True, "career": True, "property": True}}) for t in TICKERS: best_effort(f"yahoo {t}", market.fetch_yahoo_history, t, "2y") try: import risk_radar best_effort("risk radar", risk_radar.backfill, 32) except Exception: pass best_effort("forecast journal", market.backfill_forecasts, 10) # --- net worth: 30 months of growth (~388k → today's ~545k) --- nw = 388_000.0 for back in range(30, 0, -1): nw += RNG.uniform(3_000, 7_500) # savings + market drift if back in (24, 14): # two believable dips nw -= RNG.uniform(6_000, 11_000) eb._exec("insert into snapshots (date, type, data) values (?,?,?)", (month_iso(back) + "-01", "net_worth", json.dumps({"net_worth": round(nw, 2)}))) # --- mortgage history: 14 months, balance falling in step with the story --- debt = eb._rows("select id, balance from debts")[0] bal = 348_500.0 for back in range(14, 0, -1): principal = RNG.uniform(1_850, 2_150) bal -= principal eb._exec("insert into debt_values (id, debt_id, month, balance, principal_paid, " "interest_paid, note, created_at) values (?,?,?,?,?,?,?,?)", (f"demo-debt-{back}", debt["id"], month_iso(back), round(bal, 2), round(principal, 2), round(bal * 0.065 / 12, 2), "", datetime.now().isoformat())) eb._exec("update debts set balance = ? where id = ?", (round(bal, 2), debt["id"])) # --- per-item wealth history so item charts have depth too --- growth = {"ETF portfolio (broker)": (52_000, 85_000), "Company shares (RSU)": (28_000, 60_000), "Pension account": (34_000, 45_000), "Apartment — Sample City": (585_000, 620_000)} for it in eb._rows("select id, name from wealth_items"): if it["name"] not in growth: continue lo, hi = growth[it["name"]] for back in range(24, 0, -1): frac = (24 - back) / 24 v = lo + (hi - lo) * frac + RNG.uniform(-1500, 1500) eb._exec("insert into wealth_values (id, item_id, date, value, created_at) " "values (?,?,?,?,?)", (f"demo-{it['id']}-{back}", it["id"], month_iso(back) + "-01", round(v, 2), datetime.now().isoformat())) # --- career: PM persona — Senior PM offers, PM barometer, principal/HoP analysis --- eb._exec("delete from job_offers") # replace the seed's EM offers planner.add_offer({"company": "Acme Corp", "role": "Senior Product Manager", "total_monthly": 15500, "work_model": "hybrid", "status": "new", "received_at": month_iso(2) + "-10", "notes": "Sample offer — demo data.", "tier": 2}) planner.add_offer({"company": "Globex", "role": "Senior Product Manager (Platform)", "total_monthly": 17800, "work_model": "remote", "status": "interviewing", "received_at": month_iso(1) + "-18", "notes": "Sample offer — demo data.", "tier": 1}) planner.set_settings({"career_role_a": "Senior PM", "career_role_b": "Head of Product"}) for back in range(8, 0, -1): best_effort("barometer", planner.add_barometer_point, { "month": month_iso(back), "em_openings": 40 + (8 - back) * 3 + RNG.randint(-4, 4), "head_openings": 11 + (8 - back) + RNG.randint(-2, 2), "region": "US/EU (remote)", "note": "sample data" if back == 8 else ""}) planner.set_settings({"analysis_career": json.dumps({ "headline": "Senior PM with shipped outcomes — the fork ahead is Principal (IC craft " "at scale) vs Head of Product (org leverage); both pay, they compound " "differently.", "as_of": TODAY.isoformat(), "target_role": "Principal PM / Head of Product (2–3 yrs)", "comp_levels": [ {"role": "Product Manager", "comp": "$125–155k", "you": False}, {"role": "Senior PM", "comp": "$155–190k", "you": True}, {"role": "Principal PM", "comp": "$190–235k", "you": False}, {"role": "Head of Product", "comp": "$230–300k", "you": False}], "money_paths": [ {"tag": "A", "title": "Principal track where you are", "verdict": "solid", "text": "Own the hardest product area end-to-end, write the strategy memos " "leadership forwards; principal within ~2 years at +20–25% comp, " "no management overhead."}, {"tag": "B", "title": "Head of Product at a scale-up", "verdict": "highest EV", "text": "Series B–D companies pay a premium for a senior PM who has shipped " "0→1 and run discovery; expect +35–50% plus meaningful equity — and " "a genuinely different job (hiring, roadmap politics, board decks)."}, {"tag": "C", "title": "Fractional / advisory", "verdict": "later", "text": "Fractional CPO rates are strong ($150–250/h) but the pipeline needs " "a reputation that principal or HoP scope builds first."}], "head_of_eng": "Head of Product is a real option within 2–3 years — but only if " "you enjoy hiring, stakeholder management and being measured on the " "team's outcomes. If the craft is what you love, Principal pays " "nearly as well without the meetings tax. Decide by trying: lead a " "PM hire and a quarter of roadmap before you commit.", "ai_impact": [ "Spec-writing, ticket grooming and basic analytics are commoditizing — the " "junior-PM tasks of 2020.", "PMs who prototype with AI tools compress discovery from weeks to days; make " "that visible.", "Judgment, narrative, and stakeholder trust stay human longest — that's the " "principal/HoP moat."], "skills": [ {"skill": "Product strategy & narrative", "why": "The principal gate — memos that change the roadmap, not just describe it."}, {"skill": "AI-assisted discovery", "why": "Prototype-first PMs ship evidence, not opinions — becoming the baseline."}, {"skill": "Org leadership", "why": "Hiring and developing PMs — the Head-of-Product gate; testable before you commit."}], "skills_note": "Sample analysis written for the demo persona — in the real app " "you author this yourself or with any AI assistant.", "roadmap": [ {"period": "0–6 mo", "title": "Own the hardest product area", "text": "Take the messy, cross-team problem; write the strategy memo that frames it."}, {"period": "6–18 mo", "title": "Test both forks", "text": "Lead one PM hire and a quarter of roadmap (HoP test) while shipping a principal-level bet."}, {"period": "18–36 mo", "title": "Commit and cash in", "text": "Promo to Principal, or jump to Head of Product at a scale-up — whichever fork felt like energy, not drain."}], "philosophies": { "max": {"title": "Max earnings", "text": "Optimize comp aggressively: jump every 2–3 years, interview yearly " "to stay calibrated, chase the biggest scope you can hold."}, "coast": {"title": "Coast & compound", "text": "Stay where the work is sane, automate savings, let the portfolio " "do the second job. Less money, more life."}, "note": "Sample framing — the app doesn't pick for you; it shows what each " "path does to the numbers in Forecasts."}, "next_steps": [ "Write one strategy memo this quarter that proposes killing something.", "Volunteer to run the next PM hiring loop — cheapest Head-of-Product test.", "Update the market barometer monthly — trend beats snapshot.", "Re-run this analysis after the next comp review."], "sources": []})}) # --- side business: a few months of drone-services revenue/costs --- for back, entries in { 3: [("revenue", "service", 3800, "Aerial photo shoot — sample entry"), ("cost", "equipment", -950, "Spare propellers & batteries — sample entry")], 2: [("revenue", "service", 4200, "Roof inspection flight — sample entry"), ("cost", "marketing", -180, "Meta ads — sample entry"), ("cost", "software", -49, "Editing suite subscription — sample entry")], 1: [("revenue", "service", 3600, "Construction-site progress video — sample entry"), ("revenue", "content", 450, "Stock-footage royalties — sample entry"), ("cost", "marketing", -175, "Meta ads — sample entry")], }.items(): for kind, cat, amount, desc in entries: best_effort("biz entry", planner.add_biz_entry, { "date": month_iso(back) + "-14", "kind": kind, "category": cat, "amount": amount, "description": desc}) # --- brokerage portfolio for the XTB-style rule engine (duplicates + tech concentration) --- planner.set_settings({"xtb_portfolio": json.dumps({"positions": [ {"name": "NASDAQ 100 ETF", "container": "Plan 1", "value": 20000, "theme": "tech"}, {"name": "NASDAQ 100 ETF", "container": "Plan 2", "value": 10000, "theme": "tech"}, {"name": "MSCI World IT ETF", "container": "Plan 1", "value": 16000, "theme": "tech"}, {"name": "Semiconductors ETF", "container": "Plan 2", "value": 10000, "theme": "tech"}, {"name": "VWCE All-World ETF", "container": "Plan 2", "value": 20000, "theme": "world"}, {"name": "S&P 500 ETF", "container": "Plan 1", "value": 9000, "theme": "us"}]})}) # --- open-source contribution research (generic sample) --- planner.set_settings({"analysis_contributions": json.dumps({ "goal": "Become a visible contributor to one well-known open-source project " "in the tools you already use every day.", "method": "Sample research: pick projects you actually run, sort by contributor " "activity and good-first-issue availability, start with docs/tests.", "repos": [ {"name": "ggml-org/llama.cpp", "url": "https://github.com/ggml-org/llama.cpp", "activity": "very active", "lang": "C++", "difficulty": "hard", "why": "You run it locally (this app's AI) — start with docs or build fixes."}, {"name": "simonw/llm", "url": "https://github.com/simonw/llm", "activity": "very active", "lang": "Python", "difficulty": "easy", "why": "Plugin ecosystem designed for outside contributors — ship a plugin."}, {"name": "chartjs/Chart.js", "url": "https://github.com/chartjs/Chart.js", "activity": "active", "lang": "JavaScript", "difficulty": "medium", "why": "This app's chart library — reproducible bug reports are welcomed."}], "badges": [ {"name": "Pull Shark", "how": "Two merged PRs anywhere", "status": "easy"}, {"name": "Public Sponsor", "how": "Sponsor a maintainer you rely on ($1/mo counts)", "status": "instant"}, {"name": "Starstruck", "how": "Publish a useful tool that earns 16+ stars", "status": "long game"}], "playbook": [ "Pick the project you use most — familiarity beats prestige for a first PR.", "Start with a docs fix or a failing-test repro; maintainers merge those fast.", "Answer two Discussions questions — visibility compounds.", "Then take one good-first-issue and see it through review."]})}) # --- AI learning tab (Career): a sample assessment for Alex + a little progress of their own --- planner.set_settings({"analysis_ai_learning": json.dumps({ "as_of": TODAY.isoformat(), "score": 2.6, "target": 4, "score_label": "Practitioner, edging toward Builder", "scale": ai_learning.LEVELS, "verdict": "Alex prototypes with AI every week and already treats outputs as claims to verify, which is the " "hard part of level 2. Level 3 is close: two of the projects already call models from code. " "Level 4 is measurement: workflows exist but nobody scores them; one small eval set per workflow closes most of it.", "projects": [ {"name": "alex/discovery-copilot", "grade": "B+", "what": "Interview-notes summariser with a retrieval step over past research.", "gap": "No eval set: nobody knows whether v2 beat v1. Add 30 labelled questions."}, {"name": "alex/pm-metrics-bot", "grade": "B", "what": "Weekly product metrics digest written by a model from SQL results.", "gap": "Numbers are not cross-checked against the query output; add a verifier pass."}, {"name": "alex/prompt-notebook", "grade": "C+", "what": "A folder of favourite prompts.", "gap": "Unversioned and unscored. Turn the five best into tested templates."}], "skills": [ {"id": "prompting", "area": "Foundations", "name": "Structured prompting", "status": "have", "how": "Roles, examples and output schemas used daily; keep a versioned template library.", "link": "https://www.promptingguide.ai/"}, {"id": "llm-limits", "area": "Foundations", "name": "Failure modes and limits", "status": "partial", "how": "Knows hallucination and context limits in theory; practise by writing down three failures per month."}, {"id": "cost", "area": "Foundations", "name": "Token cost and latency budgeting", "status": "missing", "how": "Estimate cost per task before building; compare a small local model with a hosted one on the same task."}, {"id": "rag", "area": "Building", "name": "Retrieval (RAG) basics", "status": "partial", "how": "Built one working pipeline; next is chunking experiments and hybrid keyword + embedding search.", "evidence": ["discovery-copilot · dep: chromadb", "discovery-copilot · commit: add hybrid search over research notes"], "link": "https://huggingface.co/learn"}, {"id": "tools", "area": "Building", "name": "Tool use and agents", "status": "partial", "how": "Wire a model to two real tools (SQL, calendar) with a hard step limit and logged calls."}, {"id": "local", "area": "Building", "name": "Running a local model", "status": "have", "how": "Runs a quantised model locally for private data.", "link": "https://github.com/ggml-org/llama.cpp"}, {"id": "evals", "area": "Quality", "name": "Evals and regression sets", "status": "missing", "how": "A gold set of 30 to 50 cases per workflow, scored on every prompt or model change.", "evidence": [], "link": "https://hamel.dev/blog/posts/evals/"}, {"id": "obs", "area": "Quality", "name": "Logging and review of real usage", "status": "partial", "how": "Prompt log exists; add ratings and a monthly review of the worst ten answers."}, {"id": "safety", "area": "Quality", "name": "Data privacy and prompt injection", "status": "partial", "how": "Keeps private data local; learn to treat retrieved text as untrusted input."}, {"id": "teach", "area": "Leadership", "name": "Teaching the team", "status": "missing", "how": "Run one 45-minute session on a workflow that saved real time, with the numbers."}, {"id": "policy", "area": "Leadership", "name": "Team usage guidelines", "status": "missing", "how": "A one-page policy: what data may go where, what must be verified, who reviews."}], "certs": [ {"id": "dlai", "name": "Short courses on agents and evals", "provider": "DeepLearning.AI", "cost": "free", "time": "2 to 3 h each", "format": "online", "priority": "high", "why": "Cheapest way to fill the evals and tool-use gaps with working notebooks.", "link": "https://www.deeplearning.ai/short-courses/"}, {"id": "hf", "name": "Open-source ML course (retrieval and fine-tuning modules)", "provider": "Hugging Face", "cost": "free", "time": "self-paced, ~20 h", "format": "online", "priority": "medium", "why": "Gives the vocabulary to argue with an ML engineer about retrieval quality.", "link": "https://huggingface.co/learn"}, {"id": "cohort", "name": "Applied AI evals cohort", "provider": "Independent instructors (sample entry)", "cost": "$800 to $1,500", "time": "4 weeks, 3 h/week", "format": "online", "priority": "medium", "why": "Worth the learning budget only if the evals gap is still open after the free courses.", "link": "https://hamel.dev/"}, {"id": "conf", "name": "Regional applied-AI conference", "provider": "Sample entry", "cost": "$450 plus travel", "time": "2 days", "format": "in person", "priority": "low", "why": "Good for the network; talks are recorded, so skip unless you plan to present.", "link": "https://www.latent.space/"}, {"id": "workshop", "name": "Hands-on agents workshop", "provider": "Sample entry", "cost": "$300", "time": "1 day", "format": "hybrid", "priority": "medium", "why": "One day with a mentor beats three weeks of tutorials for tool use.", "link": "https://www.deeplearning.ai/"}, {"id": "generic", "name": "Generic 'AI for managers' certificate", "provider": "Sample entry", "cost": "$1,200", "time": "6 weeks", "format": "online", "priority": "skip", "why": "Broad slides, no artefacts; nothing you cannot get from the free courses above.", "link": "https://www.deeplearning.ai/"}], "format_note": "Sample entries: in your install the assessment lists courses you can actually reach, " "with the employer learning budget in mind.", "sources": { "method": "Three layers so that staying current costs minutes, not evenings: a daily skim, a weekly deep read, a quarterly reset.", "daily": [ {"name": "Simon Willison's weblog", "link": "https://simonwillison.net/", "what": "Hands-on notes on new models and tools, with working examples."}], "weekly": [ {"name": "Latent Space", "link": "https://www.latent.space/", "what": "Interviews and write-ups from people shipping AI systems."}, {"name": "Hamel Husain's blog", "link": "https://hamel.dev/blog/", "what": "Practical evals and debugging of LLM applications."}], "quarterly": [ {"name": "Prompting Guide", "link": "https://www.promptingguide.ai/", "what": "Re-read the techniques list and retire what newer models made unnecessary."}], "services": [ {"name": "Hugging Face papers", "link": "https://huggingface.co/papers", "what": "Daily curated research papers, upvoted by practitioners."}, {"name": "GitHub trending (Python)", "link": "https://github.com/trending/python", "what": "What the open-source community is building this week."}]}, "generated": {"by": "repos", "narration": "local model", "sources": {"local": 3, "gh": 0}}, "routine": [ "Monday: pick one workflow to measure this week.", "Friday: write the log entry, even if the result was negative.", "Monthly: review the ten worst answers from the prompt log.", "Quarterly: re-rate the skill map and archive what stopped mattering."], "plan_12m": [ "Months 1 to 2: build a 30-case eval set for the discovery copilot and score v1 against v2.", "Months 3 to 4: add a verifier pass to the metrics bot and cut manual checking.", "Months 5 to 6: run a local vs hosted cost comparison on two workflows.", "Months 7 to 9: teach one 45-minute team session and write the team usage guideline.", "Months 10 to 12: publish a short write-up of the eval method and what it caught."]})}) planner.set_settings({"ai_learning_history": json.dumps([ {"date": (TODAY - timedelta(days=90)).isoformat(), "score": 1.9, "level": 1, "have": 1, "partial": 5, "missing": 5, "repos": 2, "narration": "template"}, {"date": (TODAY - timedelta(days=45)).isoformat(), "score": 2.3, "level": 2, "have": 2, "partial": 4, "missing": 5, "repos": 3, "narration": "local model"}, {"date": TODAY.isoformat(), "score": 2.6, "level": 2, "have": 2, "partial": 5, "missing": 4, "repos": 3, "narration": "local model"}])}) planner.set_settings({"ai_learning_state": json.dumps({ "skills": {"rag": "partial", "tools": "partial"}, "certs": {"dlai": "in progress", "hf": "planned"}, "log": [ {"date": (TODAY - timedelta(days=41)).isoformat(), "text": "Wrote 12 interview-summary questions with expected answers; v1 passes 8 of 12. Baseline recorded."}, {"date": (TODAY - timedelta(days=17)).isoformat(), "text": "Hybrid keyword + embedding search on the research folder: relevant notes in the top 5 went from 7 to 10 of 12. Adopting."}, {"date": (TODAY - timedelta(days=4)).isoformat(), "text": "Metrics bot quoted a number that was not in the query result. Added a verifier pass; it flagged 2 of 9 digests."}]})}) # --- fake performance-marketing intelligence (business module) --- MARKETING = { "weeks": [ {"week": "2026-07-06 – 2026-07-12", "spend_eur": 184.5, "summary": "CTR up 14% after the new creative set; CPC steady at €0.42.", "recommendation": "Shift 20% of budget to the lookalike audience — it converts 1.8× better."}, {"week": "2026-06-29 – 2026-07-05", "spend_eur": 176.2, "summary": "Weekend impressions dipped (holiday); conversions held.", "recommendation": "Keep budgets flat; revisit after the seasonal dip."}, {"week": "2026-06-22 – 2026-06-28", "spend_eur": 190.0, "summary": "New landing page cut bounce rate from 61% to 48%.", "recommendation": "Roll the winning variant to all ad sets."}], "insights": [ {"category": "audience", "platform": "meta", "confidence": 0.86, "is_active": True, "insight": "Lookalike 1% of past customers outperforms interest targeting 1.8× on ROAS."}, {"category": "creative", "platform": "meta", "confidence": 0.74, "is_active": True, "insight": "Short vertical video beats static images on CPM-adjusted conversions."}, {"category": "timing", "platform": "meta", "confidence": 0.61, "is_active": True, "insight": "Tue–Thu mornings deliver the cheapest qualified clicks."}], "hypotheses": [ {"title": "UGC-style creative lowers CPA", "predicted_outcome": "CPA −20%", "success_metric": "CPA", "target_value": "≤ €9.50", "status": "active"}, {"title": "Broad targeting + strong creative beats narrow interests", "predicted_outcome": "ROAS +15%", "success_metric": "ROAS", "target_value": "≥ 3.2", "status": "active"}], "total_spend_eur": 1420.7, "recent_spend_eur": 550.7, "recent_clicks": 1311, } # --- RSU sized for the mid-dev story (seed's grant would dwarf the salary) --- best_effort("rsu resize", market.update_rsu, {"ticker": "AAPL", "grant_value_usd": 60000, "shares_held": 120, "shares_next_vest": 30}) # --- an "advanced" RSU vest deep-dive (sample research snapshot) --- last_aapl = (market.prices("AAPL", days=5) or [{}])[-1].get("close", 230) planner.set_settings({"rsu_vest_analysis": json.dumps({ "vest_month": "August 2026", "as_of": TODAY.isoformat(), "price": round(float(last_aapl), 2), "headline": "Hold through the vest, then sell 60% within the trading window — " "concentration risk outweighs the upside of waiting.", "sections": [ {"title": "Concentration math", "text": "Post-vest, employer stock would be ~11% of net worth and ~24% of the " "liquid portfolio — above the 10% single-name ceiling this plan uses. " "Selling 60% at vest brings it back inside the band; taxes are neutral " "since the vest itself is the taxable event."}, {"title": "What the market is pricing", "text": "Options imply ~29% annualized volatility into the next earnings print; " "the 1-month band from this app's own forecast model is wide but " "symmetric — no directional edge, so risk management decides, not timing."}, {"title": "Scenario table", "text": "Bull (+15%): extra ~$3.4k on the held tranche. Bear (−15%): −$3.4k plus " "the correlated hit to salary security. The asymmetry of holding " "concentrated employer stock is behavioral, not financial."}, {"title": "Execution", "text": "Sell in two tranches inside the window (day 1 and day 10) to average " "out the post-vest dip pattern; route proceeds to the ETF auto-invest " "the same week so the cash doesn't linger."}], "sources": []})}) # --- distilled experiences (sample lessons, so the feature shows populated) --- best_effort("experience table", __import__("experience").ensure_tables) for q, lesson in [ ("Overpay the mortgage or invest?", "When comparing loan overpayment vs investing, compare the loan rate to the " "expected market return AFTER capital-gains tax — not gross. Overpaying a " "6–7% loan usually beats a taxed ~5% net market return."), ("Should I sell RSU at vest?", "Sell enough at vest to bring any single employer stock back under ~10% of the " "liquid portfolio; the vest is already the taxable event, so selling then is " "roughly tax-neutral and cuts concentration risk."), ("Is the currency signal telling me to convert now?", "Never convert on a single day's move — wait for the FX engine's threshold to " "trigger, and prefer natural hedges (spend income in the currency you earn) " "before an explicit conversion."), ]: best_effort("experience", __import__("experience").save, q, lesson) # --- sample AI prompt-log entries, so the log + "💡 Learn from this" flow shows # (these are the questions the sample lessons above were distilled from) --- best_effort("llm_log table", __import__("llm_log").ensure_tables) for q, a in [ ("Should I overpay the mortgage or invest the surplus?", "With a ~6.5% mortgage vs a ~5% net expected market return (after 19% " "capital-gains tax), overpaying wins on a risk-adjusted basis — but keep the " "emergency fund intact first. Bottom line: keep the ETF auto-invest running and " "direct the annual bonus to overpayment."), ("Is now a good time to convert to EUR for the house deposit?", "No clear signal today — the pair is range-bound and the FX engine hasn't crossed " "its threshold. Prefer the natural hedge: let USD vests flow toward EUR. Bottom " "line: wait for the engine's trigger; don't convert on a single day's move."), ("Should I sell my RSU at the next vest?", "Employer stock would be ~24% of the liquid portfolio post-vest — above the 10% " "single-name ceiling. Sell ~60% at vest (the taxable event already happened, so " "it's roughly tax-neutral) and route the proceeds to the ETF auto-invest."), ]: best_effort("llm_log", __import__("llm_log").record, q, {"mode": "local", "rag_used": True, "local": {"ok": True, "text": a}, "best": a}) # --- the AI improvement tab: ratings attributed to the lessons in context, three quality-eval # runs and an indexed memory, so the tab tells the full loop instead of "nothing yet" --- def _ai_story(): import rag lessons = {r["question"]: r["id"] for r in eb._rows("select id, question from agent_experiences")} logs = eb._rows("select id, prompt from llm_log order by ts") by_topic = [("overpay", "Overpay the mortgage or invest?", 1), ("EUR", "Is the currency signal telling me to convert now?", -1), ("RSU", "Should I sell RSU at vest?", 1)] for word, lesson_q, rating in by_topic: row = next((r for r in logs if word in r["prompt"]), None) if row and lesson_q in lessons: eb._exec("update llm_log set rating=?, lessons=? where id=?", (rating, json.dumps([lessons[lesson_q]]), row["id"])) eval_dir = data_dir / "eval" eval_dir.mkdir(exist_ok=True) (eval_dir / "gold-demo.json").write_text("[]") for days_ago, passed in ((40, 11), (19, 13), (3, 15)): ts = (datetime.now() - timedelta(days=days_ago)).strftime("%Y-%m-%d %H:%M") (eval_dir / f"run-{ts[:10]}.json").write_text(json.dumps( {"variant": "baseline", "model": "local-9b-q6 (demo)", "ts": ts, "passed": passed, "total": 16, "cases": {}})) rag.reindex() # BM25 only on CI (no embedding server); clears the "new data" flag best_effort("AI improvement story", _ai_story) # --- market brief (handcrafted — no LLM in the build) --- brief = { "headline": "Markets are drifting sideways while rate-cut expectations firm up; " "nothing in this demo portfolio needs action this week.", "as_of": TODAY.isoformat(), "generated_by": "demo snapshot", "regime": "Calm — carry on with the plan", "highlights": [ {"icon": "📈", "title": "Equities steady", "text": "Broad indices near highs on soft-landing hopes."}, {"icon": "💱", "title": "FX quiet", "text": "EUR/USD range-bound; no conversion signal."}, {"icon": "🏦", "title": "Rates drifting down", "text": "Markets price gradual cuts into year-end."}, {"icon": "🛢️", "title": "Oil calm", "text": "Supply steady; inflation pass-through muted."}], "geopolitics": [ {"title": "Why this matters for a long-term plan", "text": "Sample context written for the demo: headlines move daily prices, " "but the plan only reacts when thresholds are crossed."}], "positions": [ {"ticker": "VWCE.DE", "stance": "hold", "text": "Core holding — keep the monthly auto-invest."}, {"ticker": "AAPL", "stance": "hold", "text": "RSU exposure already large; don't add."}, {"ticker": "EURUSD=X", "stance": "hold", "text": "No signal — wait for the engine's threshold."}], "method_note": "Demo brief — in the real app this is generated daily by YOUR local " "model from cached quotes, schema-locked so it always renders.", } planner.set_settings({"analysis_market_brief_daily": json.dumps(brief), "analysis_market_brief": json.dumps({**brief, "as_of": (TODAY - timedelta(days=5)).isoformat(), "headline": "Weekly view: same picture — stay the course. " "(Demo copy of the weekly brief.)"})}) # --- public GitHub activity of the maintainer (public data; synthetic fallback) --- def github_activity_snapshot(days=90): counts, prs, issues, reviews = {}, 0, 0, 0 try: for page in (1, 2, 3): req = urllib.request.Request( f"https://api.github.com/users/grzella/events/public?per_page=100&page={page}", headers={"User-Agent": "kist-demo-build"}) with urllib.request.urlopen(req, timeout=10) as r: events = json.loads(r.read()) if not events: break for ev in events: d = ev["created_at"][:10] if ev["type"] == "PushEvent": counts[d] = counts.get(d, 0) + len(ev["payload"].get("commits") or [1]) elif ev["type"] == "PullRequestEvent": prs += 1 elif ev["type"] == "IssuesEvent": issues += 1 elif ev["type"] == "PullRequestReviewEvent": reviews += 1 connected = bool(counts) except Exception as e: print(f" (github events fallback: {str(e)[:60]})") connected = False if not connected: # deterministic synthetic pattern for back in range(days): d = (TODAY - timedelta(days=back)).isoformat() counts[d] = max(0, RNG.randint(-2, 6)) prs, issues, reviews = 4, 2, 3 series = [{"date": (TODAY - timedelta(days=back)).isoformat(), "count": counts.get((TODAY - timedelta(days=back)).isoformat(), 0)} for back in range(days - 1, -1, -1)] active = [s for s in series if s["count"] > 0] streak = best = 0 for s in series: streak = streak + 1 if s["count"] > 0 else 0 best = max(best, streak) total = sum(s["count"] for s in series) return {"configured": True, "repos": 1, "days": days, "today": series[-1]["count"], "week": sum(s["count"] for s in series[-7:]), "streak": streak, "best_streak": best, "active_days": len(active), "active_pct": round(100 * len(active) / days), "total": total, "avg_per_active": round(total / max(1, len(active)), 1), "series": series, "github": {"connected": connected, "login": "grzella", "prs": prs, "issues": issues, "reviews": reviews}} # ================= snapshot every GET endpoint ================================ import app as kist_app # noqa: E402 client = kist_app.app.test_client() def fname(path_q): return re.sub(r"[^A-Za-z0-9._-]", "_", path_q.lstrip("/")) + ".json" snap_dir = OUT / "demo-data" if OUT.exists(): shutil.rmtree(OUT) snap_dir.mkdir(parents=True) paths = sorted(str(r) for r in kist_app.app.url_map.iter_rules() if "GET" in r.methods and not r.arguments and str(r).startswith("/api")) for t in TICKERS: paths += [f"/api/market/prices/{t}?days=100000", f"/api/market/analytics/{t}", f"/api/forecast/bands/{t}"] # parametrized rules the crawl above can't enumerate paths += ["/api/analysis/career", "/api/analysis/contributions", "/api/analysis/property"] # trajectory scenario controls (Forecasts tab): the default combination plus every single # control moved off its default, in the exact query order the UI sends. Any other combination # falls back to the base snapshot in api.get() with a toast. _TJ_DEFAULT = {"months": "24", "bonus": "1", "team": "0", "usd": "0", "real": "0"} _TJ_ALT = {"months": ["12", "36"], "bonus": ["0"], "team": ["-30", "30"], "usd": ["-10", "10"], "real": ["1"]} def _tj(q): return "/api/trajectory?" + "&".join(f"{k}={q[k]}" for k in ("months", "bonus", "team", "usd", "real")) paths.append(_tj(_TJ_DEFAULT)) for k, alts in _TJ_ALT.items(): for a in alts: paths.append(_tj({**_TJ_DEFAULT, k: a})) # Some endpoints report on the LOCAL MACHINE running this script (e.g. backup # destinations auto-detected from the filesystem: home dir, cloud-sync folder # names that embed the account's email). On a clean CI runner they're always # empty; run this script on your own laptop and they leak your real # username/email into the committed-nowhere-but-deployed static demo. Scrub # them unconditionally so the demo is safe regardless of where it's built. def _demo_ai_improve(d): """No model runs in the static demo: the "Local model" row would always be red there. Say so instead, and recount the header from the rewritten rows.""" for st in d.get("steps", []): if st["area"] == "model": st.update(state="ok", action=None, detail="Static demo: no model runs here. In your install this row shows your llama-server and turns red when it stops.") if st["area"] == "eval" and st["state"] != "ok": st["detail"] = st["detail"].split(". Will not run")[0] + "." d["errors"] = sum(1 for st in d["steps"] if st["state"] == "error") d["todo"] = sum(1 for st in d["steps"] if st["state"] == "todo") return d _SCRUB = { "/api/backup/status": lambda d: {**d, "destinations": [], "dir": ""}, "/api/ai/improve": _demo_ai_improve, # local paths of the machine that builds the demo (restart command, checkout dir) "/api/health": lambda d: {**d, "restart_cmd": "./run.sh"}, "/api/git": lambda d: {**d, "repo": ""}, } ok = fail = 0 for p in paths: try: r = client.get(p) if r.status_code == 200 and r.is_json: payload = r.get_json() scrub = _SCRUB.get(p.split("?")[0]) if scrub: payload = scrub(payload) (snap_dir / fname(p)).write_text(json.dumps(payload, ensure_ascii=False)) ok += 1 else: fail += 1 print(f" skip {p} ({r.status_code})") except Exception as e: fail += 1 print(f" skip {p} ({str(e)[:60]})") print(f"Snapshots: {ok} saved, {fail} skipped") # post-crawl patches: endpoints that need Supabase / gh CLI in a real install (snap_dir / fname("/api/business/marketing")).write_text(json.dumps(MARKETING)) (snap_dir / fname("/api/github-activity")).write_text( json.dumps(github_activity_snapshot())) # --- compute-only POSTs the Forecasts view fires (overpayment simulations): # bake their responses under a path+body key that api.js reconstructs --- def js_num(v): """Mirror JSON.stringify: integral floats print as ints (3500.0 → 3500).""" return int(v) if isinstance(v, float) and v.is_integer() else v def post_snapshot(path, body): key = "post_" + path + "__" + json.dumps(body, separators=(",", ":")) r = client.post(path, json=body) if r.status_code == 200 and r.is_json: (snap_dir / (re.sub(r"[^A-Za-z0-9._-]", "_", key.lstrip("/")) + ".json") ).write_text(json.dumps(r.get_json(), ensure_ascii=False)) else: print(f" skip POST {path} ({r.status_code})") def snap(n): return json.loads((snap_dir / fname(n)).read_text()) loan = next((d for d in snap("/api/debts")["debts"] if d["balance"] > 0), None) if loan: bonus = js_num(float(snap("/api/settings").get("annual_bonus_net") or 20000)) vest = js_num(float(snap("/api/rsu").get("next_vest_value_pln") or 0)) for amount in {bonus, vest, js_num(bonus + vest)}: if amount < loan["balance"]: # JS skips only amounts covering the whole balance post_snapshot("/api/forecast/mortgage", { "balance": js_num(loan["balance"]), "monthly_payment": js_num(loan["minimum_payment"]), "months_left": js_num(loan.get("months_left") or loan["schedule"]["months"]), "overpayment": amount}) # ================= assemble dist ============================================== for d in ("css", "js", "vendor"): shutil.copytree(ROOT / "static" / d, OUT / d) html = (ROOT / "static" / "index.html").read_text() html = html.replace('"/static/', '"') html = html.replace("window.KIST_STATIC_DEMO=1\n ", """
🟡 Demo mode — sample data (fake persona “Alex Demo”), read-only. Nothing here is real or financial advice.
""", 1) (OUT / "index.html").write_text(html) (OUT / ".nojekyll").write_text("") print(f"✅ Demo built → {OUT}")