Python Adapters
Adapters turn provider responses, framework runs, or telemetry into traces you can test. Choose the adapter for the data you already have; a trace only supports assertions about evidence it actually captures.
Overview
Section titled “Overview”Attest 1.1.0 ships these ten Python adapters, all importable from attest. Provider and framework dependencies are optional extras. Maturity tiers below match the release capability inventory.
| Adapter | Extra | Maturity | Capture method |
|---|---|---|---|
OpenAIAdapter | openai | Stable | trace_from_response(response, input_messages=...) |
AnthropicAdapter | anthropic | Stable | trace_from_response(response, input_messages=...) |
ManualAdapter | none | Stable | capture(builder_fn) or create_builder() |
GeminiAdapter | gemini | Beta | trace_from_response(response, input_messages=...) |
OllamaAdapter | ollama | Beta | trace_from_response(response, input_messages=...) |
LangChainAdapter | langchain | Beta | with adapter.capture() as handler, then adapter.trace |
CrewAIAdapter | crewai | Beta | with adapter.capture(), then trace_from_crew_output(output, crew); summary only |
OTelAdapter | otel | Beta | OTelAdapter.from_spans(spans) or from_otel_span_dicts(dicts) |
GoogleADKAdapter | google-adk | Experimental | await adapter.capture_async(...) or from_events(events, ...) |
LlamaIndexInstrumentationHandler | llamaindex | Experimental | with handler:, then build_trace(...) |
For example, install OpenAI support with uv add "attest-ai[openai]>=1.1.0". To run the pytest examples, also install pytest with uv add pytest, save tests in a test_*.py module, and run uv run pytest. Framework snippets assume you have already configured the framework objects they use.
LLM Providers
Section titled “LLM Providers”Provider adapters inherit BaseProviderAdapter and share one capture method:
adapter.trace_from_response( response, input_messages=None, started_at_ms=None, ended_at_ms=None, **metadata,) -> TracePass cost_usd and latency_ms as metadata for cost and latency assertions. If both caller timestamps are supplied, their difference supplies latency unless latency_ms is explicit. Model extraction is adapter-specific: a model keyword is not a universal override. Each adapter takes an optional agent_id. Provider adapters convert responses; your provider client makes the API call. The live-provider examples below require its credentials and may incur provider charges.
OpenAI
Section titled “OpenAI”Capture an OpenAI Chat Completions or Responses API object. Both use the same trace_from_response() method; the example below uses Chat Completions. Refusals are preserved in the LLM step and used as the visible message when no completion text is present. OpenAI output also includes structured, populated from the optional structured_output metadata dictionary.
from openai import OpenAI
from attest import AgentResult, OpenAIAdapter, expect
client = OpenAI()adapter = OpenAIAdapter(agent_id="assistant")
def test_openai_response(attest): messages = [{"role": "user", "content": "..."}] response = client.chat.completions.create(model="gpt-4.1", messages=messages)
trace = adapter.trace_from_response(response, input_messages=messages, cost_usd=0.002) result = AgentResult(trace=trace)
attest.evaluate(expect(result).output_contains("...").cost_under(0.05))Anthropic
Section titled “Anthropic”Capture an Anthropic Message response.
from anthropic import Anthropic
from attest import AgentResult, AnthropicAdapter, expect
client = Anthropic()adapter = AnthropicAdapter(agent_id="claude-agent")
def test_anthropic_response(attest): messages = [{"role": "user", "content": "..."}] response = client.messages.create( model="claude-sonnet-4-5", messages=messages, max_tokens=1024 )
trace = adapter.trace_from_response(response, input_messages=messages) attest.evaluate(expect(AgentResult(trace=trace)).output_contains("..."))Google Gemini
Section titled “Google Gemini”Capture a Gemini GenerateContentResponse. The deprecated input_text keyword is accepted with a DeprecationWarning; pass input_messages instead.
from google import genai
from attest import AgentResult, GeminiAdapter, expect
client = genai.Client()adapter = GeminiAdapter(agent_id="gemini-agent")
def test_gemini_response(attest): response = client.models.generate_content(model="gemini-2.0-flash", contents="...")
trace = adapter.trace_from_response( response, input_messages=[{"role": "user", "content": "..."}] ) attest.evaluate(expect(AgentResult(trace=trace)).output_contains("..."))Ollama
Section titled “Ollama”Capture an Ollama chat response from a local model.
import ollama
from attest import AgentResult, OllamaAdapter, expect
adapter = OllamaAdapter(agent_id="local-model")
def test_ollama_response(attest): messages = [{"role": "user", "content": "..."}] response = ollama.chat(model="mistral", messages=messages)
trace = adapter.trace_from_response(response, input_messages=messages, latency_ms=900) attest.evaluate(expect(AgentResult(trace=trace)).latency_under(5000))Framework Adapters
Section titled “Framework Adapters”LangChain
Section titled “LangChain”LangChainAdapter.capture() yields a LangChainCallbackHandler. Pass it to LangChain as a callback; the trace is built on exit and exposed as adapter.trace.
from attest import AgentResult, LangChainAdapter, expect
adapter = LangChainAdapter(agent_id="research-agent")
with adapter.capture() as handler: agent.invoke({"input": "question"}, config={"callbacks": [handler]})
result = AgentResult(trace=adapter.trace)attest.evaluate(expect(result).required_tools(["google_search"]))The handler records chain, chat model, LLM, and tool events (on_chain_start, on_llm_end, on_tool_start, and related callbacks).
CrewAI
Section titled “CrewAI”CrewAIAdapter.capture() yields the adapter. Call trace_from_crew_output(crew_output, crew) after kickoff() to capture final output, total tokens, and elapsed latency. This adapter produces no steps: configured crew agents and tasks are not proof that an agent or tool executed. Use output and metadata assertions, not agent_called or tool assertions.
from attest import AgentResult, CrewAIAdapter, expect
adapter = CrewAIAdapter(agent_id="research-crew")
with adapter.capture() as cap: output = crew.kickoff()trace = cap.trace_from_crew_output(output, crew)
attest.evaluate(expect(AgentResult(trace=trace)).output_contains("..."))LlamaIndex
Section titled “LlamaIndex”LlamaIndexInstrumentationHandler registers with the LlamaIndex instrumentation dispatcher. Use it as a context manager (or call attach() and detach()), then call build_trace.
from attest import AgentResult, LlamaIndexInstrumentationHandler, expect
handler = LlamaIndexInstrumentationHandler(agent_id="rag-agent")
with handler: response = engine.query("question")
trace = handler.build_trace(query="question", response=str(response), latency_ms=850)attest.evaluate(expect(AgentResult(trace=trace)).output_contains("..."))Google ADK
Section titled “Google ADK”GoogleADKAdapter runs an ADK Runner and captures the events, or builds a trace from events you already collected.
from attest import GoogleADKAdapter
adapter = GoogleADKAdapter(agent_id="root-agent")trace = await adapter.capture_async( runner, user_id="u1", session_id="s1", message="question")
trace = GoogleADKAdapter.from_events(events, agent_id="root-agent", input_message="question")Observability Adapters
Section titled “Observability Adapters”OpenTelemetry
Section titled “OpenTelemetry”OTelAdapter converts OpenTelemetry spans that carry gen_ai.* attributes into an Attest trace using the matching engine’s OTLP conversion. These constructors are class methods; span inputs must belong to one trace. Conversion errors propagate rather than producing an empty trace.
from attest import OTelAdapter
# From opentelemetry-sdk ReadableSpan objectstrace = OTelAdapter.from_spans(spans, agent_id="traced-agent")
# From OTLP/JSON-shaped span dictstrace = OTelAdapter.from_otel_span_dicts(span_dicts, agent_id="traced-agent")
# From a full OTLP/JSON export (a mapping, JSON string, bytes, or pathlib.Path)trace = OTelAdapter.from_otlp(otlp_export, agent_id="traced-agent")attest.adapters.otel.to_otel_spans(trace) performs the inverse and returns OTLP-compatible span dicts.
Manual Adapter
Section titled “Manual Adapter”ManualAdapter builds a trace without a provider or framework. capture(builder_fn) calls builder_fn with a TraceBuilder and returns the built Trace; create_builder() returns a pre-configured TraceBuilder.
from attest import ManualAdapter
adapter = ManualAdapter(agent_id="manual-agent")
trace = adapter.capture( lambda b: b.set_input(query="...") .add_tool_call("search", args={"q": "..."}) .set_output(message="..."))Custom Adapters
Section titled “Custom Adapters”Provider-style adapters inherit BaseProviderAdapter and override the extraction methods. Framework-style adapters inherit BaseAdapter and implement their own capture flow. The tutorial linked below includes a complete dictionary-based adapter and tests without a provider dependency.
Required overrides: _extract_completion, _extract_model, _extract_total_tokens, _extract_tool_calls. Optional overrides: _extract_input, _build_output, _extract_token_split, and _extract_refusal. BaseAdapter provides _create_builder(), _now_ms(), and _resolve_timestamps(). Provider tool-call steps record requested names and arguments, not tool execution results.
See the Writing a Framework Adapter tutorial for a complete guide.
Configuration
Section titled “Configuration”SDK configuration is separate from adapter construction. For example, simulation changes evaluation behavior; it does not prevent the provider calls in the live examples above:
from attest import config
config(simulation=True, sample_rate=0.5)| Keyword | Environment variable |
|---|---|
simulation | ATTEST_SIMULATION |
sample_rate | ATTEST_SAMPLE_RATE |
alert_webhook | ATTEST_ALERT_WEBHOOK |
alert_slack_url | ATTEST_ALERT_SLACK_URL |
Provider adapters do not make provider calls themselves; credentials such as OPENAI_API_KEY and ANTHROPIC_API_KEY are read by your provider client. GoogleADKAdapter.capture_async() is different: it runs the supplied ADK runner. Sampling and alert settings apply to continuous evaluation, not to capture by these adapters.
Related
Section titled “Related”- Python Expect DSL — Assertion methods
- Framework Adapters Guide — Adapter architecture and usage
- Writing a Framework Adapter — Build custom adapters