Skip to main content
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

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:
RecipeSource format
messages_from_openai_chatOpenAI ChatCompletion (modern tool_calls or legacy function_call)
messages_from_anthropic_messagesAnthropic Messages API content blocks
messages_from_vercel_ai_sdkVercel AI SDK UIMessage / CoreMessage
messages_from_prompt_responseFlat prompt/response strings
messages_from_role_content_pairs[(role, content), ...] tuples
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).

messages_from_openai_chat

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

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

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:
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:
messages = tj.messages_from_role_content_pairs([
    ("system", "You are a helpful assistant."),
    ("user", "Hi"),
    ("assistant", "Hello!"),
])

Building the Trajectory

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",
)
ParameterTypeRequiredDescription
messageslist[Message]YesFlat list of messages across all turns. The SDK builds cumulative steps.
conversation_idstrYesUnique conversation identifier.
data_sourcestrYesWhere the trajectory came from (e.g. "my_export", "prod_agent").
rewardReward | NoneNoAggregate reward — use tj.build_reward_from_scalar(value) for a single number.
task_metadatadict | NoneNoRecognized keys: num_turns, total_tokens, total_cost, completion_tokens. Extras are ignored.
errorstr | NoneNoError message if the conversation failed. Auto-sets termination_reason to "ERROR" if not provided.
start_time / end_timestr | NoneNoISO timestamps; used to compute execution_metrics.total_time.
termination_reasonTerminationReason | NoneNoDefaults to "ENV_DONE" or "ERROR" based on error.
extra_telemetrydict | NoneNoExtra fields merged into Trajectory.telemetry.data.
trace_idstr | NoneNoCaller-owned correlation key for linking to telemetry events.
model_idstr | NoneNoTrajectory model identifier (see 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:
HelperPurpose
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.
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:
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 for the full correlation walkthrough.

Core Concepts

Trajectories, Steps, Turns, Messages.

Linking Events to Trajectories

Correlate uploaded trajectories with telemetry via trace_id.

API Reference

Full signatures for builders, recipes, and helpers.

PII Redaction

Strip sensitive data before trajectories leave your environment.