Getting Started with MOAR
This guide walks you through running your first MOAR optimization step by step.
Step 1: Create Your Pipeline
Start with a standard DocETL pipeline. You can define it in Python or YAML.
import docetl
docetl.default_model = "gpt-4o-mini"
frame = (
docetl.read_json("data/transcripts.json")
.map(
prompt="Extract all medications mentioned in: {{ input.src }}",
output={"schema": {"medication": "list[str]"}},
)
)
datasets:
transcripts:
path: data/transcripts.json
type: file
default_model: gpt-4o-mini
operations:
- name: extract_medications
type: map
output:
schema:
medication: list[str]
prompt: |
Extract all medications mentioned in: {{ input.src }}
pipeline:
steps:
- name: medication_extraction
input: transcripts
operations:
- extract_medications
output:
type: file
path: results.json
Standard Pipeline
Your pipeline doesn't need any special configuration for MOAR. Just create a normal DocETL pipeline.
Step 2: Write an Evaluation Function
Write a Python function that scores pipeline output. MOAR calls this function for each pipeline configuration it explores.
How Evaluation Works
- Your function receives the path to the pipeline's output JSON file
- You load the results file and compute evaluation metrics
- You return a dictionary of metrics
- MOAR uses one specific key from this dictionary (specified by
metric_key) as the accuracy metric to optimize
Just define a regular Python function:
import json
def evaluate(results_path):
with open(results_path) as f:
output = json.load(f)
correct_count = 0
for result in output:
original_text = result.get("src", "").lower()
for item in result.get("medication", []):
if item.lower() in original_text:
correct_count += 1
return {
"medication_extraction_score": correct_count,
"total_extracted": len(output),
}
If you need access to the original dataset, use a two-argument signature — the dataset path is passed automatically:
def evaluate(dataset_path, results_path):
with open(results_path) as f:
output = json.load(f)
with open(dataset_path) as f:
dataset = json.load(f)
# compare output to dataset...
return {"medication_extraction_score": computed_score}
For CLI usage, create a Python file with a @register_eval decorated function:
# evaluate_medications.py
import json
from docetl.utils_evaluation import register_eval
@register_eval
def evaluate_results(dataset_file_path: str, results_file_path: str) -> dict:
with open(results_file_path) as f:
output = json.load(f)
correct_count = 0
for result in output:
original_text = result.get("src", "").lower()
for item in result.get("medication", []):
if item.lower() in original_text:
correct_count += 1
return {
"medication_extraction_score": correct_count,
"total_extracted": len(output),
}
For more details on evaluation functions, see the Evaluation Functions guide.
Step 3: Run Optimization
frame.optimize() is the single entry point. Pass your evaluation function and the metric key — everything else has smart defaults. It returns an optimized Frame you can .collect() or .write_json().
optimized = frame.optimize(
eval_fn=evaluate,
metric_key="medication_extraction_score",
)
All optional parameters:
optimized = frame.optimize(
eval_fn=evaluate,
metric_key="medication_extraction_score",
models=None, # auto-detect from API keys
agent_model=None, # auto-select best available (or set docetl.agent_model)
max_iterations=20, # search budget
save_dir=None, # defaults to temp dir
exploration_weight=1.414, # UCB exploration constant
dataset_path=None, # sample dataset for optimization
max_threads=None, # max concurrent LLM calls per run
max_concurrent_agents=3, # parallel MCTS search agents
)
Auto-Detection
Models are auto-detected from your environment API keys (OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, AZURE_API_KEY). The agent model is auto-selected as the best available model. No need to specify these unless you want to override the defaults.
Add an optimizer_config to your YAML. Only evaluation_file and metric_key are required:
optimizer_config:
evaluation_file: evaluate_medications.py
metric_key: medication_extraction_score
Then run:
docetl build pipeline.yaml
MOAR is the default optimizer -- no flag needed.
Step 4: Review Results
optimize() returns an optimized Frame. Access the full search results via .search_results:
# Run the optimized pipeline
rows = optimized.collect()
print(f"Execution cost: ${optimized.total_cost:.4f}")
# Access the MOAR search results
results = optimized.search_results
# Best pipeline by accuracy
best = results.best()
print(f"Accuracy: {best.accuracy}, Cost: ${best.cost:.4f}")
# Cheapest pipeline on the frontier
cheap = results.cheapest()
# View all frontier solutions
for plan in results.frontier:
print(f"Accuracy: {plan.accuracy}, Cost: ${plan.cost:.4f}")
# Get a DataFrame of all explored plans
print(results.to_df())
After optimization completes, check your save_dir for:
experiment_summary.json— High-level summary of the runpareto_frontier.json— List of optimal solutionsevaluation_metrics.json— Detailed evaluation resultspipeline_*.yaml— Optimized pipeline configurations
For details on interpreting results, see Understanding Results.
Next Steps
- Learn about configuration options
- See complete examples
- Read troubleshooting tips