Skip to main content

An experiment framework for easily testing multiple configurations of your code.

Project description

Spearmint 🌱

An experiment framework for testing multiple configurations of your code in parallel or sequentially. Think of it as A/B testing for your functions.

What is Spearmint?

Spearmint helps you answer questions like:

  • "Which model performs better: GPT-4 or GPT-3.5?"
  • "What temperature value gives the best results?"
  • "How does this new algorithm compare to the old one?"

Instead of manually running your code with different parameters and tracking results, Spearmint automates this process using strategies and configurations.

If you learn better by seeing code in action, check out the examples directory.

Core Concepts

1. Configurations

A configuration is a set of parameters you want to test. You can define them as:

  • Python dictionaries: {"model": "gpt-4", "temperature": 0.5}
  • YAML files: config.yaml
  • Pydantic models: Custom typed configuration classes

2. Strategies

Strategies control how your configurations are executed:

Strategy Behavior Use Case
SingleConfigStrategy Runs one config Default behavior, single execution path
ShadowStrategy Runs primary config in foreground, others in background Test alternatives without blocking main flow
MultiBranchStrategy Runs all configs in parallel, returns all results Get user feedback on multiple variants
RoundRobinStrategy Cycles through configs on each call Multi-variate testing

3. Experiments

The @experiment decorator wraps your functions to automatically inject configurations and track results.

Quick Start

Installation

pip install spearmint-framework

Basic Example

from spearmint import Spearmint
from spearmint.config import Config

# Initialize with a configuration
mint = Spearmint(configs=[{"model": "gpt-4", "temperature": 0.7}])

# Decorate your function
@mint.experiment()
def generate_text(prompt: str, config: Config) -> str:
    # config is automatically injected
    return f"Using {config['model']} at temp {config['temperature']}: {prompt}"

# Call normally - Spearmint handles the rest
result = generate_text("Hello world")
print(result)  # "Using gpt-4 at temp 0.7: Hello world"

Comparing Multiple Configurations

from spearmint import Spearmint
from spearmint.strategies import MultiBranchStrategy

mint = Spearmint(
    strategy=MultiBranchStrategy,
    configs=[
        {"model": "gpt-4o", "temperature": 0.0},
        {"model": "gpt-4o", "temperature": 0.5},
        {"model": "gpt-4o-mini", "temperature": 0.0},
    ]
)

@mint.experiment()
def generate_summary(text: str, config: dict) -> str:
    # Your implementation here
    return f"Summary using {config['model']}"

# Returns a BranchContainer with results from all configs
branches = generate_summary("Long text to summarize...")

for branch in branches:
    print(f"Config: {branch.config_id}")
    print(f"Result: {branch.output}")
    print(f"Duration: {branch.duration}s")

Advanced Features

Dynamic Value Expansion

Generate multiple configurations from any iterable:

from spearmint.config import DynamicValue

def temp_generator():
    for temp in range(0, 101, 50):
        yield temp / 100.0

configs = [{
    "model": DynamicValue(["gpt-4", "gpt-3.5-turbo"]),
    "max_tokens": DynamicValue(range(250, 501, 250)),
    "temperature": DynamicValue(temp_generator())
}]
# This creates 12 unique configurations (2 models × 2 max_tokens × 3 temperatures)

mint = Spearmint(configs=configs)

Typed Configurations with Pydantic

Use type-safe configuration models:

from pydantic import BaseModel
from spearmint import Spearmint

class LLMConfig(BaseModel):
    model: str
    temperature: float
    max_tokens: int = 1000

mint = Spearmint(configs=[
    {"model": "gpt-4", "temperature": 0.7}
])

# Bind your model to a path in the config
@mint.experiment(bindings={LLMConfig: ""})
def generate(prompt: str, config: LLMConfig) -> str:
    # config is now a typed LLMConfig object with IDE support
    return f"{config.model}: {prompt}"

Nested Configuration Binding

Access nested configuration values:

from pydantic import BaseModel

class ModelConfig(BaseModel):
    model: str
    temperature: float

configs = [{
    "llm": {
        "model_config": {
            "model": "gpt-4",
            "temperature": 0.7
        }
    }
}]

mint = Spearmint(configs=configs)

# Bind to nested path using dot notation
@mint.experiment(bindings={ModelConfig: "llm.model_config"})
def generate(prompt: str, config: ModelConfig) -> str:
    return f"{config.model}: {prompt}"

Shadow Testing in Production

Test new configurations without impacting your main code path:

from spearmint import Spearmint
from spearmint.config import Config
from spearmint.strategies import ShadowStrategy

mint = Spearmint(
    strategy=ShadowStrategy,
    configs=[
        {"model": "gpt-4"},        # Primary (index 0)
        {"model": "gpt-5-beta"},   # Shadow
    ]
)

@mint.experiment()
def api_call(query: str, config: Config) -> str:
    # Primary result returned immediately
    # Shadow runs in background for comparison
    return make_llm_call(config['model'], query)

result = api_call("What is AI?")  # Uses gpt-4, logs gpt-5-beta in background

Offline Evaluation with Datasets

Run experiments on datasets and evaluate results:

from spearmint import Spearmint
from spearmint.config import Config

mint = Spearmint(configs=[{"id": 1}])

@mint.experiment()
def process_item(input_text: str, config: Config) -> str:
    return f"{config['id']}-{input_text}"

# Run on a dataset (JSONL file)
results = mint.run(
    func=process_item,
    dataset="data/test_cases.jsonl"
)
# Each line in JSONL: {"input_text": "hello", "expected": "1-hello"}

Custom Evaluators

Evaluate experiment results with custom metrics:

def accuracy(expected: str, trace: dict) -> float:
    # Compare expected vs actual output
    actual = trace['data']['spans'][0]['outputs']['output']
    return 1.0 if expected == actual else 0.0

mint = Spearmint(
    configs=[{"id": 1}],
)

@mint.experiment(evaluators=[accuracy])
def process(input_text: str, config: dict) -> str:
    return f"{config['id']}-{input_text}"

# Evaluators run automatically after dataset processing
results = mint.run(
    func=process,
    dataset="data/test_cases.jsonl"
)

Real-World Examples

Example 1: FastAPI Experiment with Multiple Models

from fastapi import FastAPI
from spearmint import Spearmint
from spearmint.strategies import ShadowStrategy
from spearmint.config import Config, DynamicValue

app = FastAPI()

mint = Spearmint(
    strategy=ShadowStrategy,
    configs=[{
        "model": DynamicValue(["gpt-4o", "gpt-4o-mini", "gpt-5"]),
        "temperature": DynamicValue([0.0, 0.5, 1.0])
    }]
    # Creates 9 configs, first is primary, rest are shadows
)

@app.post("/summarize")
async def summarize(text: str):
    result = await _generate_summary(text)
    return {"summary": result}

@mint.experiment()
async def _generate_summary(text: str, config: Config) -> str:
    # Primary config executes and returns
    # Other 8 configs log results in background
    response = await openai_call(
        model=config['model'],
        temperature=config['temperature'],
        text=text
    )
    return response.text

Example 2: Batch Processing with Multiple Strategies

from spearmint import Spearmint
from spearmint.config import Config
from spearmint.strategies import MultiBranchStrategy

mint = Spearmint(configs=[
    {"algorithm": "v1", "threshold": 0.5},
    {"algorithm": "v2", "threshold": 0.7},
])

@mint.experiment(strategy=MultiBranchStrategy)
def process_document(doc_id: str, content: str, config: Config) -> dict:
    algo = config['algorithm']
    threshold = config['threshold']
    
    # Your processing logic
    result = run_algorithm(algo, content, threshold)
    
    return {
        "doc_id": doc_id,
        "algorithm": algo,
        "result": result
    }

# Process a dataset
results = mint.run(
    func=process_document,
    dataset="documents.jsonl"
)

Tracing and Logging

Spearmint integrates with MLflow for automatic experiment tracking:

import mlflow

# All functions decorated with @mint.experiment are logged to MLflow automatically
# using the traces API.

# Experiment runs and evaluations use the data from traces to calculate metrics.

# Access traces programmatically
traces = mlflow.search_traces()
for trace in traces:
    print(trace.to_dict())

Configuration Files

YAML Configuration

# config.yaml
model: gpt-4
temperature: 0.7
max_tokens: 1000
mint = Spearmint(configs=["config.yaml"])

Directory of Configs

# Loads all YAML files in the directory
mint = Spearmint(configs=["configs/"])

API Reference

Spearmint Class

Spearmint(
    strategy: type[Strategy] = SingleConfigStrategy,
    configs: list[dict | Config | str | Path] = None,
    bindings: dict[type[BaseModel], str] = None,
    evaluators: Sequence[Callable] = None
)

@experiment Decorator

@mint.experiment(
    strategy: type[Strategy] = None,       # Override default strategy
    configs: list = None,                  # Override default configs
    bindings: dict = None,                 # Override default binding
    evaluators: Sequence[Callable] = None  # Set custom evaluators
)

Branch Object

Returned by MultiBranchStrategy, contains execution details:

branch.config_id    # Configuration identifier
branch.config       # Configuration used
branch.output       # Function result
branch.status       # "success", "failed", "pending", "skipped"
branch.duration     # Execution time in seconds
branch.exception_info  # Error details if failed

When to Use Spearmint

Good use cases:

  • Comparing ML model outputs (different models, parameters, prompts)
  • A/B testing algorithms or business logic
  • Shadow testing new code against production
  • Running systematic experiments on datasets
  • Parameter tuning and optimization

Not ideal for:

  • Simple parameter passing (just use function arguments)
  • One-off scripts with no variants
  • Performance-critical hot paths (adds overhead)

Contributing

Contributions welcome!

License

See LICENSE file for details.

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

spearmint_framework-0.2.0.tar.gz (209.7 kB view details)

Uploaded Source

Built Distribution

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

spearmint_framework-0.2.0-py3-none-any.whl (22.9 kB view details)

Uploaded Python 3

File details

Details for the file spearmint_framework-0.2.0.tar.gz.

File metadata

  • Download URL: spearmint_framework-0.2.0.tar.gz
  • Upload date:
  • Size: 209.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.13

File hashes

Hashes for spearmint_framework-0.2.0.tar.gz
Algorithm Hash digest
SHA256 203d1eef9f0e35d47e697a1e9611cac07ca81969e9687e4a5e12fdda90eed191
MD5 bb4345ee20349726b6b9c4715357b9c1
BLAKE2b-256 1dcd57ae2d2068697521b1c8d063721e3e7e1a93a37b354f9f0a450836356cb2

See more details on using hashes here.

File details

Details for the file spearmint_framework-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for spearmint_framework-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8538423d5f5f3bfd4e5e715b3cba8bdd7517da493553bbe0e056f6be33b81c15
MD5 86f2ef0e59c21b895cef6fe0e33254ee
BLAKE2b-256 73080c8698ce9f6fa293a929acfc7038398ac7e08ca87d9da1c317940cf47868

See more details on using hashes here.

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