Skip to main content

LLM-powered iterative function optimization

Project description

Groundhog Researcher

The Grindhog - Just One More Iteration

They called it Groundhog Day. But what was the groundhog doing? Running experiments. Keeping notes. Getting better, one iteration at a time.

LLM-powered function optimization. Define how to score your code, and the groundhog iterates overnight. Wake up to a better solution.

Install

uv tool install groundhog-researcher

This gives you the groundhog CLI (and ghg alias).

Quick start

# See what LLM backends you have available
groundhog backends

# Scaffold a task
groundhog init my_task           # basic template
# groundhog init-llm my_task    # detailed template with full LLM guide
# groundhog init-mock my_task   # mock task, no LLM needed, for testing
# groundhog init-mnist my_task  # 5 sample MNIST digit classification, real ML task

cd my_task
uv run python task.py 10

# Check progress anytime
uv run python task.py status

Works with whatever LLM you have — Claude Code, GitHub Copilot, API keys, Ollama. auto_registry() discovers available backends automatically.

Prefer a backend

groundhog prefer copilot                              # use copilot for all tiers
groundhog prefer-tier max copilot claude-sonnet-4.6    # override one tier
groundhog prefer reset                                 # back to auto-discovery

CLI commands

groundhog init [dir]              Basic task template
groundhog init-llm [dir]          Detailed template with full LLM guide
groundhog init-mock [dir]         Mock task (no LLM needed, for testing)
groundhog init-mnist [dir]        MNIST example (real ML task)

groundhog new strategy [file]     Custom strategy template
groundhog new backend [file]      Custom backend template

groundhog backends                Show available backends and tier assignments
groundhog prefer <backend>        Prefer a backend for all tiers
groundhog prefer-tier <tier> <backend> [model]
groundhog prefer reset            Reset all preferences

groundhog --version

Write a task from scratch

One file is everything:

# /// script
# dependencies = ["groundhog-researcher", "python-dotenv"]
# ///

from dotenv import load_dotenv
load_dotenv()

from groundhog import (
    Task, Data, Context, Evaluator, EvalStage, StageResult,
    SimpleOptimizer, Improve, auto_registry,
)


class MyData(Data):
    def get_train(self):
        return {"inputs": [...], "targets": [...]}

    def get_test(self):
        return {"inputs": [...], "targets": [...]}


class MyContext(Context):
    def get_brief(self):
        return "Write a function that solves X."

    def get_extended(self):
        return "Write `solve(data)` that maximizes Y. Rules: ..."


class MyEvaluator(Evaluator):
    def evaluate(self, code_or_path, data):
        code = code_or_path if isinstance(code_or_path, str) else (code_or_path / "solution.py").read_text(encoding="utf-8")
        return StageResult(metrics={"score": 0.85, "time": 1.2})

    def get_stages(self, data):
        return [
            EvalStage("smoke", "Quick syntax check", lambda cp: ...),
            EvalStage("evaluate", "Full evaluation",
                      lambda cp: self.evaluate(cp, data)),
        ]


task = Task(data=MyData(), context=MyContext(), evaluator=MyEvaluator(), name="MyTask")

optimizer = SimpleOptimizer(task, strategy=Improve())
optimizer.toolkit.llm = auto_registry()
optimizer.run(n=100)

What happens

The optimizer works in the current directory:

my_task/
    task.py                 # your task definition + entry point
    .env                    # API keys (optional)
    learnings.md            # what the optimizer has learned
    attempts/               # every candidate
        001_none/           # first attempt (no parent)
            solution.py
            result.json
            conversation.json
        002_1/              # second attempt (parent=1)
            ...

The loop:

  1. Selects a prior (best attempt, weighted by potential)
  2. Runs a strategy (improve, explore, combine ideas)
  3. Evaluates -- raw results stored, scored via stage scorers
  4. Records the attempt in an immutable attempt tree
  5. Updates learnings
  6. Repeats

Every attempt is kept. Nothing discarded. Change what "good" means later -- the history is reinterpretable.

Multi-strategy optimizer

Run multiple strategies in a weighted rotation:

from groundhog import Improve, FreshApproach, CrossPollinate

optimizer = SimpleOptimizer(
    task,
    strategies=[
        (Improve(), 14),                          # 14 refinement steps
        (CrossPollinate(), 5),                     # 5 cross-pollination steps
        (FreshApproach(mode="different"), 1),       # 1 fresh exploration
    ],
    seed_strategy=FreshApproach(mode="blank"),      # seed with pure exploration
)

Backend tiers

auto_registry() discovers what's available and assigns 5 tiers (max, high, default, budget, cheap). Or configure manually:

from groundhog import BackendRegistry, AnthropicBackend, GeminiBackend, OpenAICompatibleBackend

optimizer.toolkit.llm = BackendRegistry(
    high=AnthropicBackend(model="claude-opus-4-6-20260205"),
    default=GeminiBackend(model="gemini-2.5-flash"),
    cheap=OpenAICompatibleBackend.ollama(model="llama3"),
)

Override individual tiers after auto-discovery:

optimizer.toolkit.llm = auto_registry()
optimizer.toolkit.llm.set("high", AnthropicBackend(model="claude-opus-4-6-20260205"))

Available backends:

Type Backends
API OpenAICompatibleBackend (.openai, .deepseek, .groq, .cerebras, .xai, .together, .fireworks, .ollama, .openrouter, ...), AnthropicBackend, GeminiBackend
CLI ClaudeCodeBackend, CopilotBackend, GeminiCLIBackend, OpenCodeBackend

Building a custom strategy

groundhog new strategy my_strategy.py    # generates a documented template

Or subclass directly:

from dataclasses import dataclass
from groundhog import Strategy, StrategyConfig, param

@dataclass
class MyConfig(StrategyConfig):
    temperature: float = param(0.7, "LLM sampling temperature")
    max_retries: int = param(3, "Retry attempts on failure")

class MyStrategy(Strategy):
    Config = MyConfig

    def __call__(self, toolkit, config=None):
        cfg = self._resolve_config(config)
        ws = toolkit.history.workspace(parent=None)
        # ... generate code, write to ws.path / "solution.py" ...
        result = toolkit.task.evaluate(ws.path, through="evaluate")
        attempt = ws.commit(result, metadata={"strategy": "mine"})
        return {"attempt": attempt.number}

Core concepts

Concept What it is
Task Your problem: data + context + evaluator
Strategy An action that moves the state forward (improve, explore, combine, analyse)
Attempt History Immutable tree of every candidate and its raw results
Scorer Per-stage callable that maps raw results to scores -- change it without re-running
Learnings What the optimizer has learned about how to optimize your task
Toolkit Capabilities available to strategies (LLMs, history, learnings, logging)

Architecture

base/           # interfaces only -- Task, Strategy, Optimizer, AttemptHistory, etc.
strategies/     # Improve, FreshApproach, CrossPollinate, Analyse
optimizers/     # SimpleOptimizer
histories/      # FolderAttemptHistory
backends/       # Gemini, Anthropic, OpenAI-compatible, Claude Code CLI, + more
learnings/      # MarkdownLearnings
acceptance/     # DefaultAcceptance
tools/          # conversation_log, cost_estimate, StrategyLog, queue
utils/          # codegen, subprocess_runner, selection
templates/      # task scaffolding templates (used by groundhog init)

base/ defines interfaces. Everything else is implementations. Build your own by subclassing the interfaces.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

groundhog_researcher-0.2.15.tar.gz (711.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

groundhog_researcher-0.2.15-py3-none-any.whl (148.9 kB view details)

Uploaded Python 3

File details

Details for the file groundhog_researcher-0.2.15.tar.gz.

File metadata

  • Download URL: groundhog_researcher-0.2.15.tar.gz
  • Upload date:
  • Size: 711.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for groundhog_researcher-0.2.15.tar.gz
Algorithm Hash digest
SHA256 f0e7869cf685138c3b43531a9d2d2be9fbfdb0307406b9a483b47a46b33cc4c2
MD5 be288ac1d4a12f8f6b228769841478d3
BLAKE2b-256 d41305210a56d391854bd6e618976fdd3c9d47c9dbd40101fb5e8260bff1d7f3

See more details on using hashes here.

Provenance

The following attestation bundles were made for groundhog_researcher-0.2.15.tar.gz:

Publisher: publish.yml on frizzerdk/groundhog-researcher

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file groundhog_researcher-0.2.15-py3-none-any.whl.

File metadata

File hashes

Hashes for groundhog_researcher-0.2.15-py3-none-any.whl
Algorithm Hash digest
SHA256 65a0d190ac030589ca6c654308a645563d0ad0957ee0ae0a5d6530240047d22f
MD5 0ca406df9e57f802b1d4a17c68b31652
BLAKE2b-256 db91499af9a10d22b181773206cbf62c0eb924b17aa4328839655ba9ecd33bc3

See more details on using hashes here.

Provenance

The following attestation bundles were made for groundhog_researcher-0.2.15-py3-none-any.whl:

Publisher: publish.yml on frizzerdk/groundhog-researcher

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page