from __future__ import annotations import os import sys from pathlib import Path # Add the 'src' directory to sys.path to ensure 'god_sim' package is findable # This handles cases where the app is run from different working directories src_path = str(Path(__file__).parent.parent.parent) if src_path not in sys.path: sys.path.insert(0, src_path) import pandas as pd import plotly.express as px import streamlit as st from god_sim.config import WorldConfig from god_sim.analytics.history import append_run_history, load_run_history from god_sim.engine.sim import run_simulation from god_sim.engine.optimizer import run_optimization_step from god_sim.insights.llm import check_ollama_health, generate_insights, insight_config_from_env st.set_page_config(page_title="GOD Simulator", layout="wide") st.title("GOD — Simulation Sandbox (V1)") st.caption("Tune parameters, run the world, inspect emergent metrics.") # Use tabs to organize the UI tab_main, tab_optimizer = st.tabs(["🌎 Main Simulator", "âš–ī¸ World Optimizer"]) with tab_optimizer: st.header("Sustainable Equilibrium Optimizer") st.write("Find the parameters that keep the world alive the longest for a given resource capacity.") fixed_capacity = st.number_input("Fixed Resource Capacity", min_value=100.0, value=10000.0, step=100.0) opt_iterations = st.slider("Number of trials", min_value=5, max_value=100, value=20) if st.button("Start Optimization", type="primary"): with st.spinner("Hunting for the optimal world configuration..."): opt_out = run_optimization_step(fixed_capacity, iterations=opt_iterations) st.success(f"Optimization complete! Best longevity: {opt_out['best_fitness']/10 if opt_out['best_fitness'] > 1000 else opt_out['best_fitness']} ticks.") best_cfg = opt_out["best_config"] st.subheader("Optimal Parameters Found") col1, col2, col3 = st.columns(3) col1.metric("Num Souls", best_cfg.num_souls) col2.metric("Replenish Rate", f"{best_cfg.resource_replenish_rate:.2f}") col3.metric("Initial Moral Bias", f"{best_cfg.initial_moral_bias_mean:.2f}") col4, col5, col6 = st.columns(3) col4.metric("Karmic Influence", f"{best_cfg.rebirth_influence_strength:.2f}") col5.metric("Event Rate", f"{best_cfg.event_rate:.3f}") col6.metric("Longevity (Ticks)", opt_out['best_config'].ticks if opt_out['best_fitness'] > 1000 else opt_out['best_fitness']) if st.button("Load Optimal Run to Main Dashboard"): st.session_state["display_out"] = run_simulation(best_cfg) st.rerun() with tab_main: def info_box(title: str, body: str) -> None: with st.expander(f"â„šī¸ {title}", expanded=False): st.write(body) def render_dashboard(out: dict) -> None: timestamp = out.get("created_at_utc", "Recent Run") st.subheader(f"Dashboard: {timestamp}") df = pd.DataFrame(out["series"]) c1, c2, c3, c4 = st.columns(4) c1.metric("Final mean karma", f"{df['mean_karma'].iloc[-1]:.3f}") c2.metric("Final mean wellbeing", f"{df['mean_wellbeing'].iloc[-1]:.3f}") c3.metric("Final mean health", f"{df['mean_health'].iloc[-1]:.3f}") c4.metric("Final resources", f"{df['resource'].iloc[-1]:.1f}") info_box( "What these top metrics mean", ( "- Final mean karma: average karma score at the end of the run.\n" "- Final mean wellbeing: average quality-of-life proxy (0 to 1).\n" "- Final mean health: average physical health proxy (0 to 1).\n" "- Final resources: units left in the global shared resource pool." ), ) st.subheader("Time series") left, right = st.columns(2) with left: st.plotly_chart(px.line(df, x="time", y=["mean_wellbeing", "mean_health"], title="Wellbeing & Health"), width="stretch") info_box( "Wellbeing & Health graph", ( "Tracks average wellbeing and health over time. Rising lines usually indicate favorable " "resource/event conditions; falling lines suggest sustained stress, scarcity, or frequent negative events." ), ) st.plotly_chart(px.line(df, x="time", y=["mean_karma"], title="Mean karma"), width="stretch") info_box( "Mean karma graph", ( "Shows average accumulated karma across souls. Upward trend means, on balance, actions/events are " "adding positive karma; flat/declining means neutral or negative net behavior." ), ) with right: st.plotly_chart(px.line(df, x="time", y=["resource"], title="Resources"), width="stretch") info_box( "Resources graph", ( "Shows the level of the shared global resource pool. Persistent decline indicates structural scarcity; " "stable or rising values indicate replenishment is keeping up with demand." ), ) st.plotly_chart(px.bar(df, x="time", y="events", title="Events per tick"), width="stretch") info_box( "Events per tick graph", ( "Counts random events that occurred each tick. Spikes represent volatile periods that can drive abrupt " "changes in health, wellbeing, and karma." ), ) with st.expander("Raw output"): st.json(out["config"]) st.dataframe(df, width="stretch") with st.sidebar: st.header("Scenario") seed = st.number_input("Seed", min_value=0, max_value=1_000_000_000, value=42, step=1) ticks = st.slider( "Ticks (simulation duration)", min_value=10, max_value=2000, value=200, step=10, help="How long to run the simulation (number of time steps/turns). More ticks = longer world evolution.", ) num_souls = st.slider( "Number of souls (population size)", min_value=50, max_value=5000, value=300, step=50, help="How many souls/entities exist in the world. More souls = higher population and resource pressure per tick.", ) st.caption("Ticks = for how long. Number of souls = for how many entities.") st.header("Resources") resource_capacity = st.number_input("Resource capacity", min_value=100.0, value=10_000.0, step=100.0) resource_start = st.number_input("Resource start", min_value=0.0, value=6_000.0, step=100.0) resource_replenish_rate = st.number_input("Replenish / tick", min_value=0.0, value=120.0, step=10.0) st.header("Nature / bias") initial_moral_bias_mean = st.slider("Mean moral bias (good ↔ bad)", min_value=-1.0, max_value=1.0, value=0.0, step=0.05) initial_moral_bias_std = st.slider("Moral bias std", min_value=0.0, max_value=1.0, value=0.5, step=0.05) rebirth_influence_strength = st.slider("Karma → rebirth influence", min_value=0.0, max_value=1.0, value=0.25, step=0.05) st.header("Events") event_rate = st.slider("Event rate", min_value=0.0, max_value=0.5, value=0.05, step=0.01) run = st.button("Run simulation", type="primary") if run: cfg = WorldConfig( seed=int(seed), ticks=int(ticks), num_souls=int(num_souls), resource_capacity=float(resource_capacity), resource_start=float(resource_start), resource_replenish_rate=float(resource_replenish_rate), initial_moral_bias_mean=float(initial_moral_bias_mean), initial_moral_bias_std=float(initial_moral_bias_std), rebirth_influence_strength=float(rebirth_influence_strength), event_rate=float(event_rate), ) with st.spinner("Running..."): out = run_simulation(cfg) st.session_state["display_out"] = out saved = append_run_history(out) st.success(f"Saved run to history as `{saved['run_id']}`.") if "display_out" in st.session_state: render_dashboard(st.session_state["display_out"]) st.divider() st.subheader("Run History") history = load_run_history() if history: st.caption(f"Stored runs: {len(history)} (saved in `data/run_history.json`).") # Selection mechanism run_options = {f"{r['run_id']} ({r['created_at_utc']})": r for r in reversed(history)} selected_run_label = st.selectbox("Select a past run to view dashboard", options=list(run_options.keys())) if st.button("Load selected run"): st.session_state["display_out"] = run_options[selected_run_label] st.rerun() rows: list[dict[str, object]] = [] for r in reversed(history[-20:]): cfg_hist = r.get("config", {}) if isinstance(r.get("config"), dict) else {} rows.append( { "run_id": r.get("run_id", ""), "created_at_utc": r.get("created_at_utc", ""), "seed": cfg_hist.get("seed", ""), "ticks": cfg_hist.get("ticks", ""), "num_souls": cfg_hist.get("num_souls", ""), "res_cap": cfg_hist.get("resource_capacity", ""), "res_start": cfg_hist.get("resource_start", ""), "res_replenish": cfg_hist.get("resource_replenish_rate", ""), "moral_bias": cfg_hist.get("initial_moral_bias_mean", ""), "moral_bias_std": cfg_hist.get("initial_moral_bias_std", ""), "karma_influence": cfg_hist.get("rebirth_influence_strength", ""), "event_rate": cfg_hist.get("event_rate", ""), } ) st.dataframe(pd.DataFrame(rows), width="stretch") st.subheader("âš–ī¸ AI Comparison") st.caption("Select two runs from history to compare them using AI.") compare_runs = st.multiselect( "Select exactly 2 runs", options=list(run_options.keys()), max_selections=2, help="Select two runs to see how their parameters influenced the emergent world outcomes." ) if len(compare_runs) == 2: if st.button("Generate Comparative Insights"): from god_sim.insights.llm import generate_comparative_insights run_a = run_options[compare_runs[0]] run_b = run_options[compare_runs[1]] cfg_ins = insight_config_from_env() # (Assuming standard provider/model selection from below or env) try: with st.spinner("Analyzing differences..."): comparison_text = generate_comparative_insights(run_a, run_b, cfg=cfg_ins) st.markdown(comparison_text) except Exception as e: st.error(f"Comparison failed: {e}") else: st.caption("No runs stored yet. Run a simulation to create history.") st.divider() st.subheader("Insights (local Gemma)") st.caption("Uses a local model. Choose Ollama (server) or llama.cpp (fully offline). Configure via env vars in README.") st.info("For provider `llama_cpp`, use a local `.gguf` model file path. `.bin` files are not supported by llama.cpp.") insight_cols = st.columns([1, 1, 2]) with insight_cols[0]: provider = st.selectbox("Provider", options=["ollama", "llama_cpp", "openai_compatible"], index=0) with insight_cols[1]: model = st.text_input( "Model", value="gemma2:2b" if provider == "ollama" else ("models/gemma-2-2b-it-Q4_K_M.gguf" if provider == "llama_cpp" else "gemma"), help="For llama.cpp, set this to a local model file path.", ) if provider == "ollama": hc_cols = st.columns([1, 4]) with hc_cols[0]: run_health_check = st.button("Check Ollama health") with hc_cols[1]: st.caption("Checks server reachability and whether the selected model exists locally.") if run_health_check: cfg0 = insight_config_from_env() health = check_ollama_health(base_url=cfg0.ollama_base_url, model=model) if not health["reachable"]: st.error( "Ollama server is not reachable. Start it first (example: `ollama serve`) " f"and ensure base URL is `{cfg0.ollama_base_url}`.\n\nDetails: {health['error']}" ) elif not health["model_present"]: st.warning( f"Ollama is running, but model `{model}` is not available locally. " f"Run `ollama pull {model}` first." ) if health["models"]: st.caption("Available local models: " + ", ".join(health["models"])) else: st.success(f"Ollama is healthy and model `{model}` is available.") gen = st.button("Generate insights from last/selected run") if gen: out = st.session_state.get("display_out") if not out: st.warning("Run a simulation or select one from history first.") else: cfg = insight_config_from_env() # Override UI-selected provider/model if provider == "ollama": cfg = type(cfg)( provider="ollama", ollama_base_url=cfg.ollama_base_url, ollama_model=model, openai_base_url=cfg.openai_base_url, openai_model=cfg.openai_model, openai_api_key=cfg.openai_api_key, model_path=cfg.model_path, n_ctx=cfg.n_ctx, n_threads=cfg.n_threads, temperature=cfg.temperature, max_tokens=cfg.max_tokens, ) elif provider == "llama_cpp": cfg = type(cfg)( provider="llama_cpp", ollama_base_url=cfg.ollama_base_url, ollama_model=cfg.ollama_model, openai_base_url=cfg.openai_base_url, openai_model=cfg.openai_model, openai_api_key=cfg.openai_api_key, model_path=model, n_ctx=cfg.n_ctx, n_threads=cfg.n_threads, temperature=cfg.temperature, max_tokens=cfg.max_tokens, ) else: cfg = type(cfg)( provider="openai_compatible", ollama_base_url=cfg.ollama_base_url, ollama_model=cfg.ollama_model, openai_base_url=cfg.openai_base_url, openai_model=model, openai_api_key=cfg.openai_api_key, model_path=cfg.model_path, n_ctx=cfg.n_ctx, n_threads=cfg.n_threads, temperature=cfg.temperature, max_tokens=cfg.max_tokens, ) try: with st.spinner("Calling local model..."): text = generate_insights(out, cfg=cfg) st.markdown(text if text else "_(empty response)_") except Exception as e: st.error(f"Insight generation failed: {e}")