parlot.instrumentation.langgraph

parlot-instrumentation-langgraph: OTel instrumentation for LangGraph / LangChain agents.

 1"""parlot-instrumentation-langgraph: OTel instrumentation for LangGraph / LangChain agents."""
 2
 3from parlot.core import (
 4    add_platform_ref,
 5    human_escalation,
 6    record_human_rep,
 7    set_session_attribute,
 8    set_session_metadata,
 9    stamp_platform_refs,
10)
11
12from ._auto import close_session, parlotize
13from ._callbacks import ParlotLangGraphCallbackHandler
14
15__all__ = [
16    "parlotize",
17    "close_session",
18    "ParlotLangGraphCallbackHandler",
19    "add_platform_ref",
20    "human_escalation",
21    "record_human_rep",
22    "set_session_attribute",
23    "set_session_metadata",
24    "stamp_platform_refs",
25]
def parlotize( agent_id: str, *, endpoint: Optional[str] = None, api_key: Optional[str] = None, capture_genai_content: Optional[bool] = None, service_name: Optional[str] = None, tracer_provider: opentelemetry.trace.TracerProvider | None = None, version: Optional[str] = None, channel: Optional[str] = None, modality: Optional[str] = None, capture_logs: bool | list[str] | None = None, log_level: Optional[str] = None) -> parlot.core.ParlotContext:
 22def parlotize(
 23    agent_id: str,
 24    *,
 25    endpoint: Optional[str] = None,
 26    api_key: Optional[str] = None,
 27    capture_genai_content: Optional[bool] = None,
 28    service_name: Optional[str] = None,
 29    tracer_provider: TracerProvider | None = None,
 30    version: Optional[str] = None,
 31    channel: Optional[str] = None,
 32    modality: Optional[str] = None,
 33    capture_logs: bool | list[str] | None = None,
 34    log_level: Optional[str] = None,
 35) -> ParlotContext:
 36    """Parlotize LangGraph instrumentation and OTLP export.
 37
 38    Registers a global LangChain callback handler (LangSmith-style) so
 39    ``invoke`` / ``ainvoke`` / ``astream`` emit GenAI spans without per-call
 40    callbacks. When LiveKit already owns the active session, contract spans
 41    are suppressed and GenAI ops nest under the current OTel context (LiveKit
 42    stamps channel/modality). For LangGraph-owned sessions, pass ``channel``
 43    (e.g. ``webchat``) and optionally ``modality`` so ingest does not assume
 44    voice.
 45
 46    Shared parameters (``agent_id``, plus keyword-only ``endpoint``, ``api_key``,
 47    ``version``, ``capture_genai_content``, ``capture_logs``, ``log_level``,
 48    ``service_name``, ``tracer_provider``) match every adapter — see
 49    ``parlot.core.ParlotizeProtocol``.
 50
 51    Args:
 52        agent_id: Shared — required canonical ``session.agent_id``. Must be
 53            non-empty after stripping whitespace.
 54        endpoint: Shared — Parlot OTLP base URL (or ``PARLOT_ENDPOINT``).
 55        api_key: Shared — org API key (or ``PARLOT_API_KEY``).
 56        capture_genai_content: Shared — GenAI payload capture override.
 57            Precedence: this kwarg → Settings → Generative AI → on.
 58        service_name: Shared — OTel ``service.name`` (defaults to ``agent_id``
 59            or ``"langgraph-agent"``).
 60        tracer_provider: Shared — existing ``TracerProvider``. If LiveKit
 61            already initialized one in-process, this call adopts it.
 62        version: Shared — ``gen_ai.agent.version``. Pass ``version=`` to set it;
 63            otherwise ``\"unknown\"``.
 64        channel: Communication channel for standalone LangGraph sessions
 65            (e.g. ``"webchat"``, ``"slack"``, ``"sms"``). Defaults to
 66            ``"text"``. Ignored when LiveKit owns the session.
 67        modality: Session modality (``"text"``, ``"voice"``, or
 68            ``"multimodal"``). Defaults to ``"text"`` for standalone sessions.
 69        capture_logs: Shared — session log capture (bool or globs).
 70        log_level: Shared — minimum level for session log capture.
 71
 72    Returns:
 73        The ``ParlotContext`` created for this process (or the prior one if
 74        already configured).
 75    """
 76    global _configured, _configured_agent_id, _configured_agent_version, _parlot_context
 77    if _configured:
 78        logger.debug("parlot-instrumentation.langgraph already configured — skipping")
 79        assert _parlot_context is not None
 80        return _parlot_context
 81
 82    from parlot.core import base_parlotize
 83    from parlot.core.genai_content_capture import should_capture_genai_content
 84    from parlot.core.sdk_version import resolve_parlot_sdk_version
 85
 86    _configured_agent_version = (version or "").strip() or "unknown"
 87
 88    res = base_parlotize(
 89        agent_id,
 90        endpoint=endpoint,
 91        api_key=api_key,
 92        capture_genai_content=capture_genai_content,
 93        tracer_provider=tracer_provider,
 94        version=_configured_agent_version,
 95        capture_logs=capture_logs,
 96        log_level=log_level,
 97    )
 98    _parlot_context = res.context
 99    _configured_agent_id = res.agent_id
100
101    runtime = res.context.runtime
102    capture = should_capture_genai_content(
103        _configured_agent_id,
104        capture_genai_content_config=res.capture_genai_content,
105        bootstrap_globs=list(runtime.capture_genai_content_globs) if runtime else None,
106        bootstrap_agents=runtime.capture_genai_content_agents_map() if runtime else None,
107        bootstrap_present=bool(runtime and runtime.capture_genai_content_policy_present),
108    )
109
110    tracer_provider = res.tracer_provider
111    if tracer_provider is None:
112        if not res.endpoint:
113            raise ValueError(
114                "No OTLP endpoint configured. Pass endpoint= or set the "
115                "PARLOT_ENDPOINT environment variable."
116            )
117        tracer_provider = _build_provider(
118            endpoint=res.endpoint,
119            api_key=res.api_key,
120            service_name=service_name,
121            service_version=_configured_agent_version or None,
122        )
123
124    from opentelemetry import trace
125
126    try:
127        trace.set_tracer_provider(tracer_provider)
128    except Exception:
129        logger.debug("Could not set global tracer provider", exc_info=True)
130
131    tracer = tracer_provider.get_tracer(
132        "parlot.instrumentation.langgraph",
133        resolve_parlot_sdk_version() or None,
134    )
135
136    from ._callbacks import ParlotLangGraphCallbackHandler
137    from ._hooks import install_parlotize_hook
138    from ._session import set_channel_modality, set_identity, set_tracer
139
140    set_identity(_configured_agent_id, _configured_agent_version)
141    set_channel_modality(channel=channel or "", modality=modality or "")
142    set_tracer(tracer)
143    handler = ParlotLangGraphCallbackHandler(tracer, capture_genai_content=capture)
144    install_parlotize_hook(handler)
145
146    _configured = True
147    logger.debug(
148        "parlot-instrumentation.langgraph configured (endpoint=%s)", res.endpoint
149    )
150    return res.context

Parlotize LangGraph instrumentation and OTLP export.

Registers a global LangChain callback handler (LangSmith-style) so invoke / ainvoke / astream emit GenAI spans without per-call callbacks. When LiveKit already owns the active session, contract spans are suppressed and GenAI ops nest under the current OTel context (LiveKit stamps channel/modality). For LangGraph-owned sessions, pass channel (e.g. webchat) and optionally modality so ingest does not assume voice.

Shared parameters (agent_id, plus keyword-only endpoint, api_key, version, capture_genai_content, capture_logs, log_level, service_name, tracer_provider) match every adapter — see parlot.core.ParlotizeProtocol.

Arguments:
  • agent_id: Shared — required canonical session.agent_id. Must be non-empty after stripping whitespace.
  • endpoint: Shared — Parlot OTLP base URL (or PARLOT_ENDPOINT).
  • api_key: Shared — org API key (or PARLOT_API_KEY).
  • capture_genai_content: Shared — GenAI payload capture override. Precedence: this kwarg → Settings → Generative AI → on.
  • service_name: Shared — OTel service.name (defaults to agent_id or "langgraph-agent").
  • tracer_provider: Shared — existing TracerProvider. If LiveKit already initialized one in-process, this call adopts it.
  • version: Shared — gen_ai.agent.version. Pass version= to set it; otherwise "unknown".
  • channel: Communication channel for standalone LangGraph sessions (e.g. "webchat", "slack", "sms"). Defaults to "text". Ignored when LiveKit owns the session.
  • modality: Session modality ("text", "voice", or "multimodal"). Defaults to "text" for standalone sessions.
  • capture_logs: Shared — session log capture (bool or globs).
  • log_level: Shared — minimum level for session log capture.
Returns:

The ParlotContext created for this process (or the prior one if already configured).

def close_session(thread_id: str, *, reason: str = 'completed') -> None:
177def close_session(thread_id: str, *, reason: str = "completed") -> None:
178    """Public helper to close a LangGraph-owned session by thread_id.
179
180    Emits ``parlot.session.close`` with accumulated usage and turn counts.
181    Optional — sessions are also flushed on process exit.
182
183    Args:
184        thread_id: LangGraph thread id from
185            ``config={"configurable": {"thread_id": "..."}}``.
186        reason: Close reason stamped on ``session.close_reason``. Defaults to
187            ``"completed"``.
188    """
189    from ._session import close_session as _close
190
191    _close(thread_id, reason=reason)

Public helper to close a LangGraph-owned session by thread_id.

Emits parlot.session.close with accumulated usage and turn counts. Optional — sessions are also flushed on process exit.

Arguments:
  • thread_id: LangGraph thread id from config={"configurable": {"thread_id": "..."}}.
  • reason: Close reason stamped on session.close_reason. Defaults to "completed".
class ParlotLangGraphCallbackHandler(langchain_core.callbacks.base.BaseCallbackHandler):
129class ParlotLangGraphCallbackHandler(BaseCallbackHandler):
130    """LangChain callback handler that emits Parlot GenAI spans.
131
132    Translates LangGraph / LangChain execution events (LLM starts/ends, tool
133    invocations, chain runs) into OpenTelemetry GenAI spans
134    (``invoke_agent``, ``invoke_workflow``, ``chat``, ``execute_tool``).
135
136    ``parlotize()`` registers this handler via LangChain configuration hooks,
137    so manual ``callbacks=[...]`` attachment is not required. When LiveKit
138    owns the active session, contract spans are suppressed and GenAI ops nest
139    under the current OTel context.
140    """
141
142    raise_error = False
143
144    def __init__(
145        self,
146        tracer: Tracer,
147        *,
148        capture_genai_content: bool = True,
149    ) -> None:
150        self._tracer = tracer
151        self._capture_genai_content = capture_genai_content
152        self._spans: dict[str, Span] = {}
153        self._root_runs: set[str] = set()
154
155    @property
156    def ignore_chain(self) -> bool:
157        return False
158
159    @property
160    def ignore_llm(self) -> bool:
161        return False
162
163    @property
164    def ignore_agent(self) -> bool:
165        return False
166
167    def _run_key(self, run_id: UUID | None) -> str:
168        return str(run_id) if run_id else ""
169
170    def _active_langgraph_state(self) -> _LangGraphSessionState | None:
171        state = get_active_session()
172        if isinstance(state, _LangGraphSessionState):
173            return state
174        return None
175
176    def _contract_attrs(
177        self, state: _LangGraphSessionState | None
178    ) -> dict[str, AttributeValue]:
179        """Session/turn keys required by collector operational ingest."""
180        if state is None:
181            return {}
182        turn_index = state.open_agent_turn_index or state.turn_count
183        attrs: dict[str, AttributeValue] = {
184            ATTR_SESSION_ID: state.session_id,
185            ATTR_SESSION_CONVERSATION_ID: state.conversation_id,
186            ATTR_GEN_AI_CONVERSATION_ID: state.conversation_id,
187            ATTR_LG_THREAD_ID: state.thread_id,
188        }
189        if turn_index > 0:
190            attrs[ATTR_TURN_INDEX] = turn_index
191        return attrs
192
193    def _start(
194        self,
195        name: str,
196        run_id: UUID | None,
197        *,
198        attributes: dict[str, AttributeValue] | None = None,
199        parent_run_id: UUID | None = None,
200    ) -> Span | None:
201        key = self._run_key(run_id)
202        if not key:
203            return None
204        parent_ctx = None
205        if parent_run_id is not None:
206            parent = self._spans.get(self._run_key(parent_run_id))
207            if parent is not None:
208                parent_ctx = trace.set_span_in_context(parent)
209        span = self._tracer.start_span(name, context=parent_ctx, attributes=attributes or {})
210        self._spans[key] = span
211        return span
212
213    def _end(self, run_id: UUID | None, *, error: BaseException | None = None) -> None:
214        key = self._run_key(run_id)
215        span = self._spans.pop(key, None)
216        if span is None:
217            return
218        if error is not None:
219            span.set_status(Status(StatusCode.ERROR, str(error)[:500]))
220            span.record_exception(error)
221        else:
222            span.set_status(Status(StatusCode.OK))
223        span.end()
224
225    def _thread_id(self, **kwargs: Any) -> str:
226        metadata = kwargs.get("metadata") or {}
227        tags = kwargs.get("tags")
228        tid = thread_id_from_metadata(metadata if isinstance(metadata, dict) else {})
229        if tid:
230            return tid
231        # LangGraph puts thread_id under metadata["thread_id"] or configurable
232        cfg = kwargs.get("config")
233        if isinstance(cfg, dict):
234            configurable = cfg.get("configurable") or {}
235            if isinstance(configurable, dict) and configurable.get("thread_id"):
236                return str(configurable["thread_id"])
237        if isinstance(tags, list):
238            for tag in tags:
239                if isinstance(tag, str) and tag.startswith("thread_id:"):
240                    return tag.split(":", 1)[1]
241        return ""
242
243    def on_chain_start(
244        self,
245        serialized: dict[str, Any] | None,
246        inputs: dict[str, Any],
247        *,
248        run_id: UUID,
249        parent_run_id: UUID | None = None,
250        tags: list[str] | None = None,
251        metadata: dict[str, Any] | None = None,
252        name: str | None = None,
253        **kwargs: Any,
254    ) -> Any:
255        serialized = serialized or {}
256        chain_name = name or serialized.get("name") or serialized.get("id", ["chain"])[-1]
257        thread_id = self._thread_id(metadata=metadata, tags=tags, **kwargs)
258        state = ensure_session(thread_id) if thread_id or not livekit_owns_session() else None
259        if state is None and not livekit_owns_session():
260            state = ensure_session(thread_id or "")
261
262        is_root = parent_run_id is None
263        span_name = SPAN_GEN_AI_INVOKE_AGENT if is_root else SPAN_GEN_AI_INVOKE_WORKFLOW
264        op = GEN_AI_OP_INVOKE_AGENT if is_root else GEN_AI_OP_INVOKE_WORKFLOW
265        attrs: dict[str, AttributeValue] = {
266            ATTR_AGENT_FRAMEWORK: "langgraph",
267            ATTR_GEN_AI_OP_NAME: op,
268            ATTR_AGENT_ROLE: "pipeline" if is_root else "pipeline",
269            ATTR_AGENT_STAGE: "node" if not is_root else "turn",
270            ATTR_LG_GRAPH_NAME: str(chain_name),
271            ATTR_LG_RUN_ID: str(run_id),
272        }
273        if not is_root:
274            attrs[ATTR_LG_NODE_NAME] = str(chain_name)
275        if is_root:
276            self._root_runs.add(str(run_id))
277            if state is not None and not livekit_owns_session():
278                user_text = _last_message_text(
279                    _messages_from_payload(inputs),
280                    roles={"human", "user"},
281                )
282                user_idx = emit_turn(
283                    state,
284                    role="user",
285                    utterance_text=user_text,
286                )
287                state.open_agent_turn_index = user_idx + 1
288        attrs.update(self._contract_attrs(state))
289
290        self._start(
291            span_name,
292            run_id,
293            attributes=attrs,
294            parent_run_id=parent_run_id,
295        )
296
297    def on_chain_end(
298        self,
299        outputs: dict[str, Any],
300        *,
301        run_id: UUID,
302        parent_run_id: UUID | None = None,
303        **kwargs: Any,
304    ) -> Any:
305        key = str(run_id)
306        if key in self._root_runs:
307            state = self._active_langgraph_state()
308            if state is not None and not livekit_owns_session():
309                agent_text = _last_message_text(
310                    _messages_from_payload(outputs),
311                    roles={"ai", "assistant"},
312                )
313                emit_turn(
314                    state,
315                    role="agent",
316                    turn_index=state.open_agent_turn_index,
317                    utterance_text=agent_text,
318                )
319                state.open_agent_turn_index = None
320        self._end(run_id)
321        self._root_runs.discard(key)
322
323    def on_chain_error(
324        self,
325        error: BaseException,
326        *,
327        run_id: UUID,
328        parent_run_id: UUID | None = None,
329        **kwargs: Any,
330    ) -> Any:
331        key = str(run_id)
332        if key in self._root_runs:
333            state = self._active_langgraph_state()
334            if state is not None:
335                state.open_agent_turn_index = None
336        self._end(run_id, error=error)
337        self._root_runs.discard(key)
338
339    def on_llm_start(
340        self,
341        serialized: dict[str, Any] | None,
342        prompts: list[str],
343        *,
344        run_id: UUID,
345        parent_run_id: UUID | None = None,
346        tags: list[str] | None = None,
347        metadata: dict[str, Any] | None = None,
348        **kwargs: Any,
349    ) -> Any:
350        serialized = serialized or {}
351        metadata = metadata or {}
352        model = str(
353            metadata.get("ls_model_name")
354            or serialized.get("name")
355            or metadata.get("model")
356            or ""
357        ).strip()
358        provider = str(metadata.get("ls_provider") or "").strip()
359        attrs: dict[str, AttributeValue] = {
360            ATTR_AGENT_FRAMEWORK: "langgraph",
361            ATTR_GEN_AI_OP_NAME: GEN_AI_OP_CHAT,
362            ATTR_AGENT_ROLE: "llm",
363            ATTR_AGENT_STAGE: "node",
364            ATTR_LG_RUN_ID: str(run_id),
365        }
366        if model:
367            attrs[ATTR_GEN_AI_MODEL] = model
368        if provider:
369            attrs[ATTR_GEN_AI_PROVIDER] = provider
370        attrs.update(self._contract_attrs(self._active_langgraph_state()))
371        span = self._start(
372            span_name_chat(model or None),
373            run_id,
374            attributes=attrs,
375            parent_run_id=parent_run_id,
376        )
377        if span is not None and self._capture_genai_content and prompts:
378            span.add_event(
379                EVENT_GEN_AI_USER_MESSAGE,
380                {"content": prompts[-1][:4000]},
381            )
382
383    def on_chat_model_start(
384        self,
385        serialized: dict[str, Any] | None,
386        messages: list[list[Any]],
387        *,
388        run_id: UUID,
389        parent_run_id: UUID | None = None,
390        tags: list[str] | None = None,
391        metadata: dict[str, Any] | None = None,
392        **kwargs: Any,
393    ) -> Any:
394        prompts: list[str] = []
395        for batch in messages or []:
396            for msg in batch:
397                content = getattr(msg, "content", None)
398                if isinstance(content, str) and content:
399                    prompts.append(content)
400        self.on_llm_start(
401            serialized,
402            prompts,
403            run_id=run_id,
404            parent_run_id=parent_run_id,
405            tags=tags,
406            metadata=metadata,
407            **kwargs,
408        )
409
410    def on_llm_end(
411        self,
412        response: Any,
413        *,
414        run_id: UUID,
415        parent_run_id: UUID | None = None,
416        **kwargs: Any,
417    ) -> Any:
418        span = self._spans.get(self._run_key(run_id))
419        if span is not None:
420            usage = _usage_from_llm_result(response)
421            if usage.get("input"):
422                span.set_attribute(ATTR_GEN_AI_IN_TOKENS, usage["input"])
423            if usage.get("output"):
424                span.set_attribute(ATTR_GEN_AI_OUT_TOKENS, usage["output"])
425            if self._capture_genai_content:
426                text = _text_from_llm_result(response)
427                if text:
428                    span.add_event(
429                        EVENT_GEN_AI_ASSISTANT_MESSAGE,
430                        {"content": text[:4000]},
431                    )
432        self._end(run_id)
433
434    def on_llm_error(
435        self,
436        error: BaseException,
437        *,
438        run_id: UUID,
439        parent_run_id: UUID | None = None,
440        **kwargs: Any,
441    ) -> Any:
442        self._end(run_id, error=error)
443
444    def on_tool_start(
445        self,
446        serialized: dict[str, Any] | None,
447        input_str: str,
448        *,
449        run_id: UUID,
450        parent_run_id: UUID | None = None,
451        tags: list[str] | None = None,
452        metadata: dict[str, Any] | None = None,
453        inputs: dict[str, Any] | None = None,
454        **kwargs: Any,
455    ) -> Any:
456        serialized = serialized or {}
457        tool_name = str(
458            serialized.get("name") or kwargs.get("name") or "tool"
459        ).strip() or "tool"
460        attrs: dict[str, AttributeValue] = {
461            ATTR_AGENT_FRAMEWORK: "langgraph",
462            ATTR_GEN_AI_OP_NAME: GEN_AI_OP_EXECUTE_TOOL,
463            ATTR_AGENT_ROLE: "tool",
464            ATTR_AGENT_STAGE: "call",
465            ATTR_AGENT_TOOL_NAME: tool_name,
466            ATTR_GEN_AI_TOOL_NAME: tool_name,
467            ATTR_LG_RUN_ID: str(run_id),
468        }
469        attrs.update(self._contract_attrs(self._active_langgraph_state()))
470        span = self._start(
471            span_name_execute_tool(tool_name),
472            run_id,
473            attributes=attrs,
474            parent_run_id=parent_run_id,
475        )
476        if span is not None and self._capture_genai_content:
477            payload = ""
478            if inputs is not None:
479                try:
480                    import json
481
482                    payload = json.dumps(inputs, default=str)
483                except Exception:
484                    payload = str(inputs)
485            elif input_str:
486                payload = str(input_str)
487            if payload:
488                trimmed = payload[:8192]
489                span.set_attribute(ATTR_TOOL_INPUT_PAYLOAD, trimmed)
490                span.set_attribute(
491                    ATTR_TOOL_INPUT_PAYLOAD_PREVIEW,
492                    trimmed[:512],
493                )
494                span.add_event(
495                    EVENT_GEN_AI_TOOL_MESSAGE,
496                    {"content": trimmed[:4000], "role": "tool_input"},
497                )
498
499    def on_tool_end(
500        self,
501        output: Any,
502        *,
503        run_id: UUID,
504        parent_run_id: UUID | None = None,
505        **kwargs: Any,
506    ) -> Any:
507        span = self._spans.get(self._run_key(run_id))
508        if span is not None and self._capture_genai_content and output is not None:
509            text = str(output)
510            if text:
511                preview = text[:512]
512                span.set_attribute(ATTR_TOOL_OUTPUT_PAYLOAD_PREVIEW, preview)
513                span.add_event(
514                    EVENT_GEN_AI_TOOL_MESSAGE,
515                    {"content": text[:4000], "role": "tool"},
516                )
517        self._end(run_id)
518
519    def on_tool_error(
520        self,
521        error: BaseException,
522        *,
523        run_id: UUID,
524        parent_run_id: UUID | None = None,
525        **kwargs: Any,
526    ) -> Any:
527        span = self._spans.get(self._run_key(run_id))
528        if span is not None:
529            span.set_attribute(ATTR_AGENT_TOOL_IS_ERROR, True)
530        self._end(run_id, error=error)

LangChain callback handler that emits Parlot GenAI spans.

Translates LangGraph / LangChain execution events (LLM starts/ends, tool invocations, chain runs) into OpenTelemetry GenAI spans (invoke_agent, invoke_workflow, chat, execute_tool).

parlotize() registers this handler via LangChain configuration hooks, so manual callbacks=[...] attachment is not required. When LiveKit owns the active session, contract spans are suppressed and GenAI ops nest under the current OTel context.

ParlotLangGraphCallbackHandler( tracer: opentelemetry.trace.Tracer, *, capture_genai_content: bool = True)
144    def __init__(
145        self,
146        tracer: Tracer,
147        *,
148        capture_genai_content: bool = True,
149    ) -> None:
150        self._tracer = tracer
151        self._capture_genai_content = capture_genai_content
152        self._spans: dict[str, Span] = {}
153        self._root_runs: set[str] = set()
raise_error = False

Whether to raise an error if an exception occurs.

ignore_chain: bool
155    @property
156    def ignore_chain(self) -> bool:
157        return False

Whether to ignore chain callbacks.

ignore_llm: bool
159    @property
160    def ignore_llm(self) -> bool:
161        return False

Whether to ignore LLM callbacks.

ignore_agent: bool
163    @property
164    def ignore_agent(self) -> bool:
165        return False

Whether to ignore agent callbacks.

def on_chain_start( self, serialized: dict[str, typing.Any] | None, inputs: dict[str, typing.Any], *, run_id: uuid.UUID, parent_run_id: uuid.UUID | None = None, tags: list[str] | None = None, metadata: dict[str, typing.Any] | None = None, name: str | None = None, **kwargs: Any) -> Any:
243    def on_chain_start(
244        self,
245        serialized: dict[str, Any] | None,
246        inputs: dict[str, Any],
247        *,
248        run_id: UUID,
249        parent_run_id: UUID | None = None,
250        tags: list[str] | None = None,
251        metadata: dict[str, Any] | None = None,
252        name: str | None = None,
253        **kwargs: Any,
254    ) -> Any:
255        serialized = serialized or {}
256        chain_name = name or serialized.get("name") or serialized.get("id", ["chain"])[-1]
257        thread_id = self._thread_id(metadata=metadata, tags=tags, **kwargs)
258        state = ensure_session(thread_id) if thread_id or not livekit_owns_session() else None
259        if state is None and not livekit_owns_session():
260            state = ensure_session(thread_id or "")
261
262        is_root = parent_run_id is None
263        span_name = SPAN_GEN_AI_INVOKE_AGENT if is_root else SPAN_GEN_AI_INVOKE_WORKFLOW
264        op = GEN_AI_OP_INVOKE_AGENT if is_root else GEN_AI_OP_INVOKE_WORKFLOW
265        attrs: dict[str, AttributeValue] = {
266            ATTR_AGENT_FRAMEWORK: "langgraph",
267            ATTR_GEN_AI_OP_NAME: op,
268            ATTR_AGENT_ROLE: "pipeline" if is_root else "pipeline",
269            ATTR_AGENT_STAGE: "node" if not is_root else "turn",
270            ATTR_LG_GRAPH_NAME: str(chain_name),
271            ATTR_LG_RUN_ID: str(run_id),
272        }
273        if not is_root:
274            attrs[ATTR_LG_NODE_NAME] = str(chain_name)
275        if is_root:
276            self._root_runs.add(str(run_id))
277            if state is not None and not livekit_owns_session():
278                user_text = _last_message_text(
279                    _messages_from_payload(inputs),
280                    roles={"human", "user"},
281                )
282                user_idx = emit_turn(
283                    state,
284                    role="user",
285                    utterance_text=user_text,
286                )
287                state.open_agent_turn_index = user_idx + 1
288        attrs.update(self._contract_attrs(state))
289
290        self._start(
291            span_name,
292            run_id,
293            attributes=attrs,
294            parent_run_id=parent_run_id,
295        )

Run when a chain starts running.

Arguments:
  • serialized: The serialized chain.
  • inputs: The inputs.
  • run_id: The ID of the current run.
  • parent_run_id: The ID of the parent run.
  • tags: The tags.
  • metadata: The metadata.
  • **kwargs: Additional keyword arguments.
def on_chain_end( self, outputs: dict[str, typing.Any], *, run_id: uuid.UUID, parent_run_id: uuid.UUID | None = None, **kwargs: Any) -> Any:
297    def on_chain_end(
298        self,
299        outputs: dict[str, Any],
300        *,
301        run_id: UUID,
302        parent_run_id: UUID | None = None,
303        **kwargs: Any,
304    ) -> Any:
305        key = str(run_id)
306        if key in self._root_runs:
307            state = self._active_langgraph_state()
308            if state is not None and not livekit_owns_session():
309                agent_text = _last_message_text(
310                    _messages_from_payload(outputs),
311                    roles={"ai", "assistant"},
312                )
313                emit_turn(
314                    state,
315                    role="agent",
316                    turn_index=state.open_agent_turn_index,
317                    utterance_text=agent_text,
318                )
319                state.open_agent_turn_index = None
320        self._end(run_id)
321        self._root_runs.discard(key)

Run when chain ends running.

Arguments:
  • outputs: The outputs of the chain.
  • run_id: The ID of the current run.
  • parent_run_id: The ID of the parent run.
  • **kwargs: Additional keyword arguments.
def on_chain_error( self, error: BaseException, *, run_id: uuid.UUID, parent_run_id: uuid.UUID | None = None, **kwargs: Any) -> Any:
323    def on_chain_error(
324        self,
325        error: BaseException,
326        *,
327        run_id: UUID,
328        parent_run_id: UUID | None = None,
329        **kwargs: Any,
330    ) -> Any:
331        key = str(run_id)
332        if key in self._root_runs:
333            state = self._active_langgraph_state()
334            if state is not None:
335                state.open_agent_turn_index = None
336        self._end(run_id, error=error)
337        self._root_runs.discard(key)

Run when chain errors.

Arguments:
  • error: The error that occurred.
  • run_id: The ID of the current run.
  • parent_run_id: The ID of the parent run.
  • **kwargs: Additional keyword arguments.
def on_llm_start( self, serialized: dict[str, typing.Any] | None, prompts: list[str], *, run_id: uuid.UUID, parent_run_id: uuid.UUID | None = None, tags: list[str] | None = None, metadata: dict[str, typing.Any] | None = None, **kwargs: Any) -> Any:
339    def on_llm_start(
340        self,
341        serialized: dict[str, Any] | None,
342        prompts: list[str],
343        *,
344        run_id: UUID,
345        parent_run_id: UUID | None = None,
346        tags: list[str] | None = None,
347        metadata: dict[str, Any] | None = None,
348        **kwargs: Any,
349    ) -> Any:
350        serialized = serialized or {}
351        metadata = metadata or {}
352        model = str(
353            metadata.get("ls_model_name")
354            or serialized.get("name")
355            or metadata.get("model")
356            or ""
357        ).strip()
358        provider = str(metadata.get("ls_provider") or "").strip()
359        attrs: dict[str, AttributeValue] = {
360            ATTR_AGENT_FRAMEWORK: "langgraph",
361            ATTR_GEN_AI_OP_NAME: GEN_AI_OP_CHAT,
362            ATTR_AGENT_ROLE: "llm",
363            ATTR_AGENT_STAGE: "node",
364            ATTR_LG_RUN_ID: str(run_id),
365        }
366        if model:
367            attrs[ATTR_GEN_AI_MODEL] = model
368        if provider:
369            attrs[ATTR_GEN_AI_PROVIDER] = provider
370        attrs.update(self._contract_attrs(self._active_langgraph_state()))
371        span = self._start(
372            span_name_chat(model or None),
373            run_id,
374            attributes=attrs,
375            parent_run_id=parent_run_id,
376        )
377        if span is not None and self._capture_genai_content and prompts:
378            span.add_event(
379                EVENT_GEN_AI_USER_MESSAGE,
380                {"content": prompts[-1][:4000]},
381            )

Run when LLM starts running.

!!! warning

This method is called for non-chat models (regular text completion LLMs). If
you're implementing a handler for a chat model, you should use
`on_chat_model_start` instead.
Arguments:
  • serialized: The serialized LLM.
  • prompts: The prompts.
  • run_id: The ID of the current run.
  • parent_run_id: The ID of the parent run.
  • tags: The tags.
  • metadata: The metadata.
  • **kwargs: Additional keyword arguments.
def on_chat_model_start( self, serialized: dict[str, typing.Any] | None, messages: list[list[typing.Any]], *, run_id: uuid.UUID, parent_run_id: uuid.UUID | None = None, tags: list[str] | None = None, metadata: dict[str, typing.Any] | None = None, **kwargs: Any) -> Any:
383    def on_chat_model_start(
384        self,
385        serialized: dict[str, Any] | None,
386        messages: list[list[Any]],
387        *,
388        run_id: UUID,
389        parent_run_id: UUID | None = None,
390        tags: list[str] | None = None,
391        metadata: dict[str, Any] | None = None,
392        **kwargs: Any,
393    ) -> Any:
394        prompts: list[str] = []
395        for batch in messages or []:
396            for msg in batch:
397                content = getattr(msg, "content", None)
398                if isinstance(content, str) and content:
399                    prompts.append(content)
400        self.on_llm_start(
401            serialized,
402            prompts,
403            run_id=run_id,
404            parent_run_id=parent_run_id,
405            tags=tags,
406            metadata=metadata,
407            **kwargs,
408        )

Run when a chat model starts running.

!!! warning

This method is called for chat models. If you're implementing a handler for
a non-chat model, you should use `on_llm_start` instead.

!!! note

When overriding this method, the signature **must** include the two
required positional arguments `serialized` and `messages`.  Avoid
using `*args` in your override — doing so causes an `IndexError`
in the fallback path when the callback system converts `messages`
to prompt strings for `on_llm_start`.  Always declare the
signature explicitly:

```python

def on_chat_model_start( self, serialized: dict[str, Any], messages: list[list[BaseMessage]], **kwargs: Any, ) -> None: raise NotImplementedError # triggers fallback to on_llm_start ```

Arguments:
  • serialized: The serialized chat model.
  • messages: The messages. Must be a list of message lists — this is a required positional argument and must be present in any override.
  • run_id: The ID of the current run.
  • parent_run_id: The ID of the parent run.
  • tags: The tags.
  • metadata: The metadata.
  • **kwargs: Additional keyword arguments.
def on_llm_end( self, response: Any, *, run_id: uuid.UUID, parent_run_id: uuid.UUID | None = None, **kwargs: Any) -> Any:
410    def on_llm_end(
411        self,
412        response: Any,
413        *,
414        run_id: UUID,
415        parent_run_id: UUID | None = None,
416        **kwargs: Any,
417    ) -> Any:
418        span = self._spans.get(self._run_key(run_id))
419        if span is not None:
420            usage = _usage_from_llm_result(response)
421            if usage.get("input"):
422                span.set_attribute(ATTR_GEN_AI_IN_TOKENS, usage["input"])
423            if usage.get("output"):
424                span.set_attribute(ATTR_GEN_AI_OUT_TOKENS, usage["output"])
425            if self._capture_genai_content:
426                text = _text_from_llm_result(response)
427                if text:
428                    span.add_event(
429                        EVENT_GEN_AI_ASSISTANT_MESSAGE,
430                        {"content": text[:4000]},
431                    )
432        self._end(run_id)

Run when LLM ends running.

Arguments:
  • response: The response which was generated.
  • run_id: The ID of the current run.
  • parent_run_id: The ID of the parent run.
  • tags: The tags.
  • **kwargs: Additional keyword arguments.
def on_llm_error( self, error: BaseException, *, run_id: uuid.UUID, parent_run_id: uuid.UUID | None = None, **kwargs: Any) -> Any:
434    def on_llm_error(
435        self,
436        error: BaseException,
437        *,
438        run_id: UUID,
439        parent_run_id: UUID | None = None,
440        **kwargs: Any,
441    ) -> Any:
442        self._end(run_id, error=error)

Run when LLM errors.

Arguments:
  • error: The error that occurred.
  • run_id: The ID of the current run.
  • parent_run_id: The ID of the parent run.
  • tags: The tags.
  • **kwargs: Additional keyword arguments.
def on_tool_start( self, serialized: dict[str, typing.Any] | None, input_str: str, *, run_id: uuid.UUID, parent_run_id: uuid.UUID | None = None, tags: list[str] | None = None, metadata: dict[str, typing.Any] | None = None, inputs: dict[str, typing.Any] | None = None, **kwargs: Any) -> Any:
444    def on_tool_start(
445        self,
446        serialized: dict[str, Any] | None,
447        input_str: str,
448        *,
449        run_id: UUID,
450        parent_run_id: UUID | None = None,
451        tags: list[str] | None = None,
452        metadata: dict[str, Any] | None = None,
453        inputs: dict[str, Any] | None = None,
454        **kwargs: Any,
455    ) -> Any:
456        serialized = serialized or {}
457        tool_name = str(
458            serialized.get("name") or kwargs.get("name") or "tool"
459        ).strip() or "tool"
460        attrs: dict[str, AttributeValue] = {
461            ATTR_AGENT_FRAMEWORK: "langgraph",
462            ATTR_GEN_AI_OP_NAME: GEN_AI_OP_EXECUTE_TOOL,
463            ATTR_AGENT_ROLE: "tool",
464            ATTR_AGENT_STAGE: "call",
465            ATTR_AGENT_TOOL_NAME: tool_name,
466            ATTR_GEN_AI_TOOL_NAME: tool_name,
467            ATTR_LG_RUN_ID: str(run_id),
468        }
469        attrs.update(self._contract_attrs(self._active_langgraph_state()))
470        span = self._start(
471            span_name_execute_tool(tool_name),
472            run_id,
473            attributes=attrs,
474            parent_run_id=parent_run_id,
475        )
476        if span is not None and self._capture_genai_content:
477            payload = ""
478            if inputs is not None:
479                try:
480                    import json
481
482                    payload = json.dumps(inputs, default=str)
483                except Exception:
484                    payload = str(inputs)
485            elif input_str:
486                payload = str(input_str)
487            if payload:
488                trimmed = payload[:8192]
489                span.set_attribute(ATTR_TOOL_INPUT_PAYLOAD, trimmed)
490                span.set_attribute(
491                    ATTR_TOOL_INPUT_PAYLOAD_PREVIEW,
492                    trimmed[:512],
493                )
494                span.add_event(
495                    EVENT_GEN_AI_TOOL_MESSAGE,
496                    {"content": trimmed[:4000], "role": "tool_input"},
497                )

Run when the tool starts running.

Arguments:
  • serialized: The serialized chain.
  • input_str: The input string.
  • run_id: The ID of the current run.
  • parent_run_id: The ID of the parent run.
  • tags: The tags.
  • metadata: The metadata.
  • inputs: The inputs.
  • **kwargs: Additional keyword arguments.
def on_tool_end( self, output: Any, *, run_id: uuid.UUID, parent_run_id: uuid.UUID | None = None, **kwargs: Any) -> Any:
499    def on_tool_end(
500        self,
501        output: Any,
502        *,
503        run_id: UUID,
504        parent_run_id: UUID | None = None,
505        **kwargs: Any,
506    ) -> Any:
507        span = self._spans.get(self._run_key(run_id))
508        if span is not None and self._capture_genai_content and output is not None:
509            text = str(output)
510            if text:
511                preview = text[:512]
512                span.set_attribute(ATTR_TOOL_OUTPUT_PAYLOAD_PREVIEW, preview)
513                span.add_event(
514                    EVENT_GEN_AI_TOOL_MESSAGE,
515                    {"content": text[:4000], "role": "tool"},
516                )
517        self._end(run_id)

Run when the tool ends running.

Arguments:
  • output: The output of the tool.
  • run_id: The ID of the current run.
  • parent_run_id: The ID of the parent run.
  • **kwargs: Additional keyword arguments.
def on_tool_error( self, error: BaseException, *, run_id: uuid.UUID, parent_run_id: uuid.UUID | None = None, **kwargs: Any) -> Any:
519    def on_tool_error(
520        self,
521        error: BaseException,
522        *,
523        run_id: UUID,
524        parent_run_id: UUID | None = None,
525        **kwargs: Any,
526    ) -> Any:
527        span = self._spans.get(self._run_key(run_id))
528        if span is not None:
529            span.set_attribute(ATTR_AGENT_TOOL_IS_ERROR, True)
530        self._end(run_id, error=error)

Run when tool errors.

Arguments:
  • error: The error that occurred.
  • run_id: The ID of the current run.
  • parent_run_id: The ID of the parent run.
  • **kwargs: Additional keyword arguments.
def add_platform_ref(kind: str, value: str, *, framework: str = 'custom') -> None:
 71def add_platform_ref(
 72    kind: str,
 73    value: str,
 74    *,
 75    framework: str = "custom",
 76) -> None:
 77    """
 78    Attach a searchable external ID to the active Parlot session.
 79
 80    Stamps ``platform.ref.{kind}`` (and the primary triple when this is the
 81    first ref) on the live session span so the session can be found by that
 82    value in Parlot search / resolve.
 83
 84    Args:
 85        kind: Identifier type (e.g. ``"crm_ticket"``, ``"order_number"``,
 86            ``"call_sid"``).
 87        value: Unique identifier value (e.g. ``"TKT-9921"``).
 88        framework: Originating framework name. Defaults to ``"custom"``.
 89
 90    Example::
 91
 92        from parlot.instrumentation.livekit import add_platform_ref
 93
 94        add_platform_ref("crm_ticket", "TKT-9")
 95    """
 96    kind = str(kind or "").strip()
 97    value = str(value or "").strip()
 98    framework = str(framework or "custom").strip() or "custom"
 99    if not kind or not value:
100        return
101    span = get_active_session_span()
102    if span is None:
103        return
104    stamp_platform_refs(span, [(framework, kind, value)])

Attach a searchable external ID to the active Parlot session.

Stamps platform.ref.{kind} (and the primary triple when this is the first ref) on the live session span so the session can be found by that value in Parlot search / resolve.

Arguments:
  • kind: Identifier type (e.g. "crm_ticket", "order_number", "call_sid").
  • value: Unique identifier value (e.g. "TKT-9921").
  • framework: Originating framework name. Defaults to "custom".

Example::

from parlot.instrumentation.livekit import add_platform_ref

add_platform_ref("crm_ticket", "TKT-9")
@contextmanager
def human_escalation(label: str | None = None) -> Iterator[NoneType]:
50@contextmanager
51def human_escalation(label: str | None = None) -> Iterator[None]:
52    """Mark the next participant who joins the active session as a human rep.
53
54    Args:
55        label: Optional role or team label for the incoming human representative.
56    """
57    token = _pending_escalation_label.set(label)
58    try:
59        yield
60    finally:
61        _pending_escalation_label.reset(token)

Mark the next participant who joins the active session as a human rep.

Arguments:
  • label: Optional role or team label for the incoming human representative.
def record_human_rep(participant_id: str, *, label: str | None = None) -> None:
19def record_human_rep(participant_id: str, *, label: str | None = None) -> None:
20    """
21    Mark a participant as a human representative.
22
23    Stamps ``session.topology.agents`` on the active session span and registers
24    the participant for ``turn.participant_role=human_rep`` on future turns.
25
26    Args:
27        participant_id: Participant identifier within the room/session.
28        label: Optional human-readable name (e.g. ``"Tier 2 Escalation Desk"``).
29    """
30    participant_id = str(participant_id or "").strip()
31    if not participant_id:
32        return
33
34    state = get_active_session()
35    if state is not None:
36        state.human_rep_participant_ids.add(participant_id)
37        entry: dict[str, str] = {"id": participant_id, "role": "human_rep"}
38        if label:
39            entry["label"] = label
40        if not any(a.get("id") == participant_id for a in state.topology_agents):
41            state.topology_agents.append(entry)
42
43    span = get_active_session_span()
44    if span is not None and state is not None and state.topology_agents:
45        span.set_attribute(
46            ATTR_SESSION_TOPOLOGY_AGENTS, json.dumps(state.topology_agents)
47        )

Mark a participant as a human representative.

Stamps session.topology.agents on the active session span and registers the participant for turn.participant_role=human_rep on future turns.

Arguments:
  • participant_id: Participant identifier within the room/session.
  • label: Optional human-readable name (e.g. "Tier 2 Escalation Desk").
def set_session_attribute(key: str, value: str | int | float | bool) -> None:
20def set_session_attribute(key: str, value: str | int | float | bool) -> None:
21    """
22    Stamp one custom attribute on the active session under ``session.metadata.*``.
23
24    Keys are normalized to ``session.metadata.<key>``. Values are stored as
25    strings on the live session span and remembered on session state so they
26    are also present on ``parlot.session.close``.
27
28    Args:
29        key: Attribute name. If not prefixed with ``session.metadata.``, the
30            prefix is added automatically.
31        value: Value to record (``str``, ``int``, ``float``, or ``bool``).
32    """
33    full_key = session_metadata_key(key)
34    if not full_key or full_key == ATTR_SESSION_METADATA_PREFIX:
35        return
36    str_value = value if isinstance(value, str) else str(value)
37
38    state = get_active_session()
39    if state is not None:
40        state.custom_metadata[full_key] = str_value
41
42    span = get_active_session_span()
43    if span is not None and hasattr(span, "set_attribute"):
44        span.set_attribute(full_key, str_value)

Stamp one custom attribute on the active session under session.metadata.*.

Keys are normalized to session.metadata.<key>. Values are stored as strings on the live session span and remembered on session state so they are also present on parlot.session.close.

Arguments:
  • key: Attribute name. If not prefixed with session.metadata., the prefix is added automatically.
  • value: Value to record (str, int, float, or bool).
def set_session_metadata(**pairs: str | int | float | bool) -> None:
47def set_session_metadata(**pairs: str | int | float | bool) -> None:
48    """
49    Attach custom key/value metadata to the active Parlot session.
50
51    Each keyword becomes ``session.metadata.<name>`` on the session span and
52    appears in session detail in the Parlot UI.
53
54    Args:
55        **pairs: Keyword metadata pairs (values ``str``, ``int``, ``float``,
56            or ``bool``).
57
58    Example::
59
60        from parlot.instrumentation.livekit import set_session_metadata
61
62        set_session_metadata(order_id="12345", crm_ticket="TKT-9")
63    """
64    for key, value in pairs.items():
65        set_session_attribute(key, value)

Attach custom key/value metadata to the active Parlot session.

Each keyword becomes session.metadata.<name> on the session span and appears in session detail in the Parlot UI.

Arguments:
  • **pairs: Keyword metadata pairs (values str, int, float, or bool).

Example::

from parlot.instrumentation.livekit import set_session_metadata

set_session_metadata(order_id="12345", crm_ticket="TKT-9")
def stamp_platform_refs(span: Any, refs: list[tuple[str, str, str]]) -> None:
28def stamp_platform_refs(
29    span: Any,
30    refs: list[tuple[str, str, str]],
31) -> None:
32    """Stamp ``platform.ref.*`` triples onto a span.
33
34    ``refs`` is a list of ``(framework, kind, value)`` tuples, e.g.
35    ``("livekit", "room_sid", "RM_abc")``. The first tuple is also written
36    to the canonical triple attributes (``platform.ref.framework/kind/value``)
37    so backends can pivot on a single primary ref.
38
39    Every tuple is also written as a flat ``platform.ref.{kind} = value`` key
40    for ingestion fallbacks.
41
42    Args:
43        span: A live OTel span (``set_attribute``) or a ``ReadableSpan`` with a
44            mutable ``_attributes`` dict.
45        refs: Non-empty list of reference triples.
46    """
47    if not refs:
48        return
49
50    def _write(key: str, value: str) -> None:
51        if hasattr(span, "set_attribute") and callable(span.set_attribute):
52            try:
53                span.set_attribute(key, value)
54                return
55            except Exception:
56                pass
57        try:
58            ParlotBaseProcessor._set(span, key, value)
59        except Exception:
60            return
61
62    fw, kind, val = refs[0]
63    _write(ATTR_PLATFORM_FRAMEWORK, fw)
64    _write(ATTR_PLATFORM_KIND, kind)
65    _write(ATTR_PLATFORM_VALUE, val)
66
67    for _fw_i, kind_i, val_i in refs:
68        _write(platform_ref_flat_key(kind_i), val_i)

Stamp platform.ref.* triples onto a span.

refs is a list of (framework, kind, value) tuples, e.g. ("livekit", "room_sid", "RM_abc"). The first tuple is also written to the canonical triple attributes (platform.ref.framework/kind/value) so backends can pivot on a single primary ref.

Every tuple is also written as a flat platform.ref.{kind} = value key for ingestion fallbacks.

Arguments:
  • span: A live OTel span (set_attribute) or a ReadableSpan with a mutable _attributes dict.
  • refs: Non-empty list of reference triples.