> ## Documentation Index
> Fetch the complete documentation index at: https://arize-ax.mintlify.site/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent Trajectory Evaluations

> Evaluate and monitor the quality of an agent's step-by-step tool-calling trajectory across traces.

export const AskAlyx = ({children}) => {
  const gradientId = `askAlyxGradient-${Math.random().toString(36).slice(2)}`;
  return <div style={{
    display: "flex",
    alignItems: "flex-start",
    gap: "0.625rem",
    margin: "1rem 0",
    padding: "0.75rem 1rem",
    borderRadius: "10px",
    border: "1px solid rgba(120, 115, 245, 0.25)",
    background: "linear-gradient(135deg, rgba(255, 110, 196, 0.08), rgba(120, 115, 245, 0.08))"
  }}>
      <svg width="18" height="18" viewBox="0 0 21 17" xmlns="http://www.w3.org/2000/svg" style={{
    flexShrink: 0,
    marginTop: "0.2rem"
  }}>
        <defs>
          <linearGradient id={gradientId} x1="0%" y1="100%" x2="100%" y2="0%">
            <stop offset="0%" stopColor="#FF3CA8" />
            <stop offset="100%" stopColor="#4827C1" />
          </linearGradient>
        </defs>
        <path d="M6.28906 12.7223C6.28889 11.3831 5.24007 10.3385 3.98926 10.3385C2.73859 10.3387 1.68963 11.3832 1.68945 12.7223C1.68945 14.0616 2.73849 15.1059 3.98926 15.1061C5.24018 15.1061 6.28906 14.0617 6.28906 12.7223ZM7.81152 0.557254C9.70554 -0.563135 12.1081 0.0645388 13.2607 1.91468L13.3691 2.09827V2.09925L20.7266 15.402C20.8713 15.6637 20.8667 15.9823 20.7148 16.2399C20.5629 16.4975 20.2864 16.6559 19.9873 16.6559H14.5459C13.0953 16.6474 11.7648 15.848 11.0469 14.5748V14.5739L6.33301 6.19104C5.22656 4.2273 5.87813 1.706 7.80957 0.558231L7.81152 0.557254ZM11.8906 2.91761C11.2374 1.74047 9.78961 1.34962 8.67188 2.01038L8.67285 2.01136C7.61521 2.64 7.19477 3.99924 7.69336 5.13733L7.80566 5.36194V5.36292L12.5186 13.7448C12.9466 14.5038 13.7274 14.9616 14.5557 14.9664H18.5547L11.8906 2.91663V2.91761ZM7.97949 12.7223C7.97949 14.9527 6.21527 16.7965 3.98926 16.7965C1.7634 16.7963 0 14.9526 0 12.7223C0.000173728 10.4921 1.76351 8.64923 3.98926 8.64905C6.21516 8.64905 7.97932 10.492 7.97949 12.7223Z" fill={`url(#${gradientId})`} />
      </svg>
      <span>{children}</span>
    </div>;
};

When an agent tackles a task it usually takes **multiple steps** - invoking tools, writing code, making API calls, and reasoning along the way. Even if the final answer is right, a poor sequence of steps can waste time, money, or expose users to risk.

Individual span or trace evaluations check that *one* step or response is correct, but they can miss costly mistakes an agent makes *between* steps. **Agent trajectory evaluations** measure the *entire sequence* of tool calls an agent takes to solve a task.

<AskAlyx>**Ask Alyx** to build a trace-scoped evaluator that scores an agent's tool-call trajectory for you -- try *"Create a trace eval that checks whether the agent's tool calls were logical and efficient."*</AskAlyx>

<Frame>
  <img src="https://storage.googleapis.com/arize-phoenix-assets/assets/images/agent-trajectory-eval.png" alt="Agent Trajectory Eval" />
</Frame>

<Info>
  A Colab notebook that walks through the complete workflow is available in the [Agent Trajectory Evaluation Notebook](https://colab.research.google.com/github/Arize-ai/tutorials/blob/main/python/llm/evaluation/agent_trajectory.ipynb).
</Info>

# How It Works

1. **Group tool-calling spans per trace** – each tool call (function call) is captured as a span when you instrument with OpenInference.
2. **Send the ordered list of tool calls to an LLM judge** – Phoenix Evals classifies the trajectory as `correct` or `incorrect` (and can produce an explanation).
3. **Log the evaluation back to Arize AX** – the result is attached to the *root span* of the trace so you can filter and pivot in the UI.

# Prerequisites

1. **Instrumented traces** of your agent with the [OpenInference schema](https://github.com/Arize-ai/openinference)
2. Python 3.10+ and the following packages:

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
pip install arize arize-phoenix arize-phoenix-evals pandas openai nest_asyncio
```

# Implementation

## 1. Pull trace data from Arize AX

<CodeGroup>
  ```python Python SDK v8 theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  Coming Soon
  ```

  ```python Python SDK v7 theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  from arize.exporter import ArizeExportClient
  from arize.utils.types import Environments
  from datetime import datetime, timedelta, timezone

  client = ArizeExportClient()

  df = client.export_model_to_df(
      space_id="your-arize-space-id",
      model_id="your-project-name",
      environment=Environments.TRACING,
      start_time=datetime.now(timezone.utc) - timedelta(days=7),
      end_time=datetime.now(timezone.utc),
  )
  ```
</CodeGroup>

## 2. Filter to the spans you want to score

Most agents emit many spans (retrieval, LLM calls, DB writes, …). For trajectory scoring we usually care about **LLM spans that contain tool calls**.

```python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
from typing import Dict, Any
import pandas as pd

# A reusable helper that applies both trace-level and span-level filters
from agent_trajectory_utils import filter_spans_by_trace_criteria  # provided in the notebook

trajectory_spans = filter_spans_by_trace_criteria(
    df            = df,
    trace_filters = {"name": {"contains": "searchrouter"}},      # tailor to your app
    span_filters  = {"attributes.openinference.span.kind": {"==": "LLM"}},
)
```

## 3. Extract ordered tool calls for each trace

```python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
from agent_trajectory_utils import (
    extract_tool_calls,
    prepare_trace_data_for_evaluation,
)

# Parse `attributes.llm.output_messages` → list of {name, arguments}
trajectory_spans["tool_calls"] = trajectory_spans[
    "attributes.llm.output_messages"
].apply(extract_tool_calls)

# Collapse every trace into a single row that contains its ordered tool calls
trace_df = prepare_trace_data_for_evaluation(
    df = trajectory_spans,
    extract_cols = {
        "tool_calls": "tool_calls",
        "attributes.llm.tools": "attributes.llm.tools",           # reference tool schema
        "attributes.input.value": "attributes.input.value",       # original user input
    },
)
```

## 4. Define the evaluation prompt

The LLM judge receives:

* **`{tool_calls}`** – the actual trajectory (step → tool → arguments)
* **`{attributes.input.value}`** – the user input that kicked off the trace
* **`{attributes.llm.tools}`** – the JSON schema of available tools
* *(Optional)* **`{reference_outputs}`** – a golden trajectory you expect

```python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
TRAJECTORY_ACCURACY_PROMPT = """
You are a helpful AI bot that checks whether an AI agent's internal trajectory is accurate and effective.

You will be given:
1. The agent's actual trajectory of tool calls
2. The user input that initiated the trajectory
3. The definition of each tool that can be called

An accurate trajectory:
- Progresses logically from step to step
- Uses the right tools for the task
- Is reasonably efficient (no unnecessary detours)

##
Actual Trajectory:
{tool_calls}

User Input:
{attributes.input.value}

Tool Definitions:
{attributes.llm.tools}
##

Respond with **exactly** one word: `correct` or `incorrect`.
- `correct` → trajectory adheres to the rubric and achieves the task.
- `incorrect` → trajectory is confusing, inefficient, or fails the task.
"""
```

## 5. Run the evaluation

```python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
from phoenix.evals import llm_classify, OpenAIModel
import nest_asyncio, os

nest_asyncio.apply()

model = OpenAIModel(
    api_key = os.environ["OPENAI_API_KEY"],
    model   = "gpt-4o-mini",
    temperature = 0.0,
)

rails = ["correct", "incorrect"]
results = llm_classify(
    dataframe           = trace_df,
    template            = TRAJECTORY_ACCURACY_PROMPT,
    model               = model,
    rails               = rails,
    provide_explanation = True,   # add a free-text rationale for debugging
    verbose             = False,
)
```

## 6. Log the results back to Arize AX

Link the evaluation to the **root span** of each trace so you can slice & dice in the UI.

<CodeGroup>
  ```python Python SDK v8 theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  Coming Soon
  ```

  ```python Python SDK v7 theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  from arize.pandas.logger import Client

  # Merge eval results with original trace data to grab span id
  merged = trace_df.merge(results, left_index=True, right_index=True)
  merged.rename(
      columns={
          "label": "trace_eval.AgentTrajectoryAccuracy.label",
          "explanation": "trace_eval.AgentTrajectoryAccuracy.explanation",
      },
      inplace=True,
  )

  root_spans = df[df["parent_id"].isna()][["context.trace_id", "context.span_id"]]
  log_df = merged.merge(root_spans, on="context.trace_id", how="left")
  log_df.set_index("context.span_id", inplace=True)

  arize_client = Client(
      space_id="your-arize-space-id",
      api_key="your-arize-api-key",
  )
  resp = arize_client.log_evaluations_sync(
      dataframe=log_df,
      model_id="your-project-name",
  )
  ```
</CodeGroup>
