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

# Bring Your Own Data

> Build trajectories from CSV, JSONL, OpenAI, Anthropic, or any custom format — no provider required.

When your traces don't live in LangSmith, build trajectories directly from your own data.
`tj.build_trajectory_from_messages()` takes a flat list of `Message` objects and produces a
fully-formed `Trajectory` ready to upload — same shape as the provider-ingested ones.

The SDK ships recipes (`messages_from_*`) for the most common formats. For anything else,
construct `Message` objects yourself using the same primitives.

## Quickstart

```python theme={null}
import trajectory_sdk as tj

tj.init(project_id="acme", trajectory_api_key="tj_key_...")

trajectories = []
for row in rows:
    messages = tj.messages_from_openai_chat(row["messages"])
    trajectory = tj.build_trajectory_from_messages(
        messages=messages,
        conversation_id=row["id"],
        data_source="my_export",
    )
    trajectories.append(trajectory)

tj.upload(trajectories, dataset="my_dataset")
```

No provider credentials are needed — only `trajectory_api_key`.

## Message Recipes

The SDK ships adapters for these source formats:

| Recipe                                                                  | Source format                                                         |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------- |
| [`messages_from_openai_chat`](#messages-from-openai-chat)               | OpenAI ChatCompletion (modern `tool_calls` or legacy `function_call`) |
| [`messages_from_anthropic_messages`](#messages-from-anthropic-messages) | Anthropic Messages API content blocks                                 |
| [`messages_from_vercel_ai_sdk`](#messages-from-vercel-ai-sdk)           | Vercel AI SDK `UIMessage` / `CoreMessage`                             |
| [`messages_from_prompt_response`](#messages-from-prompt-response)       | Flat prompt/response strings                                          |
| [`messages_from_role_content_pairs`](#messages-from-role-content-pairs) | `[(role, content), ...]` tuples                                       |

<Note>
  **All recipes fail loud on unexpected input.** Silent fallbacks turn parse bugs into silent
  data-quality bugs, so unrecognized shapes raise `ValueError` / `TypeError` with descriptive
  messages. Fix the input, or write a custom recipe (see [Custom Formats](#custom-formats)).
</Note>

### messages\_from\_openai\_chat

```python theme={null}
raw = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "What's the weather in SF?"},
    {
        "role": "assistant",
        "content": None,
        "tool_calls": [{
            "id": "call_1",
            "function": {"name": "get_weather", "arguments": '{"city": "SF"}'},
        }],
    },
    {"role": "tool", "tool_call_id": "call_1", "content": '{"temp": 62}'},
    {"role": "assistant", "content": "It's 62°F in SF."},
]
messages = tj.messages_from_openai_chat(raw)
```

Handles role normalization (`human → user`, `ai → assistant`), multi-modal content arrays,
modern `tool_calls`, legacy `function_call`, `usage`, `finish_reason`, and `reasoning`
pass-through.

### messages\_from\_anthropic\_messages

```python theme={null}
raw = [
    {"role": "user", "content": "What's the weather in SF?"},
    {
        "role": "assistant",
        "content": [
            {"type": "text", "text": "Let me check."},
            {"type": "tool_use", "id": "tu_1", "name": "get_weather", "input": {"city": "SF"}},
        ],
    },
    {
        "role": "user",
        "content": [
            {"type": "tool_result", "tool_use_id": "tu_1", "content": [{"type": "text", "text": "62°F"}]},
        ],
    },
]
messages = tj.messages_from_anthropic_messages(raw)
```

Parallel `tool_result` blocks are fanned out into one `role="tool"` `Message` each so step
builders and tool-failure metrics see every result individually.

### messages\_from\_vercel\_ai\_sdk

```python theme={null}
raw = [
    {"role": "user", "content": "What's the weather in SF?"},
    {
        "role": "assistant",
        "parts": [
            {"type": "text", "text": "Looking it up."},
            {
                "type": "tool-invocation",
                "toolInvocation": {"toolCallId": "tc_1", "toolName": "get_weather", "args": {"city": "SF"}},
            },
        ],
    },
    {
        "role": "tool",
        "parts": [
            {"type": "tool-result", "toolCallId": "tc_1", "result": {"temp": 62}},
        ],
    },
]
messages = tj.messages_from_vercel_ai_sdk(raw)
```

Supports both `UIMessage` (`parts`) and `CoreMessage` (`content`) shapes, `text` /
`tool-invocation` / `tool-call` / `tool-result` / `reasoning` parts.

### messages\_from\_prompt\_response

For the simplest case — a single prompt and response, optionally with a system message:

```python theme={null}
messages = tj.messages_from_prompt_response(
    prompt="What's 2+2?",
    response="4",
    system="You are a calculator.",
)
```

### messages\_from\_role\_content\_pairs

For ad-hoc construction from tuples:

```python theme={null}
messages = tj.messages_from_role_content_pairs([
    ("system", "You are a helpful assistant."),
    ("user", "Hi"),
    ("assistant", "Hello!"),
])
```

## Building the Trajectory

```python theme={null}
trajectory = tj.build_trajectory_from_messages(
    messages=messages,
    conversation_id="conv-1",
    data_source="my_export",
    reward=tj.build_reward_from_scalar(0.85),
    task_metadata={"num_turns": 3, "total_tokens": 1200, "total_cost": 0.012},
    start_time="2025-05-01T10:00:00Z",
    end_time="2025-05-01T10:00:05Z",
    termination_reason="ENV_DONE",
    trace_id="req_01HXYZ",
    model_id="model_internal_123",
)
```

| Parameter                 | Type                        | Required | Description                                                                                            |
| ------------------------- | --------------------------- | -------- | ------------------------------------------------------------------------------------------------------ |
| `messages`                | `list[Message]`             | Yes      | Flat list of messages across all turns. The SDK builds cumulative steps.                               |
| `conversation_id`         | `str`                       | Yes      | Unique conversation identifier.                                                                        |
| `data_source`             | `str`                       | Yes      | Where the trajectory came from (e.g. `"my_export"`, `"prod_agent"`).                                   |
| `reward`                  | `Reward \| None`            | No       | Aggregate reward — use `tj.build_reward_from_scalar(value)` for a single number.                       |
| `task_metadata`           | `dict \| None`              | No       | Recognized keys: `num_turns`, `total_tokens`, `total_cost`, `completion_tokens`. Extras are ignored.   |
| `error`                   | `str \| None`               | No       | Error message if the conversation failed. Auto-sets `termination_reason` to `"ERROR"` if not provided. |
| `start_time` / `end_time` | `str \| None`               | No       | ISO timestamps; used to compute `execution_metrics.total_time`.                                        |
| `termination_reason`      | `TerminationReason \| None` | No       | Defaults to `"ENV_DONE"` or `"ERROR"` based on `error`.                                                |
| `extra_telemetry`         | `dict \| None`              | No       | Extra fields merged into `Trajectory.telemetry.data`.                                                  |
| `trace_id`                | `str \| None`               | No       | Caller-owned correlation key for linking to telemetry events.                                          |
| `model_id`                | `str \| None`               | No       | Trajectory model identifier (see [model id helpers](/sdk/api-reference#model-id-helpers)).             |

The builder automatically computes cumulative steps, tool-call metrics
(`num_tool_calls` / `num_tool_failures` / `tool_error_rate`), and content-hash / idempotency-key
telemetry fields.

## Custom Formats

If none of the shipped recipes fit your data, write your own. The public helpers cover the
fiddly parts:

| Helper                             | Purpose                                                                                                           |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `tj.normalize_role(role_str)`      | Canonicalize role to `system` / `user` / `assistant` / `tool`. Recognizes aliases like `human`, `ai`, `function`. |
| `tj.flatten_text_content(content)` | Collapse multi-modal `content` (string, list of blocks, dict) into plain text.                                    |
| `tj.parse_tool_arguments(raw)`     | Normalize tool-call arguments (dict, JSON string, or `None`) into a dict.                                         |

```python theme={null}
from trajectory_sdk import Message, ToolCall, ToolResponse

def messages_from_my_format(raw):
    out = []
    for entry in raw:
        role = tj.normalize_role(entry["who"])
        out.append(Message(
            role=role,
            content=tj.flatten_text_content(entry.get("body")),
        ))
    return out
```

## Pair With Telemetry

`build_trajectory_from_messages` accepts a `trace_id` so trajectories built this way participate
in the same correlation story as provider-imported ones. Pair it with `tj.start_trace()` and
`tj.upload_trace()` for the cleanest workflow:

```python theme={null}
trace = tj.start_trace()

trajectory = trace.build_trajectory(
    messages=tj.messages_from_openai_chat(row["messages"]),
    conversation_id=row["id"],
    data_source="my_export",
)

trace.event("dataset.row_processed", {"row_id": row["id"]})

tj.upload_trace(trajectory, trace.events, dataset="my_dataset")
```

See [Linking Events to Trajectories](/sdk/linking-events-to-trajectories) for the full
correlation walkthrough.

## Related

<CardGroup cols={2}>
  <Card title="Core Concepts" icon="lightbulb" href="/concepts">
    Trajectories, Steps, Turns, Messages.
  </Card>

  <Card title="Linking Events to Trajectories" icon="link" href="/sdk/linking-events-to-trajectories">
    Correlate uploaded trajectories with telemetry via `trace_id`.
  </Card>

  <Card title="API Reference" icon="square-terminal" href="/sdk/api-reference">
    Full signatures for builders, recipes, and helpers.
  </Card>

  <Card title="PII Redaction" icon="shield-halved" href="/sdk/pii-redaction">
    Strip sensitive data before trajectories leave your environment.
  </Card>
</CardGroup>
