Voxli Voxli

Send Traces

Give Voxli your agent’s traces, so each conversation shows what your agent did in every turn.

Traces reach Voxli in two ways: your agent sends them with an OpenTelemetry exporter, or Voxli reads them from a connected Langfuse or LangSmith. Either way, Voxli matches traces to a chat by its id, so the chat id in your traces must equal the externalId you import the chat with.

Sending traces with OpenTelemetry

Point an OTLP/HTTP exporter at POST /traces, with your API key as Authorization: Bearer <key>.

import json
import os
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http import Compression
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
api_key = os.getenv("VOXLI_API_KEY")
base_url = os.getenv("VOXLI_API_URL", "https://api.voxli.io")
exporter = OTLPSpanExporter(
endpoint=f"{base_url}/traces",
headers={"Authorization": f"Bearer {api_key}"},
compression=Compression.Gzip,
)
provider = TracerProvider(resource=Resource.create({"service.name": "support-agent"}))
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("support-agent")
def messages(role, text):
return json.dumps([{"role": role, "parts": [{"type": "text", "content": text}]}])
def check_order(order):
with tracer.start_as_current_span("execute_tool check_order") as span:
span.set_attribute("gen_ai.operation.name", "execute_tool")
span.set_attribute("gen_ai.tool.name", "check_order")
span.set_attribute("gen_ai.tool.call.arguments", json.dumps({"order": order}))
result = {"status": "shipped", "eta": "Friday"}
span.set_attribute("gen_ai.tool.call.result", json.dumps(result))
return result
# One trace per turn. Its root span names the chat and holds what was said.
def answer(chat_id, user_message):
with tracer.start_as_current_span("invoke_agent support-agent") as span:
span.set_attribute("gen_ai.operation.name", "invoke_agent")
span.set_attribute("gen_ai.conversation.id", chat_id)
span.set_attribute("gen_ai.input.messages", messages("user", user_message))
order = check_order("NS-1042")
reply = f"NS-1042 has {order['status']} and should arrive on {order['eta']}."
span.set_attribute("gen_ai.output.messages", messages("assistant", reply))
return reply
answer("chat-8f2c1a", "Where is my order NS-1042?") # your agent's own id for the chat
provider.shutdown() # sends what is left before the process exits
  • Body - an OTLP trace export request as protobuf (application/x-protobuf, the exporter’s default) or JSON (application/json), optionally compressed with gzip or deflate.
  • Size - at most 10 MB per request, before and after decompression. A larger request gets 413.
  • Path - an exporter given only a base URL, such as OTEL_EXPORTER_OTLP_ENDPOINT=https://api.voxli.io/traces, appends /v1/traces. Voxli takes traces at /traces/v1/traces too.
  • Response - an OTLP export response. Spans without valid trace and span ids are rejected and counted in its partial success; the rest are kept. A body that isn’t an OTLP trace request gets 400.

Naming the chat

Each span belongs to the chat named in its gen_ai.conversation.id attribute, or in session.id when that is missing. A span that names no chat takes the id from another span of its trace, or from its resource. Naming the chat on the root span of each turn is enough.

Spans of one trace don’t have to arrive together. A span whose trace hasn’t named a chat yet is held until a span of that trace does. Voxli deletes held spans after 7 days if their trace never names a chat.

What Voxli reads from a trace

Voxli treats each trace as one turn of the chat. The user’s message and the reply come from the trace’s input and output, usually set on its root span: gen_ai.input.messages and gen_ai.output.messages, or the same values in OpenInference, Langfuse, OpenLLMetry or Vercel AI SDK attributes. Tool calls, retrievals and guardrails come from the spans inside the turn.

Voxli merges new spans in the background, shortly after they arrive.

Reading traces from Langfuse or LangSmith

If your agent already sends traces to Langfuse or LangSmith, connect it in Settings > Integrations instead. Conversations shows how. Voxli then reads a chat’s traces by its id:

  • Langfuse - the session with that id.
  • LangSmith - the thread with that id: runs whose metadata has it as session_id, thread_id or conversation_id, in any project of the workspace.

Voxli reads them at two moments:

  • After each import. About a minute after you import a chat, Voxli reads its traces from every connection, from up to 30 days before the chat started. Importing the chat again reads them again, so a longer chat gets its newer turns.
  • When Voxli AI looks the chat up by its id and Voxli has no traces for it.

A connection alone doesn’t bring chats into Voxli; Voxli reads it only for a chat id it has. If your agent sends its traces to Voxli with OpenTelemetry, Voxli doesn’t also read that chat from your connections after an import.

How traces and transcripts combine

  • With an imported transcript, the transcript stays as you sent it. The traces attach to it and show how each turn was produced.
  • Without one, Voxli builds the conversation from the traces, and the app marks it From traces. Traces record what your agent’s code did, which can differ from what the user saw. Importing the transcript later replaces the built one, and the traces stay attached.

Traces with no user message or reply in any turn don’t make a conversation.

Traces of a Voxli test don’t make one either, since the test already shows the chat. Voxli recognizes them when the chat id is the test’s id (what Voxli sends your agent as test_result_id), or when a span carries that id as voxli_test_id or test_result_id, in an attribute or in its metadata. The traces are still kept.

What’s next