god / src /god_sim /app /streamlit_app.py
Vikram Vasudevan
compararive insights
696dddf
Raw
History Blame Contribute Delete
14.9 kB
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}")