Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
58 KiB
JSON
1 line
58 KiB
JSON
{"repo": "anthonyrodrigues443/autonomous-ml-and-llm-iteration-agent", "files": {".env.example": {"4fca15e649879f1464c990930a1ab44e408a8c69": {"t": 1785747077.1963506, "n": 2218, "h": "d91cc2e7f34e1122", "c": "# Copy this file to .env and fill in real values. .env is gitignored.\n# See pyproject.toml for which keys are required vs optional.\n\n# ═══════════════════════════════════════════════════════════════\n# Default: LOCAL Ollama backend — $0, no API keys, fully private\n# ═══════════════════════════════════════════════════════════════\n\nITERATE_BACKEND_URL=http://localhost:11434/v1\nITERATE_BACKEND_API_KEY=ollama\nITERATE_MODEL=qwen3:14b\nITERATE_BACKEND_TIMEOUT=120\n\n# Run `ollama pull qwen3:14b` then `ollama serve` to start.\n# (qwen3 emits structured tool calls — required for the agentic loop; the\n# qwen2.5-coder variant does not. qwen3 also has a thinking mode on by default.)\n# Any OpenAI-compatible endpoint works here — just change the URL + model.\n\n# ─── Required: sandbox for code execution ────────────────────\nE2B_API_KEY=\n\n# ─── Required: dataset access ────────────────────────────────\nKAGGLE_USERNAME=\nKAGGLE_KEY=\n\n# ═══════════════════════════════════════════════════════════════\n# Optional: cloud LLM backends (switch ITERATE_BACKEND_URL to use)\n# ═══════════════════════════════════════════════════════════════\n\n# Groq — free tier, fast Llama 3.3 70B\n# ITERATE_BACKEND_URL=https://api.groq.com/openai/v1\n# ITERATE_BACKEND_API_KEY=<your-groq-key>\n# ITERATE_MODEL=llama-3.3-70b-versatile\nGROQ_API_KEY=\n\n# Together AI — paid, very cheap Llama 3.3 70B\n# ITERATE_BACKEND_URL=https://api.together.xyz/v1\n# ITERATE_BACKEND_API_KEY=<your-together-key>\n# ITERATE_MODEL=meta-llama/Llama-3.3-70B-Instruct-Turbo\nTOGETHER_API_KEY=\n\n# Deepseek — paid, strong on code\n# ITERATE_BACKEND_URL=https://api.deepseek.com/v1\n# ITERATE_BACKEND_API_KEY=<your-deepseek-key>\n# ITERATE_MODEL=deepseek-chat\nDEEPSEEK_API_KEY=\n\n# OpenAI — for GPT-4/5\nOPENAI_API_KEY=\n\n# Anthropic — for Claude (uses separate non-OpenAI-compatible adapter)\nANTHROPIC_API_KEY=\n\n# ─── Optional: logging adapters ──────────────────────────────\nNOTION_API_KEY=\nNOTION_DATABASE_ID=\nSLACK_WEBHOOK_URL=\n\n# ─── Optional: tracing ───────────────────────────────────────\nLANGFUSE_PUBLIC_KEY=\nLANGFUSE_SECRET_KEY=\nLANGFUSE_HOST=\n\n# ─── Local config ────────────────────────────────────────────\nITERATE_MEMORY_DB=.iterate/memory.db\nITERATE_RUNS_DIR=.iterate/runs\n"}, "57f9a83c6f8e322ce2b598423dcc7e70ab73164a": {"t": 1785770269.9334006, "n": 2218, "h": "d91cc2e7f34e1122", "c": "# Copy this file to .env and fill in real values. .env is gitignored.\n# See pyproject.toml for which keys are required vs optional.\n\n# ═══════════════════════════════════════════════════════════════\n# Default: LOCAL Ollama backend — $0, no API keys, fully private\n# ═══════════════════════════════════════════════════════════════\n\nITERATE_BACKEND_URL=http://localhost:11434/v1\nITERATE_BACKEND_API_KEY=ollama\nITERATE_MODEL=qwen3:14b\nITERATE_BACKEND_TIMEOUT=120\n\n# Run `ollama pull qwen3:14b` then `ollama serve` to start.\n# (qwen3 emits structured tool calls — required for the agentic loop; the\n# qwen2.5-coder variant does not. qwen3 also has a thinking mode on by default.)\n# Any OpenAI-compatible endpoint works here — just change the URL + model.\n\n# ─── Required: sandbox for code execution ────────────────────\nE2B_API_KEY=\n\n# ─── Required: dataset access ────────────────────────────────\nKAGGLE_USERNAME=\nKAGGLE_KEY=\n\n# ═══════════════════════════════════════════════════════════════\n# Optional: cloud LLM backends (switch ITERATE_BACKEND_URL to use)\n# ═══════════════════════════════════════════════════════════════\n\n# Groq — free tier, fast Llama 3.3 70B\n# ITERATE_BACKEND_URL=https://api.groq.com/openai/v1\n# ITERATE_BACKEND_API_KEY=<your-groq-key>\n# ITERATE_MODEL=llama-3.3-70b-versatile\nGROQ_API_KEY=\n\n# Together AI — paid, very cheap Llama 3.3 70B\n# ITERATE_BACKEND_URL=https://api.together.xyz/v1\n# ITERATE_BACKEND_API_KEY=<your-together-key>\n# ITERATE_MODEL=meta-llama/Llama-3.3-70B-Instruct-Turbo\nTOGETHER_API_KEY=\n\n# Deepseek — paid, strong on code\n# ITERATE_BACKEND_URL=https://api.deepseek.com/v1\n# ITERATE_BACKEND_API_KEY=<your-deepseek-key>\n# ITERATE_MODEL=deepseek-chat\nDEEPSEEK_API_KEY=\n\n# OpenAI — for GPT-4/5\nOPENAI_API_KEY=\n\n# Anthropic — for Claude (uses separate non-OpenAI-compatible adapter)\nANTHROPIC_API_KEY=\n\n# ─── Optional: logging adapters ──────────────────────────────\nNOTION_API_KEY=\nNOTION_DATABASE_ID=\nSLACK_WEBHOOK_URL=\n\n# ─── Optional: tracing ───────────────────────────────────────\nLANGFUSE_PUBLIC_KEY=\nLANGFUSE_SECRET_KEY=\nLANGFUSE_HOST=\n\n# ─── Local config ────────────────────────────────────────────\nITERATE_MEMORY_DB=.iterate/memory.db\nITERATE_RUNS_DIR=.iterate/runs\n"}, "3d760e6738f4a7d9f5c060da1638d2b4b9c9e1a6": {"t": 1785770271.394142, "n": 2078, "h": "379125827bfbe89f", "c": "# Copy this file to .env and fill in real values. .env is gitignored.\n# See pyproject.toml for which keys are required vs optional.\n\n# ═══════════════════════════════════════════════════════════════\n# Default: LOCAL Ollama backend — $0, no API keys, fully private\n# ═══════════════════════════════════════════════════════════════\n\nITERATE_BACKEND_URL=http://localhost:11434/v1\nITERATE_BACKEND_API_KEY=ollama\nITERATE_MODEL=qwen2.5-coder:14b\nITERATE_BACKEND_TIMEOUT=120\n\n# Run `ollama pull qwen2.5-coder:14b` then `ollama serve` to start.\n# Any OpenAI-compatible endpoint works here — just change the URL + model.\n\n# ─── Required: sandbox for code execution ────────────────────\nE2B_API_KEY=\n\n# ─── Required: dataset access ────────────────────────────────\nKAGGLE_USERNAME=\nKAGGLE_KEY=\n\n# ═══════════════════════════════════════════════════════════════\n# Optional: cloud LLM backends (switch ITERATE_BACKEND_URL to use)\n# ═══════════════════════════════════════════════════════════════\n\n# Groq — free tier, fast Llama 3.3 70B\n# ITERATE_BACKEND_URL=https://api.groq.com/openai/v1\n# ITERATE_BACKEND_API_KEY=<your-groq-key>\n# ITERATE_MODEL=llama-3.3-70b-versatile\nGROQ_API_KEY=\n\n# Together AI — paid, very cheap Llama 3.3 70B\n# ITERATE_BACKEND_URL=https://api.together.xyz/v1\n# ITERATE_BACKEND_API_KEY=<your-together-key>\n# ITERATE_MODEL=meta-llama/Llama-3.3-70B-Instruct-Turbo\nTOGETHER_API_KEY=\n\n# Deepseek — paid, strong on code\n# ITERATE_BACKEND_URL=https://api.deepseek.com/v1\n# ITERATE_BACKEND_API_KEY=<your-deepseek-key>\n# ITERATE_MODEL=deepseek-chat\nDEEPSEEK_API_KEY=\n\n# OpenAI — for GPT-4/5\nOPENAI_API_KEY=\n\n# Anthropic — for Claude (uses separate non-OpenAI-compatible adapter)\nANTHROPIC_API_KEY=\n\n# ─── Optional: logging adapters ──────────────────────────────\nNOTION_API_KEY=\nNOTION_DATABASE_ID=\nSLACK_WEBHOOK_URL=\n\n# ─── Optional: tracing ───────────────────────────────────────\nLANGFUSE_PUBLIC_KEY=\nLANGFUSE_SECRET_KEY=\nLANGFUSE_HOST=\n\n# ─── Local config ────────────────────────────────────────────\nITERATE_MEMORY_DB=.iterate/memory.db\nITERATE_RUNS_DIR=.iterate/runs\n"}, "7da26a9d1cbfcb5de3aae8f46a49397e41885f5b": {"t": 1785770272.674322, "n": 2050, "h": "0173a9c9a17622e4", "c": "# Copy this file to .env and fill in real values. .env is gitignored.\n# See pyproject.toml for which keys are required vs optional.\n\n# ═══════════════════════════════════════════════════════════════\n# Default: LOCAL Ollama backend — $0, no API keys, fully private\n# ═══════════════════════════════════════════════════════════════\n\nITERATE_BACKEND_URL=http://localhost:11434/v1\nITERATE_BACKEND_API_KEY=ollama\nITERATE_MODEL=qwen2.5-coder:14b\n\n# Run `ollama pull qwen2.5-coder:14b` then `ollama serve` to start.\n# Any OpenAI-compatible endpoint works here — just change the URL + model.\n\n# ─── Required: sandbox for code execution ────────────────────\nE2B_API_KEY=\n\n# ─── Required: dataset access ────────────────────────────────\nKAGGLE_USERNAME=\nKAGGLE_KEY=\n\n# ═══════════════════════════════════════════════════════════════\n# Optional: cloud LLM backends (switch ITERATE_BACKEND_URL to use)\n# ═══════════════════════════════════════════════════════════════\n\n# Groq — free tier, fast Llama 3.3 70B\n# ITERATE_BACKEND_URL=https://api.groq.com/openai/v1\n# ITERATE_BACKEND_API_KEY=<your-groq-key>\n# ITERATE_MODEL=llama-3.3-70b-versatile\nGROQ_API_KEY=\n\n# Together AI — paid, very cheap Llama 3.3 70B\n# ITERATE_BACKEND_URL=https://api.together.xyz/v1\n# ITERATE_BACKEND_API_KEY=<your-together-key>\n# ITERATE_MODEL=meta-llama/Llama-3.3-70B-Instruct-Turbo\nTOGETHER_API_KEY=\n\n# Deepseek — paid, strong on code\n# ITERATE_BACKEND_URL=https://api.deepseek.com/v1\n# ITERATE_BACKEND_API_KEY=<your-deepseek-key>\n# ITERATE_MODEL=deepseek-chat\nDEEPSEEK_API_KEY=\n\n# OpenAI — for GPT-4/5\nOPENAI_API_KEY=\n\n# Anthropic — for Claude (uses separate non-OpenAI-compatible adapter)\nANTHROPIC_API_KEY=\n\n# ─── Optional: logging adapters ──────────────────────────────\nNOTION_API_KEY=\nNOTION_DATABASE_ID=\nSLACK_WEBHOOK_URL=\n\n# ─── Optional: tracing ───────────────────────────────────────\nLANGFUSE_PUBLIC_KEY=\nLANGFUSE_SECRET_KEY=\nLANGFUSE_HOST=\n\n# ─── Local config ────────────────────────────────────────────\nITERATE_MEMORY_DB=.iterate/memory.db\nITERATE_RUNS_DIR=.iterate/runs\n"}, "b40b6d4e1f886c7bf91f4204c10d76c03a96b3d4": {"t": 1786068048.5813062, "n": 2218, "h": "d91cc2e7f34e1122", "c": "# Copy this file to .env and fill in real values. .env is gitignored.\n# See pyproject.toml for which keys are required vs optional.\n\n# ═══════════════════════════════════════════════════════════════\n# Default: LOCAL Ollama backend — $0, no API keys, fully private\n# ═══════════════════════════════════════════════════════════════\n\nITERATE_BACKEND_URL=http://localhost:11434/v1\nITERATE_BACKEND_API_KEY=ollama\nITERATE_MODEL=qwen3:14b\nITERATE_BACKEND_TIMEOUT=120\n\n# Run `ollama pull qwen3:14b` then `ollama serve` to start.\n# (qwen3 emits structured tool calls — required for the agentic loop; the\n# qwen2.5-coder variant does not. qwen3 also has a thinking mode on by default.)\n# Any OpenAI-compatible endpoint works here — just change the URL + model.\n\n# ─── Required: sandbox for code execution ────────────────────\nE2B_API_KEY=\n\n# ─── Required: dataset access ────────────────────────────────\nKAGGLE_USERNAME=\nKAGGLE_KEY=\n\n# ═══════════════════════════════════════════════════════════════\n# Optional: cloud LLM backends (switch ITERATE_BACKEND_URL to use)\n# ═══════════════════════════════════════════════════════════════\n\n# Groq — free tier, fast Llama 3.3 70B\n# ITERATE_BACKEND_URL=https://api.groq.com/openai/v1\n# ITERATE_BACKEND_API_KEY=<your-groq-key>\n# ITERATE_MODEL=llama-3.3-70b-versatile\nGROQ_API_KEY=\n\n# Together AI — paid, very cheap Llama 3.3 70B\n# ITERATE_BACKEND_URL=https://api.together.xyz/v1\n# ITERATE_BACKEND_API_KEY=<your-together-key>\n# ITERATE_MODEL=meta-llama/Llama-3.3-70B-Instruct-Turbo\nTOGETHER_API_KEY=\n\n# Deepseek — paid, strong on code\n# ITERATE_BACKEND_URL=https://api.deepseek.com/v1\n# ITERATE_BACKEND_API_KEY=<your-deepseek-key>\n# ITERATE_MODEL=deepseek-chat\nDEEPSEEK_API_KEY=\n\n# OpenAI — for GPT-4/5\nOPENAI_API_KEY=\n\n# Anthropic — for Claude (uses separate non-OpenAI-compatible adapter)\nANTHROPIC_API_KEY=\n\n# ─── Optional: logging adapters ──────────────────────────────\nNOTION_API_KEY=\nNOTION_DATABASE_ID=\nSLACK_WEBHOOK_URL=\n\n# ─── Optional: tracing ───────────────────────────────────────\nLANGFUSE_PUBLIC_KEY=\nLANGFUSE_SECRET_KEY=\nLANGFUSE_HOST=\n\n# ─── Local config ────────────────────────────────────────────\nITERATE_MEMORY_DB=.iterate/memory.db\nITERATE_RUNS_DIR=.iterate/runs\n"}}, "src/iterate/cli.py": {"b40b6d4e1f886c7bf91f4204c10d76c03a96b3d4": {"t": 1786067378.8646042, "n": 39827, "h": "d6e061a79ca0ea80", "c": "\"\"\"CLI entry point.\n\n`iterate version` / `iterate config` / `iterate setup` / `iterate run`. The `run`\ncommand wires the agent end-to-end: load data → build target → build LLM client →\nreconstruct baseline from `--source` if given → loop via the Orchestrator → render\na summary. By default the agent WRITES model code (`--code`) and runs it locally\n(`--compute local`); `iterate setup` saves a user's preferred defaults so they\ndon't repeat flags.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nimport json\nimport logging\nimport os\nimport signal\nimport sys\nfrom datetime import UTC, datetime\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, cast\n\nimport typer\nfrom rich.console import Console\nfrom rich.logging import RichHandler\nfrom rich.table import Table\n\nfrom iterate import __version__, userconfig\nfrom iterate.config import get_settings\n\n# NOTE: the heavy stack (pandas, scikit-learn, joblib, the orchestrator/model\n# chain) is imported lazily inside `run()` — not at module load — so `iterate\n# version` / `config` / `--help` stay instant instead of paying the ~2-3s\n# sklearn+pandas import cost on every invocation.\n\nif TYPE_CHECKING:\n from iterate.core.interactive import RunController\n from iterate.core.memory import Memory\n from iterate.core.orchestrator import RunResult\n from iterate.schemas.experiment import Candidate, Experiment, ExperimentResult\n from iterate.targets.model import ModelTarget\n\napp = typer.Typer(\n name=\"iterate\",\n help=(\n \"Autonomous research-aware iteration agent for ML models and LLM prompts. \"\n \"See https://github.com/anthonyrodrigues443/iterate\"\n ),\n no_args_is_help=True,\n)\n\nconsole = Console()\n\n\n@app.callback()\ndef _root() -> None:\n \"\"\"Root callback — its presence keeps subcommands as real subcommands.\"\"\"\n\n\n@app.command()\ndef version() -> None:\n \"\"\"Print the iterate version.\"\"\"\n typer.echo(f\"iterate {__version__}\")\n\n\n@app.command()\ndef config() -> None:\n \"\"\"Show the resolved configuration (the backend api-key is masked).\"\"\"\n settings = get_settings()\n typer.echo(f\"model: {settings.iterate_model}\")\n typer.echo(f\"backend_url: {settings.iterate_backend_url}\")\n typer.echo(f\"api_key: {_mask(settings.iterate_backend_api_key)}\")\n typer.echo(f\"timeout: {settings.iterate_backend_timeout}s\")\n typer.echo(f\"ollama_host: {settings.ollama_host}\")\n typer.echo(f\"memory_db: {settings.iterate_memory_db}\")\n\n\n@app.command()\ndef setup() -> None:\n \"\"\"Save your default backend, model, keys, compute venue, and install consent.\n\n Writes ~/.config/iterate/config.toml. Override any of it per run with a flag.\n \"\"\"\n console.print(\n \"[bold]iterate setup[/bold] — your saved defaults \"\n \"(override any of these per run with a flag).\\n\"\n )\n backend = typer.prompt(\n \"LLM backend (ollama / openai / groq / together / deepseek)\", default=\"ollama\"\n ).strip()\n model = typer.prompt(\"Model name (blank = backend default)\", default=\"\", show_default=False)\n api_key = \"\"\n if backend != \"ollama\":\n api_key = typer.prompt(\n f\"API key for {backend}\", default=\"\", hide_input=True, show_default=False\n ).strip()\n\n compute = typer.prompt(\"Run generated code on (local / e2b)\", default=\"local\").strip().lower()\n e2b_api_key = \"\"\n install = False\n if compute == \"e2b\":\n e2b_api_key = typer.prompt(\n \"E2B API key\", default=\"\", hide_input=True, show_default=False\n ).strip()\n else:\n console.print(\n \"[dim]On local, generated code runs on THIS machine with your \"\n \"permissions.[/dim]\"\n )\n install = typer.confirm(\n \"May iterate pip-install packages your generated code imports \"\n \"(into iterate's environment)?\",\n default=False,\n )\n\n values: dict[str, object] = {\"backend\": backend, \"compute\": compute, \"install\": install}\n if model.strip():\n values[\"model\"] = model.strip()\n if api_key:\n values[\"api_key\"] = api_key\n if e2b_api_key:\n values[\"e2b_api_key\"] = e2b_api_key\n path = userconfig.save_user_config(values)\n console.print(f\"\\n[green]saved[/green] → {path}\")\n\n\n@app.command()\ndef run(\n data: Path = typer.Option(\n ..., \"--data\", help=\"Path to the CSV dataset.\", exists=True, dir_okay=False\n ),\n target: str = typer.Option(..., \"--target\", help=\"Name of the target column.\"),\n metric: str = typer.Option(\n ..., \"--metric\", help=\"Primary metric: f1 | accuracy | roc_auc | log_loss | rmse | r2 | …\"\n ),\n average: str | None = typer.Option(\n None,\n \"--average\",\n help=\"Averaging for f1/precision/recall: binary | micro | macro | weighted. \"\n \"Default adapts to the target (binary on two classes, macro otherwise).\",\n ),\n baseline: float | None = typer.Option(\n None, \"--baseline\", help=\"Your reported baseline score (sanity check; requires --source).\"\n ),\n source: Path | None = typer.Option(\n None,\n \"--source\",\n help=\"md / txt / .py / .ipynb describing the user's approach (read as TEXT, never executed).\",\n exists=True,\n dir_okay=False,\n ),\n backend: str | None = typer.Option(\n None, \"--backend\", help=\"ollama | openai-compatible (or aliases: groq, together, …)\"\n ),\n model: str | None = typer.Option(None, \"--model\", help=\"Override the backend's default model.\"),\n base_url: str | None = typer.Option(\n None, \"--base-url\", help=\"Override the backend's base URL.\"\n ),\n api_key: str | None = typer.Option(None, \"--api-key\", help=\"Override the backend's API key.\"),\n think: bool = typer.Option(\n False,\n \"--think/--no-think\",\n help=\"Enable thinking mode for the CODING agent (ollama only; slower per call). \"\n \"The supervisor always runs with thinking off — its reply must be a single \"\n \"tool call, and a long thinking trace crowds that call out.\",\n ),\n code: bool = typer.Option(\n True,\n \"--code/--spec\",\n help=\"Agent WRITES model code (default), or picks an allow-listed estimator (--spec).\",\n ),\n compute: str | None = typer.Option(\n None, \"--compute\", help=\"Where generated code runs: local (default) | e2b.\"\n ),\n install: bool | None = typer.Option(\n None,\n \"--install/--no-install\",\n help=\"Let iterate install packages your code imports (local only; e2b always installs).\",\n ),\n max_iterations: int = typer.Option(\n 10, \"--max-iterations\", min=1, help=\"Hard cap on experiments.\"\n ),\n patience: int = typer.Option(\n 3, \"--patience\", min=1, help=\"Stop after N consecutive non-improvements.\"\n ),\n until: str | None = typer.Option(\n None, \"--until\", help='Wall-clock deadline, e.g. \"30m\" or \"2h\".'\n ),\n fresh: bool = typer.Option(\n False,\n \"--fresh\",\n help=\"Archive the existing memory db and start a new chapter with the factory default baseline.\",\n ),\n memory_path: Path | None = typer.Option(None, \"--memory\", help=\"Override the memory db path.\"),\n output: Path | None = typer.Option(\n None,\n \"--output\",\n help=\"Where to save the best model. Default: .iterate/runs/<run_id>/best_model.joblib\",\n ),\n notebooks: str = typer.Option(\n \"best\",\n \"--notebooks\",\n help=(\n \"Runnable .ipynb deliverable: best (winner only) | all (one per experiment) | none. \"\n \"Saved under .iterate/runs/<run_id>/ (best.ipynb; all → notebooks/).\"\n ),\n ),\n plain: bool = typer.Option(\n False,\n \"--plain\",\n help=(\n \"Plain terminal output instead of the interactive UI (log pane + input box). \"\n \"Chat still works on a terminal: type a line and press Enter. Non-tty runs \"\n \"are always plain and non-interactive.\"\n ),\n ),\n) -> None:\n \"\"\"Run the agent on a tabular dataset.\"\"\"\n # Heavy imports live here, not at module top, so `iterate version`/`--help`\n # don't pay the pandas + scikit-learn import cost.\n from iterate.adapters.compute.kernel import E2BKernel, LocalKernel, StatefulKernel\n from iterate.adapters.compute.local import LocalExecutor\n from iterate.adapters.data.tabular import load_csv\n from iterate.core.agent_loop import run_supervised\n from iterate.core.coder import CodingAgent\n from iterate.core.memory import SqliteMemory\n from iterate.core.orchestrator import Orchestrator\n from iterate.core.proposer import Proposer, summarize_dataset\n from iterate.core.reconstructor import Reconstructor\n from iterate.core.scoring import AVERAGES, CLASSIFICATION_METRICS, REGRESSION_METRICS\n from iterate.core.scoring import direction as metric_direction\n from iterate.core.summarizer import Summarizer\n from iterate.core.supervisor import Supervisor\n from iterate.core.terminator import default_terminator\n from iterate.llm.factory import build_client\n from iterate.targets.model import ModelTarget\n\n # ─── First run with no saved config? Offer the setup wizard. ───────────\n if not userconfig.exists() and sys.stdin.isatty():\n console.print(\"[dim]No saved config found — let's set your defaults once.[/dim]\\n\")\n setup()\n console.print()\n cfg = userconfig.load_user_config()\n\n # ─── Resolve: explicit flag > saved config > built-in default ──────────\n backend = backend or cfg.get(\"backend\") or \"ollama\"\n model = model or cfg.get(\"model\")\n compute = (compute or cfg.get(\"compute\") or \"local\").lower()\n install = install if install is not None else bool(cfg.get(\"install\", False))\n if compute not in (\"local\", \"e2b\"):\n raise typer.BadParameter(f\"--compute must be 'local' or 'e2b', got {compute!r}\")\n notebooks = notebooks.lower()\n if notebooks not in (\"best\", \"all\", \"none\"):\n raise typer.BadParameter(f\"--notebooks must be best | all | none, got {notebooks!r}\")\n\n # ─── Validate ──────────────────────────────────────────────────────────\n if baseline is not None and source is None:\n raise typer.BadParameter(\"--baseline requires --source\")\n metric = metric.lower()\n if metric not in CLASSIFICATION_METRICS and metric not in REGRESSION_METRICS:\n raise typer.BadParameter(\n f\"unknown metric {metric!r}; expected one of \"\n f\"{sorted(CLASSIFICATION_METRICS | REGRESSION_METRICS)}\"\n )\n if average is not None:\n average = average.lower()\n if average not in AVERAGES:\n raise typer.BadParameter(f\"unknown average {average!r}; expected one of {list(AVERAGES)}\")\n\n settings = get_settings()\n resolved_memory_path = memory_path or Path(settings.iterate_memory_db)\n\n # ─── New chapter? Archive the existing db. ─────────────────────────────\n # Any of --fresh, --source, --baseline+--source means \"new chapter.\"\n starting_new_chapter = fresh or source is not None\n if starting_new_chapter:\n archived = _archive_memory_db(resolved_memory_path)\n if archived is not None:\n console.print(\n f\"[dim]memory: archived [/dim]{resolved_memory_path}[dim] → \"\n f\"[/dim]{archived.name}[dim]; starting fresh[/dim]\"\n )\n\n # ─── Cloud backend? API key required. ──────────────────────────────────\n if backend != \"ollama\":\n resolved_key = api_key or cfg.get(\"api_key\") or _resolved_api_key_from_env(settings, backend)\n if not resolved_key:\n raise typer.BadParameter(\n f\"backend {backend!r} requires --api-key or a corresponding env var \"\n f\"(ITERATE_BACKEND_API_KEY / OPENAI_API_KEY / GROQ_API_KEY / …)\"\n )\n api_key = resolved_key\n\n # ─── e2b compute? API key required. ────────────────────────────────────\n e2b_api_key = cfg.get(\"e2b_api_key\") or settings.e2b_api_key\n if compute == \"e2b\" and not e2b_api_key:\n raise typer.BadParameter(\n \"--compute e2b needs an E2B API key — run 'iterate setup' or set E2B_API_KEY \"\n \"(get a free key at e2b.dev).\"\n )\n\n # ─── Configure logging so per-iteration messages stream live. ──────────\n # The TUI owns logging for its lifetime (a stdout handler would vanish into\n # the alternate screen); it installs its own pane handler in run_in_tui.\n # Interactivity needs FOREGROUND tty ownership, not just a tty: a\n # backgrounded job (`iterate run ... &`) that reads its controlling tty gets\n # SIGTTIN and the OS suspends the whole process — the run itself.\n interactive_tty = _stdin_owns_tty()\n use_tui = code and not plain and interactive_tty and sys.stdout.isatty()\n if not use_tui:\n _configure_logging()\n\n # ─── Load data + build target ──────────────────────────────────────────\n dataset = load_csv(data, target=target)\n model_target = ModelTarget(dataset, metric=metric, average=average)\n data_summary = summarize_dataset(dataset)\n direction = metric_direction(metric)\n\n # ─── LLM clients + memory ──────────────────────────────────────────────\n # Two clients on purpose: thinking applies to the CODER only. The supervisor\n # (and every other tool-only role) must answer with a single structured tool\n # call, and a thinking trace eats the reply budget before the call is emitted —\n # observed live: every supervisor turn failed with \"no plan\" under thinking.\n client = build_client(backend, model=model, base_url=base_url, api_key=api_key)\n coder_client = (\n build_client(backend, model=model, base_url=base_url, api_key=api_key, think=True)\n if think\n else client\n )\n # NOTE: the sqlite Memory is deliberately NOT constructed here. A sqlite\n # connection can only be used on the thread that created it, and in TUI mode\n # the supervised loop runs on a worker thread — each branch below constructs\n # its Memory on the thread that will actually use it.\n\n # ─── Terminator ────────────────────────────────────────────────────────\n deadline_seconds = _parse_duration(until) if until is not None else None\n terminator = default_terminator(\n max_iterations=max_iterations, patience=patience, deadline_seconds=deadline_seconds\n )\n\n mode = \"cell-by-cell\" if code else \"spec\"\n console.print(\n f\"\\n[dim]Running on {model_target.name}; target={target!r}, metric={metric}, \"\n f\"mode={mode}, compute={compute}[/dim]\\n\"\n )\n\n if code:\n # ─── Cell-by-cell: Supervisor briefs → Coding agent runs a kernel session ──\n # Each session gets a fixed kernel-execution budget (no cell cap; LLM latency\n # is not charged). --until bounds the WHOLE run via the terminator above —\n # it is not a single experiment's budget.\n # Supervisor and Summarizer are tool-only structured-output roles, so they use\n # the no-think client even when --think is set (thinking crowds out the call).\n supervisor = Supervisor(client, metric=metric)\n summarizer = Summarizer(client, metric=metric)\n # Local models live in a small context window (num_ctx); cap the coder's\n # prompt well under it so the system prompt is never what truncates.\n context_budget = 48_000 if backend == \"ollama\" else 400_000\n\n # ─── Interactive chat (v0.3): type anything, anytime ──────────────────\n # Default face on a terminal: the TUI (scrollable log pane + pinned input\n # box), where the Input widget feeds the controller. With --plain (or a\n # tty stdin but piped stdout), a daemon thread reads stdin lines instead.\n # Control words act immediately; other messages queue with a timing-only\n # ack and are interpreted at the next safe boundary (the loop binds the\n # interpreter). Non-tty (scripts/CI) keeps exactly the old behavior.\n controller: RunController | None = None\n if interactive_tty:\n from iterate.core.interactive import RunController as _RunController\n\n controller = _RunController()\n if not use_tui:\n import threading\n\n # Replies carry user/LLM text with brackets (code, lists) — print\n # them literally, never through rich markup (which would eat\n # \"df[cols]\" or raise on a stray closing tag).\n controller.bind_reply(\n lambda text: console.print(f\">> {text}\", style=\"cyan\", markup=False)\n )\n # If the run is later backgrounded (Ctrl-Z + bg), the listener's\n # tty read must fail with EIO (ending the thread quietly) instead\n # of SIGTTIN-stopping the whole process mid-run.\n with contextlib.suppress(ValueError, OSError):\n signal.signal(signal.SIGTTIN, signal.SIG_IGN)\n\n def _listen(ctrl: RunController = controller) -> None:\n try:\n for line in sys.stdin:\n ctrl.submit_line(line)\n except Exception: # a dying listener must never touch the run\n pass\n\n threading.Thread(target=_listen, daemon=True, name=\"iterate-chat\").start()\n console.print(\n \"[dim]interactive: type anything, anytime — pause / resume work as plain \"\n \"words, stop (or /stop) quits now; messages reach the agent at the next \"\n \"safe point[/dim]\"\n )\n\n def _stop_now(ctrl: RunController = controller) -> None:\n # A typed stop quits NOW but still shows the scoreboard:\n # render whatever the loop finished, then leave. os._exit,\n # deliberately — the main thread is blocked inside the loop\n # and cannot be joined; notebooks + memory are already on\n # disk, and orphaned kernels self-reap.\n snap = ctrl.snapshot\n result = snap() if callable(snap) else None\n if result is not None:\n with contextlib.suppress(Exception):\n _render_summary(cast(\"RunResult\", result), metric)\n console.print(\n \">> stopped — everything already saved is under .iterate/\",\n style=\"cyan\", markup=False,\n )\n os._exit(0)\n\n controller.on_stop_now = _stop_now\n\n _sigint_presses: list[int] = []\n\n def _sigint(signum: int, frame: object) -> None:\n # First Ctrl-C: the existing graceful interrupt (floor banked,\n # memory finalized). Second Ctrl-C: quit immediately.\n if _sigint_presses:\n _stop_now()\n _sigint_presses.append(1)\n raise KeyboardInterrupt\n\n with contextlib.suppress(ValueError, OSError):\n signal.signal(signal.SIGINT, _sigint)\n\n def make_coder() -> CodingAgent:\n kernel: StatefulKernel = (\n E2BKernel(api_key=e2b_api_key) if compute == \"e2b\" else LocalKernel()\n )\n return CodingAgent(\n coder_client, kernel, metric=metric, average=average,\n install=(install or compute == \"e2b\"),\n context_budget_chars=context_budget,\n controller=controller,\n )\n\n def on_experiment(\n *, experiment: Experiment, baseline: ExperimentResult, is_best: bool, run_id: str\n ) -> None:\n # Incremental deliverable: each finished iteration's notebook is saved\n # NOW (and best.ipynb tracks the best-so-far), so a crash or Ctrl-C\n # mid-run still leaves everything finished on disk.\n if notebooks == \"none\":\n return\n _write_experiment_notebook(\n experiment, baseline=baseline, is_best=is_best,\n run_dir=Path(settings.iterate_runs_dir) / run_id, mode=notebooks,\n data_path=str(data), target=target, metric=metric,\n )\n\n def _run_loop() -> RunResult:\n # The Memory is born HERE so its sqlite connection lives on the\n # thread that runs the loop (the TUI's worker, or the main thread\n # in plain mode) — sqlite objects are single-thread by design.\n loop_memory: Memory = SqliteMemory(resolved_memory_path)\n return run_supervised(\n target=model_target, dataset=dataset, supervisor=supervisor,\n make_coder=make_coder, terminator=terminator, memory=loop_memory,\n data_summary=data_summary, summarizer=summarizer,\n on_experiment=on_experiment, controller=controller,\n )\n\n if use_tui and controller is not None:\n from iterate.ui.tui import run_in_tui\n\n result = run_in_tui(\n _run_loop, controller,\n title=(\n f\"iterate · {model_target.name} · target={target} · {metric} · \"\n f\"{mode} · {compute} — type below; / for commands\"\n ),\n configure_logging=_configure_logging,\n )\n if result is None:\n # Hard stop before the loop recorded anything: nothing to show.\n console.print(\"stopped — the run ended before anything finished.\")\n raise typer.Exit(0)\n else:\n result = _run_loop()\n else:\n # ─── Spec path (allow-listed estimators) + the baseline precedence ─────\n memory: Memory = SqliteMemory(resolved_memory_path) # main thread creates + uses\n baseline_candidate: Candidate | None\n baseline_model: str\n if source is not None:\n baseline_candidate = Reconstructor(client).reconstruct(\n data_summary=data_summary, source_text=_read_source(source),\n metric=metric, direction=direction,\n )\n baseline_model = _candidate_model(baseline_candidate)\n console.print(\n f\"[bold]baseline from source[/bold] ({source.name}): \"\n f\"{baseline_candidate.description}\"\n )\n elif not fresh and (prior := _prior_best(memory, model_target.name, direction)) is not None:\n assert prior.result is not None\n assert prior.result.metrics is not None\n baseline_candidate = prior.candidate\n baseline_model = _candidate_model(prior.candidate)\n console.print(\n f\"[bold]baseline from memory[/bold]: {prior.candidate.description} \"\n f\"({metric}={prior.result.metrics.primary_value:.4f}); re-measuring\"\n )\n else:\n baseline_candidate = None\n baseline_model = _default_baseline_model(metric)\n\n orchestrator = Orchestrator(\n model_target, Proposer(client), LocalExecutor(), terminator, memory,\n data_summary=data_summary, baseline_model=baseline_model,\n baseline_candidate=baseline_candidate,\n )\n result = orchestrator.run()\n\n # ─── Sanity check on user-reported baseline ────────────────────────────\n if baseline is not None and result.baseline.metrics is not None:\n _check_baseline_divergence(\n reported=baseline, measured=result.baseline.metrics.primary_value\n )\n\n # ─── Save the winning model so the user can actually use it ────────────\n run_dir = Path(settings.iterate_runs_dir) / (result.run_id or \"run\")\n if result.best is not None and result.best.result is not None:\n out_path = output or (run_dir / \"best_model.joblib\")\n _save_best_model(model_target, result, metric, out_path)\n\n # ─── Notebook deliverable (full record is already in Memory) ───────────\n if notebooks != \"none\":\n _write_notebooks(\n result, mode=notebooks, run_dir=run_dir, data_path=str(data), target=target, metric=metric\n )\n\n # ─── Summary ───────────────────────────────────────────────────────────\n _render_summary(result, metric)\n\n\n# ── helpers ──────────────────────────────────────────────────────────────\n\n\ndef _mask(secret: str) -> str:\n if len(secret) <= 4:\n return \"****\"\n return f\"{secret[:2]}…{secret[-2:]}\"\n\n\ndef _stdin_owns_tty() -> bool:\n \"\"\"True only when stdin is a tty AND this process owns it (foreground job).\n\n `isatty()` alone is not enough: a backgrounded job still has a tty stdin,\n and reading it raises SIGTTIN, which by default STOPS the entire process —\n the run would silently freeze until foregrounded.\"\"\"\n try:\n return sys.stdin.isatty() and os.tcgetpgrp(sys.stdin.fileno()) == os.getpgrp()\n except (OSError, ValueError, AttributeError):\n return False\n\n\ndef _configure_logging(handler: logging.Handler | None = None) -> None:\n \"\"\"Stream orchestrator INFO logs to the console via rich, once — or into\n ``handler`` (the TUI's log pane) when one is given.\"\"\"\n root = logging.getLogger()\n if handler is not None:\n root.addHandler(handler)\n if root.level == logging.NOTSET or root.level > logging.INFO:\n root.setLevel(logging.INFO)\n elif root.handlers:\n return # something already configured (likely a test)\n else:\n logging.basicConfig(\n level=logging.INFO,\n format=\"%(message)s\",\n datefmt=\"[%X]\",\n handlers=[RichHandler(console=console, show_path=False, markup=False)],\n )\n # Third-party HTTP chatter (one line per LLM call, plus e2b keepalive/execute\n # pairs around every cell) drowns the run's own progress; keep it debug-only.\n for name in (\"httpx\", \"httpcore\", \"e2b\"):\n logging.getLogger(name).setLevel(logging.WARNING)\n\n\ndef _read_source(path: Path) -> str:\n \"\"\"Read a source document as text; for `.ipynb`, walk cells.\"\"\"\n raw = path.read_text(encoding=\"utf-8\")\n if path.suffix.lower() != \".ipynb\":\n return raw\n try:\n nb = json.loads(raw)\n except json.JSONDecodeError as exc:\n raise typer.BadParameter(f\"could not parse {path.name} as a notebook: {exc}\") from exc\n parts: list[str] = []\n for cell in nb.get(\"cells\", []):\n src = cell.get(\"source\", \"\")\n text = \"\".join(src) if isinstance(src, list) else str(src)\n if not text.strip():\n continue\n if cell.get(\"cell_type\") == \"code\":\n parts.append(f\"```python\\n{text}\\n```\")\n else:\n parts.append(text)\n return \"\\n\\n\".join(parts)\n\n\ndef _parse_duration(text: str) -> float:\n \"\"\"Parse '30s' / '15m' / '2h' / '1h30m' to seconds.\"\"\"\n text = text.strip().lower()\n total = 0.0\n number = \"\"\n for ch in text:\n if ch.isdigit() or ch == \".\":\n number += ch\n continue\n if not number:\n raise typer.BadParameter(f\"invalid duration {text!r}\")\n value = float(number)\n if ch == \"h\":\n total += value * 3600\n elif ch == \"m\":\n total += value * 60\n elif ch == \"s\":\n total += value\n else:\n raise typer.BadParameter(f\"invalid duration unit {ch!r} in {text!r}\")\n number = \"\"\n if number: # trailing bare number — assume seconds\n total += float(number)\n if total <= 0:\n raise typer.BadParameter(f\"duration must be > 0 ({text!r} parsed as 0)\")\n return total\n\n\ndef _archive_memory_db(path: Path) -> Path | None:\n \"\"\"Rename an existing memory db to a timestamped `.bak`. Returns the new path.\"\"\"\n if not path.exists():\n return None\n stamp = datetime.now(UTC).strftime(\"%Y%m%d-%H%M%S\")\n archived = path.with_name(f\"{path.stem}.{stamp}.bak{path.suffix}\")\n path.rename(archived)\n return archived\n\n\ndef _resolved_api_key_from_env(settings: object, backend: str) -> str | None:\n \"\"\"Pull an api key from settings env-overrides for a cloud backend.\n\n The user might have set ITERATE_BACKEND_API_KEY (generic), or a vendor-specific\n one like OPENAI_API_KEY / GROQ_API_KEY.\n \"\"\"\n candidates = {\n \"openai-compatible\": (\"openai_api_key\", \"iterate_backend_api_key\"),\n \"openai\": (\"openai_api_key\", \"iterate_backend_api_key\"),\n \"groq\": (\"groq_api_key\", \"iterate_backend_api_key\"),\n \"together\": (\"together_api_key\", \"iterate_backend_api_key\"),\n \"deepseek\": (\"deepseek_api_key\", \"iterate_backend_api_key\"),\n \"vllm\": (\"iterate_backend_api_key\",),\n }\n for attr in candidates.get(backend, ()):\n value = getattr(settings, attr, None)\n if value and value != \"ollama\": # \"ollama\" is the placeholder default\n return str(value)\n return None\n\n\ndef _candidate_model(candidate: Candidate) -> str:\n \"\"\"A display string for a candidate's approach: the estimator name (spec path)\n or the description (code path, where there is no single estimator name).\"\"\"\n model = candidate.changes.get(\"model\")\n if isinstance(model, str) and model.strip():\n return model.strip()\n return candidate.description or \"generated code\"\n\n\ndef _default_baseline_model(metric: str) -> str:\n \"\"\"Factory default per task. Mirrors the private mapping in adapters.models.registry.\"\"\"\n from iterate.core.scoring import task_for_metric\n\n if task_for_metric(metric) == \"classification\":\n return \"sklearn.ensemble.HistGradientBoostingClassifier\"\n return \"sklearn.ensemble.HistGradientBoostingRegressor\"\n\n\ndef _prior_best(memory: Memory, target_name: str, direction: str) -> Experiment | None:\n \"\"\"Return the best succeeded experiment for this target across memory, or None.\"\"\"\n history = memory.history(target_name)\n succeeded = [e for e in history if e.result and e.result.succeeded and e.result.metrics]\n if not succeeded:\n return None\n def _score(experiment: Experiment) -> float:\n assert experiment.result is not None\n assert experiment.result.metrics is not None\n return experiment.result.metrics.primary_value\n\n return min(succeeded, key=_score) if direction == \"minimize\" else max(succeeded, key=_score)\n\n\ndef _check_baseline_divergence(\n *, reported: float, measured: float, threshold: float = 0.10\n) -> None:\n if reported == 0:\n return\n divergence = abs(measured - reported) / abs(reported)\n if divergence > threshold:\n console.print(\n f\"\\n[yellow]warning:[/yellow] your reported baseline {reported:.4f} \"\n f\"differs from our re-measurement {measured:.4f} ({divergence:.1%} divergence). \"\n f\"Likely a feature/eval mismatch.\"\n )\n\n\ndef _save_best_model(\n target: ModelTarget, result: RunResult, metric: str, path: Path\n) -> None:\n \"\"\"Persist the winning approach + a sidecar best.json.\n\n Spec winner → refit and pickle the fitted pipeline (joblib). Code winner →\n save the `train_and_predict` source (a code-gen winner returns predictions, not\n a pickled model — by design; see LIMITATIONS.md). The notebook deliverable\n (Day 6) turns that source into a runnable artifact.\n \"\"\"\n best = result.best\n assert best is not None\n best_result = best.result\n assert best_result is not None\n spec = best.candidate.changes\n score = best_result.metrics.primary_value if best_result.metrics else None\n path.parent.mkdir(parents=True, exist_ok=True) # the run dir (the code path skips save_model)\n\n if spec.get(\"code\"):\n # Code winner: the runnable artifact is best.ipynb (written by _write_notebooks);\n # a code-gen winner returns predictions, not a pickle — by design.\n artifact = None\n load_hint = \"the winning approach is in best.ipynb (runnable)\"\n else:\n target.save_model(spec, path)\n artifact = path\n load_hint = f\"load it: joblib.load({str(path)!r}).predict(X)\"\n\n sidecar = path.with_name(\"best.json\")\n sidecar.write_text(\n json.dumps(\n {\n \"run_id\": result.run_id,\n \"model\": spec.get(\"model\"),\n \"params\": spec.get(\"params\", {}),\n \"code\": spec.get(\"code\"),\n \"description\": best.candidate.description,\n \"rationale\": best.candidate.rationale,\n \"metric\": metric,\n \"score\": score,\n \"artifact_path\": str(artifact) if artifact else None,\n },\n indent=2,\n ),\n encoding=\"utf-8\",\n )\n if artifact is not None:\n console.print(f\"\\n[bold]saved best model[/bold] → {artifact}\\n[dim]{load_hint}[/dim]\")\n else:\n console.print(f\"\\n[dim]{load_hint}[/dim]\")\n\n\ndef _render_experiment(\n exp: Experiment,\n *,\n is_best: bool,\n baseline_score: float | None,\n metric: str,\n data_path: str,\n target: str,\n leaderboard: list[Experiment] | None = None,\n) -> Any:\n \"\"\"Render ONE experiment to a notebook node — shared by the incremental\n per-iteration save and the end-of-run write, so both produce identical files.\n Cell-by-cell experiments carry their session (\"cells\"); render the real\n session. Spec / one-shot experiments render through the contract.\"\"\"\n from iterate.deliver.notebook import build_notebook, build_session_notebook\n\n cells = exp.candidate.changes.get(\"cells\")\n if isinstance(cells, list):\n score = exp.result.metrics.primary_value if exp.result and exp.result.metrics else None\n title = (\"best: \" if is_best else \"\") + exp.candidate.description\n if exp.candidate.changes.get(\"duplicate_submission\"):\n note = (\n \"this submission is byte-identical to an earlier experiment's — \"\n \"the score adds no new information beyond it\"\n )\n elif exp.candidate.changes.get(\"lever_unmeasured\"):\n note = (\n \"the briefed change never executed successfully — \"\n \"the score belongs to the carried pipeline, not the briefed idea\"\n )\n else:\n note = None\n return build_session_notebook(\n cells, title=title, metric=metric, score=score, baseline_score=baseline_score,\n hypothesis=exp.hypothesis, findings=exp.digest, honesty_note=note,\n )\n return build_notebook(\n exp, data_path=data_path, target=target, metric=metric,\n baseline_score=baseline_score, is_best=is_best, leaderboard=leaderboard,\n )\n\n\ndef _write_experiment_notebook(\n exp: Experiment,\n *,\n baseline: ExperimentResult,\n is_best: bool,\n run_dir: Path,\n mode: str,\n data_path: str,\n target: str,\n metric: str,\n) -> None:\n \"\"\"Save one finished iteration's notebook the moment it completes (and keep\n best.ipynb pointing at the best-so-far), so a crash or Ctrl-C mid-run still\n leaves every finished deliverable on disk. The end-of-run `_write_notebooks`\n rewrite is idempotent on top of these.\"\"\"\n from iterate.deliver.notebook import save_notebook, slug\n\n baseline_score = baseline.metrics.primary_value if baseline.metrics is not None else None\n if mode == \"all\":\n name = f\"iter_{exp.iteration:02d}_{slug(exp.candidate.description)}.ipynb\"\n save_notebook(\n _render_experiment(\n exp, is_best=False, baseline_score=baseline_score,\n metric=metric, data_path=data_path, target=target,\n ),\n run_dir / \"notebooks\" / name,\n )\n if is_best and exp.result is not None and exp.result.succeeded:\n save_notebook(\n _render_experiment(\n exp, is_best=True, baseline_score=baseline_score,\n metric=metric, data_path=data_path, target=target,\n ),\n run_dir / \"best.ipynb\",\n )\n\n\ndef _write_notebooks(\n result: RunResult,\n *,\n mode: str,\n run_dir: Path,\n data_path: str,\n target: str,\n metric: str,\n) -> None:\n \"\"\"Render the run as runnable notebooks: the winner (`best`) or one per\n experiment (`all`). The full record is already in Memory; this just renders it.\"\"\"\n from iterate.deliver.notebook import save_notebook, slug\n\n baseline_score = (\n result.baseline.metrics.primary_value if result.baseline.metrics is not None else None\n )\n\n written: list[Path] = []\n if mode == \"all\":\n for exp in result.history:\n is_best = result.best is not None and exp.id == result.best.id\n name = f\"iter_{exp.iteration:02d}_{slug(exp.candidate.description)}.ipynb\"\n written.append(\n save_notebook(\n _render_experiment(\n exp, is_best=is_best, baseline_score=baseline_score,\n metric=metric, data_path=data_path, target=target,\n ),\n run_dir / \"notebooks\" / name,\n )\n )\n\n if result.best is not None:\n written.append(\n save_notebook(\n _render_experiment(\n result.best, is_best=True, baseline_score=baseline_score,\n metric=metric, data_path=data_path, target=target,\n leaderboard=result.history,\n ),\n run_dir / \"best.ipynb\",\n )\n )\n\n if not written:\n return\n if mode == \"all\":\n n_journey = len([p for p in written if p.parent.name == \"notebooks\"])\n console.print(f\"\\n[bold]notebooks[/bold] → {run_dir / 'notebooks'}/ ({n_journey} files)\")\n if result.best is not None:\n console.print(f\"[bold]best notebook[/bold] → {run_dir / 'best.ipynb'}\")\n\n\ndef _render_summary(result: RunResult, metric: str) -> None:\n baseline_score = (\n result.baseline.metrics.primary_value if result.baseline.metrics is not None else None\n )\n best_id = result.best.id if result.best is not None else None\n\n table = Table(title=\"Run summary\", show_lines=False)\n table.add_column(\"iter\", justify=\"right\")\n table.add_column(\"model\")\n table.add_column(metric, justify=\"right\")\n table.add_column(\"Δ vs baseline\", justify=\"right\")\n\n if baseline_score is not None:\n table.add_row(\"base\", \"baseline\", f\"{baseline_score:.4f}\", \"—\")\n\n for exp in result.history:\n model_name = _candidate_model(exp.candidate)\n if exp.id == best_id:\n model_name += \" [bold green]← best[/bold green]\"\n if exp.result is None or exp.result.metrics is None:\n err = exp.result.error if exp.result else \"no result\"\n table.add_row(str(exp.iteration), model_name, \"[red]FAILED[/red]\", str(err)[:40])\n continue\n score = exp.result.metrics.primary_value\n delta = (score - baseline_score) if baseline_score is not None else 0.0\n arrow = \"↑\" if delta > 0 else (\"↓\" if delta < 0 else \"—\")\n table.add_row(str(exp.iteration), model_name, f\"{score:.4f}\", f\"{arrow} {delta:+.4f}\")\n\n console.print()\n console.print(table)\n console.print(f\"\\n[bold]stopped:[/bold] {result.stopped_because}\")\n if result.best is not None and result.best.result and result.best.result.metrics:\n improvement = (\n result.best.result.metrics.primary_value - baseline_score\n if baseline_score is not None\n else 0.0\n )\n console.print(\n f\"[bold]best:[/bold] {result.best.candidate.description} \"\n f\"({metric}={result.best.result.metrics.primary_value:.4f}, \"\n f\"{improvement:+.4f} vs baseline)\"\n )\n else:\n console.print(\"[dim]no candidate beat the baseline.[/dim]\")\n\n\nif __name__ == \"__main__\":\n app()\n"}}}} |