struct-sdk 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: struct-sdk
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Struct agent observability SDK — auto-instruments AI agent frameworks with OpenTelemetry
5
5
  Project-URL: Homepage, https://struct.ai
6
6
  Project-URL: Documentation, https://struct.ai/docs
@@ -30,8 +30,9 @@ Provides-Extra: claude-agent-sdk
30
30
  Requires-Dist: claude-agent-sdk>=0.1.59; extra == 'claude-agent-sdk'
31
31
  Provides-Extra: demo
32
32
  Requires-Dist: langchain-anthropic>=0.3.0; extra == 'demo'
33
- Requires-Dist: langchain-core>=0.3.0; extra == 'demo'
33
+ Requires-Dist: langchain-core>=1.3.3; extra == 'demo'
34
34
  Requires-Dist: langchain-openai>=0.2.0; extra == 'demo'
35
+ Requires-Dist: langchain>=1.3.0; extra == 'demo'
35
36
  Requires-Dist: langgraph>=0.2.0; extra == 'demo'
36
37
  Requires-Dist: python-dotenv>=1.0.0; extra == 'demo'
37
38
  Provides-Extra: dev
@@ -40,7 +41,7 @@ Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
40
41
  Requires-Dist: pytest>=9.0.3; extra == 'dev'
41
42
  Requires-Dist: ruff>=0.5; extra == 'dev'
42
43
  Provides-Extra: langchain
43
- Requires-Dist: langchain-core>=0.2.0; extra == 'langchain'
44
+ Requires-Dist: langchain-core>=1.3.3; extra == 'langchain'
44
45
  Description-Content-Type: text/markdown
45
46
 
46
47
  # struct-sdk
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "struct-sdk"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Struct agent observability SDK — auto-instruments AI agent frameworks with OpenTelemetry"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -38,9 +38,10 @@ Issues = "https://struct.ai/support"
38
38
  [project.optional-dependencies]
39
39
  anthropic = ["anthropic>=0.30.0"]
40
40
  claude-agent-sdk = ["claude-agent-sdk>=0.1.59"]
41
- langchain = ["langchain-core>=0.2.0"]
41
+ langchain = ["langchain-core>=1.3.3"]
42
42
  demo = [
43
- "langchain-core>=0.3.0",
43
+ "langchain>=1.3.0",
44
+ "langchain-core>=1.3.3",
44
45
  "langchain-openai>=0.2.0",
45
46
  "langchain-anthropic>=0.3.0",
46
47
  "langgraph>=0.2.0",
@@ -64,10 +65,10 @@ dev = [
64
65
  [tool.uv]
65
66
  override-dependencies = [
66
67
  "cryptography>=46.0.7",
67
- "langgraph>=1.0.10,!=1.1.7",
68
- "langgraph-checkpoint>=4.0.0",
68
+ "langgraph>=1.2.0,<1.3.0",
69
+ "langgraph-checkpoint>=4.1.0,<5.0.0",
69
70
  "langchain-text-splitters>=1.1.2",
70
- "python-multipart>=0.0.26",
71
+ "python-multipart>=0.0.27",
71
72
  ]
72
73
 
73
74
  [tool.mypy]
@@ -116,11 +116,41 @@ def _create_common(
116
116
  extraction, error-path attribute writes) are wrapped in ``_safe`` so a
117
117
  failure inside instrumentation can never replace the user's response or
118
118
  mask the user's API exception.
119
+
120
+ Two paths:
121
+
122
+ 1. **Enrich** — when ``_current_langchain_chat_span`` is set, this call
123
+ is happening underneath a LangChain handler that's already created
124
+ a ``chat <model>`` span. We do NOT create our own span (that's the
125
+ duplicate-Anthropic-spans issue). Instead we attach HTTP-layer
126
+ attrs (the real provider msg_id, exact response_model, usage,
127
+ finish_reasons, error info on failure) onto the langchain span.
128
+ Pre-call attrs are skipped — LangChain already set them.
129
+
130
+ 2. **Standalone** — no LangChain in the picture. Create our own span
131
+ and set the full attribute set as before.
119
132
  """
120
- from struct_sdk.core import _safe
133
+ from struct_sdk.core import _safe, _current_langchain_chat_span
121
134
 
122
135
  model = kwargs.get("model", "unknown")
123
136
 
137
+ # Enrich path: a LangChain handler upstream already created a ``chat
138
+ # <model>`` span for this call. Attach Anthropic HTTP-layer detail to it
139
+ # without creating a duplicate span.
140
+ host_span = _current_langchain_chat_span.get(None)
141
+ if host_span is not None:
142
+ try:
143
+ result = yield f, args, kwargs
144
+ except Exception as e:
145
+ _safe(lambda: host_span.set_attribute("error.type", type(e).__name__),
146
+ site="anthropic.create.enrich.error_type")
147
+ raise
148
+ _safe(
149
+ lambda: _set_response_attrs(host_span, sdk, model, result, otel_logger),
150
+ site="anthropic.create.enrich.set_response_attrs",
151
+ )
152
+ return result # noqa: B901
153
+
124
154
  with tracer.start_as_current_span(
125
155
  f"chat {model}", kind=trace.SpanKind.CLIENT
126
156
  ) as span:
@@ -273,9 +303,16 @@ def _wrap_stream(original: Any, tracer: trace.Tracer, sdk: StructSDK, otel_logge
273
303
  if is_async:
274
304
  @functools.wraps(original)
275
305
  async def wrapper(*args: Any, **kwargs: Any) -> Any:
276
- from struct_sdk.core import _safe, _current_session_id
306
+ from struct_sdk.core import _safe, _current_session_id, _current_langchain_chat_span
277
307
  model = kwargs.get("model", "unknown")
278
308
 
309
+ # Enrich path: a LangChain handler upstream already owns a chat
310
+ # span for this call. Don't create a duplicate; just pass through.
311
+ # (Stream end-handling will set response attrs on the host span
312
+ # when the LangChain handler's on_llm_end fires.)
313
+ if _current_langchain_chat_span.get(None) is not None:
314
+ return await original(*args, **kwargs) if _is_coroutine(original) else original(*args, **kwargs)
315
+
279
316
  span: Optional[trace.Span] = None
280
317
 
281
318
  def start() -> None:
@@ -318,9 +355,14 @@ def _wrap_stream(original: Any, tracer: trace.Tracer, sdk: StructSDK, otel_logge
318
355
  else:
319
356
  @functools.wraps(original)
320
357
  def wrapper(*args: Any, **kwargs: Any) -> Any:
321
- from struct_sdk.core import _safe, _current_session_id
358
+ from struct_sdk.core import _safe, _current_session_id, _current_langchain_chat_span
322
359
  model = kwargs.get("model", "unknown")
323
360
 
361
+ # Enrich path: a LangChain handler upstream already owns a chat
362
+ # span for this call. Don't create a duplicate; just pass through.
363
+ if _current_langchain_chat_span.get(None) is not None:
364
+ return original(*args, **kwargs)
365
+
324
366
  span: Optional[trace.Span] = None
325
367
 
326
368
  def start() -> None:
@@ -47,6 +47,27 @@ _current_session_id: contextvars.ContextVar[Optional[str]] = contextvars.Context
47
47
  _current_conversation_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("_current_conversation_id", default=None)
48
48
  _current_agent_span: contextvars.ContextVar[Optional[trace.Span]] = contextvars.ContextVar("_current_agent_span", default=None)
49
49
 
50
+ # When the LangChain handler is creating a ``chat <model>`` span for an LLM
51
+ # call that LangChain will dispatch through a provider SDK (anthropic,
52
+ # openai, etc.) that we ALSO instrument, set this contextvar to the
53
+ # in-progress langchain chat span. Provider-SDK instrumentations check it
54
+ # at the top of their ``messages.create`` / equivalent wrapper:
55
+ #
56
+ # - If set: enrich the existing langchain span with HTTP-layer attributes
57
+ # (real provider response.id, exact retries, rate-limit headers, etc.)
58
+ # and SKIP creating their own span — there's already a span for this
59
+ # call, we just want to attach more data to it.
60
+ #
61
+ # - If not set: this is a standalone provider-SDK invocation (no LangChain
62
+ # in the picture); the provider instrumentation creates its own span as
63
+ # usual.
64
+ #
65
+ # This eliminates the duplicate-span / orphan-Anthropic-span problem while
66
+ # preserving both layers' data on a single span.
67
+ _current_langchain_chat_span: contextvars.ContextVar[Optional[trace.Span]] = contextvars.ContextVar(
68
+ "_current_langchain_chat_span", default=None
69
+ )
70
+
50
71
  # Pending tool_use ids keyed by tool name (FIFO per name).
51
72
  # Populated by the Anthropic monkey-patch when a chat response arrives with
52
73
  # tool_use blocks, consumed by @struct.tool() / struct.tool(...) when the
@@ -251,6 +251,7 @@ _AGENT_CLASSES = {
251
251
  # ``serialized`` is usually ``None`` for these, so we filter on run_name via a
252
252
  # denylist. Matches LangSmith's promotion heuristic.
253
253
  _INTERNAL_RUN_NAMES = {
254
+ # Runnable wiring/plumbing
254
255
  "RunnableSequence",
255
256
  "RunnableLambda",
256
257
  "RunnablePassthrough",
@@ -263,15 +264,31 @@ _INTERNAL_RUN_NAMES = {
263
264
  "RunnableEach",
264
265
  "RunnablePick",
265
266
  "RunnableGenerator",
267
+ # Prompt templates
266
268
  "Prompt",
267
269
  "ChatPromptTemplate",
268
270
  "PromptTemplate",
271
+ # langchain.agents (1.x) / legacy create_react_agent internal node names
269
272
  "agent",
270
273
  "tools",
271
274
  "call_model",
272
275
  "should_continue",
273
276
  "__start__",
274
277
  "__end__",
278
+ # Output parsers — invoked as Runnables but not agents. LangChain's
279
+ # ``langchain.agents.create_agent`` with ``ToolStrategy`` (or fallback
280
+ # from ``ProviderStrategy`` on models without native structured output)
281
+ # invokes these as a separate step and they fire on_chain_start.
282
+ "PydanticToolsParser",
283
+ "PydanticOutputParser",
284
+ "JsonOutputParser",
285
+ "JsonOutputToolsParser",
286
+ "JsonOutputKeyToolsParser",
287
+ "StrOutputParser",
288
+ "OutputParser",
289
+ "BaseOutputParser",
290
+ "OpenAIToolsAgentOutputParser",
291
+ "OpenAIFunctionsAgentOutputParser",
275
292
  }
276
293
 
277
294
  _INTERNAL_RUN_NAME_PREFIXES = (
@@ -281,17 +298,68 @@ _INTERNAL_RUN_NAME_PREFIXES = (
281
298
  )
282
299
 
283
300
 
301
+ # Threading-id metadata keys, in resolution order.
302
+ #
303
+ # ``thread_id`` is LangGraph's canonical name (checkpointer key); ``session_id``
304
+ # and ``conversation_id`` are the LangSmith conventions documented at
305
+ # https://docs.langchain.com/langsmith/threads — when users tag a run with any
306
+ # of these, we treat it as the conversation grouping key. This makes
307
+ # struct-sdk drop-in compatible for users following either naming.
308
+ _THREAD_KEYS: tuple[str, ...] = ("thread_id", "session_id", "conversation_id")
309
+
310
+
311
+ def _metadata_thread_id(metadata: Optional[dict[str, Any]]) -> Optional[str]:
312
+ """Pull the conversation/thread id from a LangChain ``metadata`` dict.
313
+
314
+ Reads ``thread_id`` first (LangGraph canonical), then falls back to
315
+ ``session_id`` and ``conversation_id`` (LangSmith conventions). Returns
316
+ ``None`` if none are present as non-empty strings, so callers can chain
317
+ to their own fallbacks (ambient ``_current_session_id``, fresh UUID).
318
+ """
319
+ if not metadata:
320
+ return None
321
+ for key in _THREAD_KEYS:
322
+ v = metadata.get(key)
323
+ if isinstance(v, str) and v:
324
+ return v
325
+ return None
326
+
327
+
284
328
  def _is_agent_chain(
285
329
  serialized: Optional[dict[str, Any]],
286
330
  run_type: Optional[str],
287
331
  run_name: Optional[str],
332
+ metadata: Optional[dict[str, Any]] = None,
288
333
  ) -> bool:
289
- """Only promote user-meaningful chains to ``invoke_agent`` spans."""
334
+ """Only promote user-meaningful chains to ``invoke_agent`` spans.
335
+
336
+ Decision order:
337
+
338
+ 1. Explicit ``run_type='agent'`` (legacy AgentExecutor) → agent.
339
+ 2. ``serialized`` class identifies a Pregel/CompiledStateGraph → agent.
340
+ 3. ``metadata['langgraph_node'] == run_name`` → INTERNAL Pregel node
341
+ (every internal step of a ``create_agent`` Pregel fires on_chain_start
342
+ with metadata.langgraph_node set to its node name; for real top-level
343
+ agents or sub-agents the names differ or langgraph_node is absent).
344
+ 4. Known LangChain plumbing run names (denylist) → not agent.
345
+ 5. Otherwise, if there's a run_name → agent (user-named chain).
346
+ """
290
347
  if run_type == "agent":
291
348
  return True
292
349
  cls = _extract_class_name(serialized)
293
350
  if cls in _AGENT_CLASSES:
294
351
  return True
352
+ # Internal Pregel node detection: LangGraph populates metadata with
353
+ # ``langgraph_node`` (and ``langgraph_step``) on every internal node
354
+ # callback. The run_name of an internal node matches its langgraph_node;
355
+ # for the top-level Pregel invocation, langgraph_node is absent; for a
356
+ # sub-agent invoked from a tool body, langgraph_node may be set BUT
357
+ # contains the *parent's* node name (e.g. "tools"), which differs from
358
+ # the sub-agent's own run_name. So equality is the discriminator.
359
+ if metadata and run_name:
360
+ lg_node = metadata.get("langgraph_node")
361
+ if isinstance(lg_node, str) and lg_node and lg_node == run_name:
362
+ return False
295
363
  if run_name:
296
364
  if run_name in _INTERNAL_RUN_NAMES:
297
365
  return False
@@ -526,7 +594,7 @@ class StructCallbackHandler(BaseCallbackHandler): # type: ignore[misc]
526
594
  key = str(run_id)
527
595
  parent_key = str(parent_run_id) if parent_run_id else None
528
596
 
529
- if not _is_agent_chain(serialized or {}, run_type, run_name):
597
+ if not _is_agent_chain(serialized or {}, run_type, run_name, metadata):
530
598
  # Skipped chain — record entry so descendants can walk the parent
531
599
  # chain and find the nearest agent ancestor's session id.
532
600
  effective_parent = self._resolve_parent(parent_key).span
@@ -590,10 +658,19 @@ class StructCallbackHandler(BaseCallbackHandler): # type: ignore[misc]
590
658
  # Don't set gen_ai.agent.id from session_id — the spec uses agent.id
591
659
  # for a stable agent-definition identifier, not per-invocation.
592
660
  span.set_attribute("gen_ai.conversation.id", session_id)
593
- if (
594
- parent_agent_session_id
595
- and parent_agent_session_id != session_id
596
- ):
661
+ # Always set parent_session_id when there's a parent agent — even
662
+ # if it matches our own session_id (which happens when the SDK's
663
+ # ambient ``_current_session_id`` propagates through nested
664
+ # invoke_agent spans, e.g. ``struct.agent(session_id=conv_id)``
665
+ # wrapping multiple inner agents).
666
+ #
667
+ # The attribute encodes "this span has a parent agent" as
668
+ # structural information — the UI uses it to decide whether to
669
+ # inline a subagent under its triggering tool call vs render it
670
+ # as a top-level turn. When the values match, this signals an
671
+ # *intentionally inlined* subagent; when they differ, the UI's
672
+ # existing "View sub-agent →" drill-in flow kicks in.
673
+ if parent_agent_session_id:
597
674
  span.set_attribute("struct.agent.parent_session_id", parent_agent_session_id)
598
675
 
599
676
  _safe(set_attrs, site="langchain.on_chain_start.start_attrs")
@@ -722,6 +799,17 @@ class StructCallbackHandler(BaseCallbackHandler): # type: ignore[misc]
722
799
 
723
800
  _safe(set_attrs, site="langchain.on_chat_model_start.start_attrs")
724
801
 
802
+ # Announce this langchain chat span to any provider-SDK instrumentation
803
+ # (anthropic, etc.) that runs UNDER LangChain — they'll enrich this
804
+ # span with HTTP-layer attrs instead of creating their own duplicate.
805
+ # The token is saved on the RunState so on_llm_end can reset it.
806
+ from struct_sdk.core import _current_langchain_chat_span
807
+ enrich_token = None
808
+ try:
809
+ enrich_token = _current_langchain_chat_span.set(span)
810
+ except Exception: # noqa: BLE001 — never fail the host call on instrumentation
811
+ enrich_token = None
812
+
725
813
  self._runs[key] = _RunState(
726
814
  span=span,
727
815
  effective_parent_span=span,
@@ -729,6 +817,7 @@ class StructCallbackHandler(BaseCallbackHandler): # type: ignore[misc]
729
817
  nearest_agent_session_id=self._inherited_agent_session_id(parent_key),
730
818
  nearest_agent_span=self._inherited_agent_span(parent_key),
731
819
  kind="llm",
820
+ enrich_token=enrich_token,
732
821
  )
733
822
 
734
823
  def on_llm_start(
@@ -761,13 +850,31 @@ class StructCallbackHandler(BaseCallbackHandler): # type: ignore[misc]
761
850
  parent_run_id: Optional[UUID] = None,
762
851
  **kwargs: Any,
763
852
  ) -> None:
764
- from struct_sdk.core import _safe
853
+ from struct_sdk.core import _safe, _current_langchain_chat_span
765
854
 
766
855
  r = self._runs.pop(str(run_id), None)
767
856
  if not r or not r.span:
857
+ # Even if the span was never created (telemetry-disabled fallback),
858
+ # we still need to reset the enrich-contextvar token so it doesn't
859
+ # leak into the next operation in this task.
860
+ if r is not None and r.enrich_token is not None:
861
+ _safe(
862
+ lambda: _current_langchain_chat_span.reset(r.enrich_token),
863
+ site="langchain.on_llm_end.reset_enrich_token",
864
+ )
768
865
  return
769
866
  span = r.span
770
867
 
868
+ # Reset the enrich-token contextvar BEFORE ending the span. Any
869
+ # post-end attribute set by provider-SDK instrumentation would race
870
+ # against ``span.end()`` and likely no-op anyway, so we close the
871
+ # door before we close the span.
872
+ if r.enrich_token is not None:
873
+ _safe(
874
+ lambda: _current_langchain_chat_span.reset(r.enrich_token),
875
+ site="langchain.on_llm_end.reset_enrich_token",
876
+ )
877
+
771
878
  def set_response_attrs() -> None:
772
879
  generations = getattr(response, "generations", None) or []
773
880
  first = generations[0][0] if generations and generations[0] else None
@@ -811,10 +918,20 @@ class StructCallbackHandler(BaseCallbackHandler): # type: ignore[misc]
811
918
  parent_run_id: Optional[UUID] = None,
812
919
  **kwargs: Any,
813
920
  ) -> None:
814
- from struct_sdk.core import _safe
921
+ from struct_sdk.core import _safe, _current_langchain_chat_span
815
922
 
816
923
  r = self._runs.pop(str(run_id), None)
817
- if not r or not r.span:
924
+ if not r:
925
+ return
926
+ # Always reset the enrich-token contextvar, even when there's no
927
+ # span — leaving it set would leak the (now-defunct) span into the
928
+ # next operation in this task.
929
+ if r.enrich_token is not None:
930
+ _safe(
931
+ lambda: _current_langchain_chat_span.reset(r.enrich_token),
932
+ site="langchain.on_llm_error.reset_enrich_token",
933
+ )
934
+ if not r.span:
818
935
  return
819
936
  span = r.span
820
937
  _safe(lambda: _record_error(span, error),
@@ -1134,8 +1251,9 @@ class StructCallbackHandler(BaseCallbackHandler): # type: ignore[misc]
1134
1251
  p = self._runs.get(parent_run_id)
1135
1252
  if p and p.session_id:
1136
1253
  return p.session_id
1137
- if metadata and isinstance(metadata.get("thread_id"), str) and metadata["thread_id"]:
1138
- return metadata["thread_id"]
1254
+ thread_id = _metadata_thread_id(metadata)
1255
+ if thread_id:
1256
+ return thread_id
1139
1257
  from struct_sdk.core import _current_session_id
1140
1258
  ambient = _current_session_id.get(None)
1141
1259
  if ambient:
@@ -1159,8 +1277,8 @@ class StructCallbackHandler(BaseCallbackHandler): # type: ignore[misc]
1159
1277
  against the nearest-agent-ancestor's session and assign a fresh
1160
1278
  UUID if they match.
1161
1279
  """
1162
- thread_id = metadata.get("thread_id") if metadata else None
1163
- if isinstance(thread_id, str) and thread_id:
1280
+ thread_id = _metadata_thread_id(metadata)
1281
+ if thread_id:
1164
1282
  if parent_agent_session_id and thread_id == parent_agent_session_id:
1165
1283
  return str(uuid.uuid4())
1166
1284
  return thread_id
@@ -1186,6 +1304,13 @@ class _RunState:
1186
1304
  "nearest_agent_session_id",
1187
1305
  "nearest_agent_span",
1188
1306
  "kind",
1307
+ # Only set on LLM / chat runs. Holds the ``contextvars.Token`` returned
1308
+ # by ``_current_langchain_chat_span.set(span)`` so on_llm_end /
1309
+ # on_llm_error can reset the contextvar. The contextvar's purpose:
1310
+ # tell provider-SDK instrumentations (anthropic, openai, etc.)
1311
+ # "you're running underneath this LangChain chat span — enrich it
1312
+ # with your HTTP-layer attrs, don't create a duplicate span."
1313
+ "enrich_token",
1189
1314
  )
1190
1315
 
1191
1316
  def __init__(
@@ -1197,6 +1322,7 @@ class _RunState:
1197
1322
  nearest_agent_session_id: Optional[str],
1198
1323
  nearest_agent_span: Optional[trace.Span],
1199
1324
  kind: str,
1325
+ enrich_token: Any = None,
1200
1326
  ) -> None:
1201
1327
  self.span = span
1202
1328
  self.effective_parent_span = effective_parent_span
@@ -1204,6 +1330,7 @@ class _RunState:
1204
1330
  self.nearest_agent_session_id = nearest_agent_session_id
1205
1331
  self.nearest_agent_span = nearest_agent_span
1206
1332
  self.kind = kind
1333
+ self.enrich_token = enrich_token
1207
1334
 
1208
1335
 
1209
1336
  class _ParentInfo:
@@ -1282,7 +1409,23 @@ def _set_llm_response_attrs(
1282
1409
 
1283
1410
  resp_id = getattr(message, "id", None) or resp_meta.get("id")
1284
1411
  if isinstance(resp_id, str):
1285
- span.set_attribute("gen_ai.response.id", resp_id)
1412
+ # If a provider-SDK instrumentation (e.g. struct-sdk-anthropic via
1413
+ # the enrich path) has already set gen_ai.response.id to the
1414
+ # real provider message id (e.g. ``msg_…``), don't clobber it
1415
+ # with LangChain's run UUID (``lc_run--…``). The provider id is
1416
+ # more useful for API-level debugging. LangChain's run UUID is
1417
+ # preserved under ``langchain.run.id`` for joining back to
1418
+ # LangSmith / LangChain run data.
1419
+ try:
1420
+ existing = (span.attributes or {}).get("gen_ai.response.id") \
1421
+ if hasattr(span, "attributes") else None
1422
+ except Exception: # noqa: BLE001
1423
+ existing = None
1424
+ if not existing:
1425
+ span.set_attribute("gen_ai.response.id", resp_id)
1426
+ elif resp_id != existing:
1427
+ # LangChain's id is distinct from the provider's — keep both.
1428
+ span.set_attribute("langchain.run.id", resp_id)
1286
1429
 
1287
1430
  if sdk.emit_events and otel_logger:
1288
1431
  _emit_choice_event(otel_logger, message, provider or "langchain", session_id, span)
File without changes
File without changes
File without changes