zapier / zapier/AutomationBench

Mapping AutomationBench's Task/AssertionRegistry/partial_credit onto EvalPort's open TestCase/Grader/Result interchange

Open
#22 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
290
Forks
45
PR merge metrics
No merged PRs in 30d

Description

Hi — I maintain EvalPort, an open (Apache 2.0) interchange spec for portable LLM eval test cases, graders, and results, with TestCase / Grader / Result / ResultSet as the core objects. I spent a while in automationbench/ before writing this, so this isn't a drive-by — happy to be told it's not a fit.

What I actually read

  • automationbench/schema/salesforce/task.py — the Task(SalesforceRecord) pydantic model (subject, status, priority, due_date, contact_id/related_to_id/assigned_to_id, is_closed, plus the who_id/what_id Salesforce-alias properties and to_display_dict()), and SalesforceRecord in schema/salesforce/base.py (id, created_date, last_modified_date, owner_id).
  • automationbench/schema/world.pyWorldState, the root object composing 40+ per-app states (salesforce, hubspot, gmail, slack, quickbooks, xero, wave, zendesk, …) plus WorldMeta (schema_version, current_time, allowed_services).
  • automationbench/rubric/registry.pyAssertionRegistry.register(assertion_type) / .check(world, assertion), the negative_assertion marker decorator, and AUTOMATIONBENCH_STRICT_ASSERTIONS strict/non-strict error handling.
  • automationbench/rubric/__init__.py — the actual partial_credit() / task_completed_correctly() scoring functions, including the "free assertion" exclusion logic (an assertion already true in the initial state earns no credit but still penalizes regression) and the excluded/scored: false escape hatches for inverse tasks.
  • automationbench/task_contract.pytask_contract_payload() / task_contract_sha256(), which fingerprint (example_id, prompt, assertions_multiset, initial_state, zapier_tools) for rollout caching.
  • The README's scoring section (partial_credit 0.0–1.0 as the dense reward signal, task_completed_correctly 0.0/1.0 as the strict official pass rate) and the 600-task public benchmark across sales/marketing/operations/support/finance/HR.

That's a real, actively maintained benchmark (commits into early August), not a toy — so this seemed worth raising rather than skipping.

Why I think it maps cleanly

AutomationBench EvalPort
A task's trigger prompt + info["initial_state"] (serialized WorldState) + info["zapier_tools"] TestCase.input + TestCase.metadata (automationbench.initial_state, automationbench.zapier_tools, automationbench.task_name)
One assertion dict, e.g. {"type": "contact_phone_equals", "contact_id": "...", "phone": "..."}, dispatched through AssertionRegistry An EvalPort Grader — most naturally type: "custom" (per the spec's "Custom grader handling" rule), with the real registered assertion_type string preserved under metadata rather than forced into one of the built-in types
partial_credit (fraction of assertions passed) GraderResult.score
task_completed_correctly (strict, only 1.0 if every assertion passes) Result.passed
task_contract_sha256() (hash of prompt + assertions + initial_state + tools) Directly relevant to the open ResultSet/repeated-attempt and suite-signing questions already being discussed in Discussion #22

The part I'd actually flag as non-trivial, not glossed over: partial_credit's free-assertion exclusion and negative_assertion handling are scoring semantics, not just a value — a naive to_openeval() that only copies passed/score per assertion would silently lose "this assertion was excluded because it was already true at t=0" and "this is a negative/anti-shotgun assertion that only counts if all positives passed." That needs to survive round-trip in GraderResult.metadata, or a from_openeval() back to AutomationBench's own scorer would compute a different number.

Sketch (illustrative, not a working PR yet)

def to_openeval(task_dict: dict, *, suite_id: str) -> dict:
    """AutomationBench task -> EvalPort TestCase."""
    info = task_dict["info"]
    return {
        "id": info["task_name"],
        "input": task_dict["prompt"],  # list[{"role", "content"}]
        "graders": [a["type"] for a in info["assertions"]],  # ids only; full specs below
        "metadata": {
            "automationbench.initial_state": info["initial_state"],
            "automationbench.zapier_tools": info["zapier_tools"],
            "automationbench.domain": info.get("domain"),
        },
    }

def assertion_to_grader(assertion: dict) -> dict:
    """One AssertionRegistry-dispatched assertion -> EvalPort Grader."""
    atype = assertion["type"]
    return {
        "id": atype,
        "type": "custom",
        "metadata": {
            "automationbench.assertion_type": atype,
            "automationbench.negative": atype in NEGATIVE_ASSERTION_TYPES,  # from AssertionRegistry.is_negative()
            "automationbench.params": {k: v for k, v in assertion.items() if k != "type"},
        },
    }

def result_to_openeval(state: dict, *, task_id: str) -> dict:
    """AutomationBench per-task eval state -> EvalPort Result."""
    return {
        "test_case_id": task_id,
        "passed": bool(state["partial_credit"] == 1.0),   # task_completed_correctly
        "grader_results": [
            {
                "grader_id": ar["type"],
                "score": 1.0 if ar["passed"] else 0.0,
                "passed": ar["passed"],
                "metadata": {"automationbench.excluded": ar["excluded"]},
            }
            for ar in state["_assertion_results"]  # populated by rubric.partial_credit()
        ],
    }

Comparable adapters already merged

Two existing adapters in adapters/ are the closest precedent, for different reasons:

  • financebench-openeval-adapter — also a real benchmark rather than a live SDK, and its README documents exactly the kind of "field that doesn't round-trip cleanly" issue I'm flagging above for the free/negative assertion semantics.
  • braintrust-openeval-adapter — closer in spirit for the scored-agent-rollout shape (a task, a set of checks, a pass/fail + partial score per run).

Ask

Would a PR adding an automationbench-openeval-adapter (same shape as the others: to_openeval() / from_openeval() for tasks, a separate result-side converter, tests run against a real subset of the public 600-task set, README) be something this repo would want linked from, or is AutomationBench deliberately scoped to stay Zapier/verifiers-only? I'd rather ask than send an unwanted PR. If useful, I can build it against the public task set already in this repo (no need to touch the private held-out set).

— Sahi, independent contributor (not affiliated with Zapier)

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by comparing the existing financebench-openeval-adapter and braintrust-openeval-adapter, then read automationbench/rubric/registry.py, automationbench/rubric/init.py, and automationbench/task_contract.py. First confirm whether this adapter belongs in EvalPort or AutomationBench; done should include task and result converters, tests using public tasks, README coverage, and preservation of free-assertion and negative-assertion semantics.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
ai, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.