Skip to main content

Quick Start

From zero to a graded eval run in a few minutes. For the full walkthrough with a real agent, see Testing Your Agent.

Fastest path: let your coding agent set it up

If you use Claude Code, Cursor, or Codex, you don't have to wire any of this up by hand. Run the bootstrapper once:

npx ashr-labs

That drops a setup-ashr skill into your agent's skill directories (.claude/skills/, .cursor/skills/, .agents/skills/). Open your coding agent and run the setup-ashr skill. It walks through onboarding for you:

  1. Detects your project — language, agent entrypoint, and agent class (by reading your code, not guessing).
  2. Validates your API key against the live API and saves it to .env.
  3. Scaffolds everything.ashr.json, a CLAUDE.md section, and test-agent + improve-agent skills for whichever agents you use.
  4. Runs one real eval so you see a graded result and a dashboard link before it's done.

From then on, run the test-agent skill (or /test-agent in Claude Code) after any change to your agent. Already have a key handy? npx ashr-labs tp_your_key stashes it in .env first.

Prefer to set it up yourself? The manual steps below do the same thing.

1. Get an API key

  1. Log in at lab.ashr.io
  2. Click API Keys in the sidebar
  3. Click Create New Key, name it, pick an expiration
  4. Copy the key (it starts with tp_) — it's shown only once

2. Initialize the client

from ashr_labs import AshrLabsClient

client = AshrLabsClient(api_key="tp_your_api_key_here")

# Or read ASHR_LABS_API_KEY (and optional ASHR_LABS_BASE_URL) from the environment:
# client = AshrLabsClient.from_env()

base_url defaults to production and your tenant_id is resolved from the key on first use, so there's nothing else to configure.

3. Bring an agent

An agent is any object with two methods: respond(message) returning {"text": ..., "tool_calls": [...]}, and reset() to clear state between scenarios.

class MyAgent:
def respond(self, message: str) -> dict:
# call your LLM, collect any tool calls it made
return {"text": "...", "tool_calls": []}

def reset(self) -> None:
self.history = []

If you're wrapping an Anthropic or OpenAI tool-calling loop, skip the class entirely. Define tools with @tool and use an adapter — it runs the tool loop and accumulates tool_calls for you:

from ashr_labs import AnthropicAgent, tool

@tool
def lookup_order(order_id: str) -> dict:
"""Look up the status of a customer order.

Args:
order_id: The order ID (e.g. ORD-12345).
"""
return shop.get_order(order_id)

agent = AnthropicAgent(model="claude-sonnet-4-6", system="You are a support agent.", tools=[lookup_order])

OpenAIAgent is the same for OpenAI, and FunctionAgent wraps any other framework — see API Reference → Agent adapters.

4. Find a dataset

datasets = client.list_datasets()
for d in datasets["datasets"]:
print(f"{d['id']}: {d['name']}")

A note on dataset IDs

The dashboard displays dataset IDs in hexadecimal — a dataset shown as #00000332 is integer 818 in the API (0x332 == 818). The SDK always takes the plain integer:

dataset_id = 0x332          # == 818, the dataset shown as #00000332
# dataset_id = int("00000332", 16) # same thing, if you're copying the string

Passing the decimal value 332 will not error — it just targets a different dataset (whatever row 332 happens to be). When in doubt, confirm the name:

print(client.get_dataset(818)["name"])

5. Run the eval

from ashr_labs import EvalRunner

runner = EvalRunner.from_dataset(client, dataset_id=818)
run = runner.run(agent) # runs your agent against every scenario

run() returns a RunBuilder with every scenario recorded. Inspect it locally before sending anything:

m = run.build()["aggregate_metrics"]
print(m["total_tests"], m["tests_passed"], m["tests_failed"])

Local metrics reflect what the builder recorded. The authoritative pass/fail verdicts come from server-side grading after you deploy (next step).

6. Deploy and wait for grading

created = run.deploy(client, dataset_id=818)    # grading runs async, server-side
print(f"Run #{created['id']} submitted")

graded = client.poll_run(created["id"]) # blocks until grading finishes (1–3 min)
m = graded["result"]["aggregate_metrics"]
print(f"Passed: {m['tests_passed']}/{m['total_tests']}")

To get a clickable link into the dashboard, build one with deeplink. Pass agent_id (see step 7) — the analysis view only opens the run drawer once an agent is selected:

print(client.deeplink(818, run_id=created["id"], agent_id=my_agent_id))

The graded["deeplink"] field is also populated, but it omits agent_id, so it lands on the agents list rather than the run. See API Reference → deeplink.

Steps 5 and 6 collapse into one call with run_and_deploy:

created = runner.run_and_deploy(agent, client, dataset_id=818)

7. Group runs under an agent (optional)

If you eval the same agent across many datasets, create an agent to keep your datasets and runs organized under one record. The agent's configuration lives in config.form_data — a saved dataset-generation preset (the agent block, tools, and context) the dashboard uses to pre-fill new requests. Resolve a stable agent ID once, then key everything off it:

# get_or_create_agent is idempotent — safe to call on every run.
agent_id = client.get_or_create_agent(
"ShopWave Support Agent",
description="Handles orders, refunds, and stock questions",
config={
"form_data": {
"agent": {
"system_prompt": "You are a support agent.",
"tools": [lookup_order.to_ashr()],
},
"context": {"domain": "ecommerce", "use_case": "Order and refund support"},
},
},
)["id"]

runner.run_and_deploy(agent, client, dataset_id=818, agent_id=agent_id)

config is an AgentConfig; form_data is the saved preset that pre-fills new requests (see API Reference → create_agent). tool_definitions/behavior_rules may also be stored as optional descriptive metadata. Datasets and runs linked via agent_id are grouped under the agent in the dashboard.

Complete example

from ashr_labs import AshrLabsClient, AnthropicAgent, EvalRunner, tool, NotFoundError

client = AshrLabsClient.from_env()

@tool
def lookup_order(order_id: str) -> dict:
"""Look up the status of a customer order."""
return shop.get_order(order_id)

agent = AnthropicAgent(model="claude-sonnet-4-6", system="You are a support agent.", tools=[lookup_order])

def main() -> None:
try:
runner = EvalRunner.from_dataset(client, dataset_id=818)
except NotFoundError:
print("Dataset not found")
return

created = runner.run_and_deploy(
agent, client, dataset_id=818,
on_scenario=lambda sid, s: print(f"Running: {s.get('title', sid)}"),
)
graded = client.poll_run(created["id"])
m = graded["result"]["aggregate_metrics"]
print(f"Passed: {m['tests_passed']}/{m['total_tests']}")

if __name__ == "__main__":
main()

Next steps