demo/build_demo.py
Rama main · réplica de lectura del repositorio original.
#!/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("<script", '<script>window.KIST_STATIC_DEMO=1</script>\n <script', 1)
html = html.replace("<body>", """<body>
<div style="background:#ffd166;color:#1c1e26;text-align:center;padding:6px 12px;
font-weight:600;font-size:.9em">🟡 Demo mode — sample data (fake persona
“Alex Demo”), read-only. Nothing here is real or financial advice.</div>""", 1)
(OUT / "index.html").write_text(html)
(OUT / ".nojekyll").write_text("")
print(f"✅ Demo built → {OUT}")