otelzero 0.2.0__py3-none-any.whl
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.
- otelzero/__init__.py +62 -0
- otelzero/__main__.py +140 -0
- otelzero/context.py +86 -0
- otelzero/detection.py +58 -0
- otelzero/hooks/__init__.py +96 -0
- otelzero/hooks/_content.py +865 -0
- otelzero/hooks/_langsmith.py +504 -0
- otelzero/hooks/_native.py +517 -0
- otelzero/hooks/_usage.py +166 -0
- otelzero/hooks/adk.py +128 -0
- otelzero/hooks/base.py +546 -0
- otelzero/hooks/claude_sdk.py +267 -0
- otelzero/hooks/crewai.py +338 -0
- otelzero/hooks/databricks.py +260 -0
- otelzero/hooks/langchain.py +56 -0
- otelzero/hooks/langgraph.py +57 -0
- otelzero/hooks/llama_index.py +724 -0
- otelzero/hooks/microsoft.py +160 -0
- otelzero/hooks/openai_agents.py +313 -0
- otelzero/hooks/pydantic_ai.py +173 -0
- otelzero/hooks/smolagents.py +331 -0
- otelzero/hooks/snowflake.py +271 -0
- otelzero/hooks/strands.py +173 -0
- otelzero/otel.py +372 -0
- otelzero/semconv.py +327 -0
- otelzero/telemetry.py +238 -0
- otelzero/translate/__init__.py +214 -0
- otelzero/translate/langsmith.py +401 -0
- otelzero-0.2.0.dist-info/METADATA +1345 -0
- otelzero-0.2.0.dist-info/RECORD +33 -0
- otelzero-0.2.0.dist-info/WHEEL +5 -0
- otelzero-0.2.0.dist-info/licenses/LICENSE +202 -0
- otelzero-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Databricks Agent Bricks / Mosaic AI instrumentation for MLflow agents."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import wrapt
|
|
6
|
+
|
|
7
|
+
from otelzero import semconv
|
|
8
|
+
from otelzero.hooks import _content
|
|
9
|
+
from otelzero.hooks._usage import _field, extract_usage
|
|
10
|
+
from otelzero.hooks.base import _ImportInstrumenter, install_hook, trace_agent_run
|
|
11
|
+
|
|
12
|
+
# Cap free-text task detail (tool arguments / output) so a large payload can't
|
|
13
|
+
# bloat every task event; mirrors the [:100] truncation the other hooks use.
|
|
14
|
+
_MAX_DETAIL = 200
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _truncate(value):
|
|
18
|
+
"""Coerce to a short string for a telemetry ``details`` field (or ``None``)."""
|
|
19
|
+
if value is None:
|
|
20
|
+
return None
|
|
21
|
+
text = value if isinstance(value, str) else str(value)
|
|
22
|
+
return text if len(text) <= _MAX_DETAIL else text[:_MAX_DETAIL] + "…"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _first_present(obj, *keys):
|
|
26
|
+
"""First of ``keys`` that reads as a non-``None`` field off ``obj``."""
|
|
27
|
+
for key in keys:
|
|
28
|
+
value = _field(obj, key)
|
|
29
|
+
if value is not None:
|
|
30
|
+
return value
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _emit_tool_task_events(telemetry, agent_name, response):
|
|
35
|
+
"""Surface each tool a ResponsesAgent called as a otelzero task on its timeline.
|
|
36
|
+
|
|
37
|
+
A ``ResponsesAgent`` records its tool calls in ``response.output`` as
|
|
38
|
+
``function_call`` items, with results in matching ``function_call_output``
|
|
39
|
+
items (paired by ``call_id``). Each call is a unit of work the run did, so we
|
|
40
|
+
emit a task_start/task_end pair for it -- retrospectively, since the tool has
|
|
41
|
+
already executed inside ``predict`` (the ``with`` body is empty; we're
|
|
42
|
+
recording the completed call, not wrapping a live one). ``agent_role`` is set
|
|
43
|
+
to the agent name so the task attaches to that agent's card rather than
|
|
44
|
+
spawning its own (see ``kiji_agent_registry.db``'s task-event guard).
|
|
45
|
+
|
|
46
|
+
Responses without an ``output`` list -- a plain-text answer, or a ``ChatAgent``
|
|
47
|
+
(whose response carries ``messages``, not ``output``) -- yield no tasks.
|
|
48
|
+
"""
|
|
49
|
+
output = _field(response, "output")
|
|
50
|
+
if not isinstance(output, (list, tuple)) or not output:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
# Index tool results by call_id so each task_end can report its outcome.
|
|
54
|
+
results = {
|
|
55
|
+
_field(it, "call_id"): it
|
|
56
|
+
for it in output
|
|
57
|
+
if _field(it, "type") == "function_call_output"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for item in output:
|
|
61
|
+
if _field(item, "type") != "function_call":
|
|
62
|
+
continue
|
|
63
|
+
tool = _field(item, "name") or "tool"
|
|
64
|
+
call_id = _field(item, "call_id")
|
|
65
|
+
start_details = {
|
|
66
|
+
"tool": tool,
|
|
67
|
+
"call_id": call_id,
|
|
68
|
+
"arguments": _truncate(_field(item, "arguments")),
|
|
69
|
+
}
|
|
70
|
+
end_details = {"status": "success"}
|
|
71
|
+
result = results.get(call_id)
|
|
72
|
+
if result is not None:
|
|
73
|
+
end_details["output"] = _truncate(_field(result, "output"))
|
|
74
|
+
|
|
75
|
+
with trace_agent_run(
|
|
76
|
+
telemetry,
|
|
77
|
+
semconv.span_name(semconv.OP_EXECUTE_TOOL, tool),
|
|
78
|
+
{
|
|
79
|
+
semconv.GEN_AI_OPERATION_NAME: semconv.OP_EXECUTE_TOOL,
|
|
80
|
+
semconv.GEN_AI_AGENT_NAME: agent_name,
|
|
81
|
+
semconv.GEN_AI_TOOL_NAME: tool,
|
|
82
|
+
semconv.GEN_AI_TOOL_CALL_ID: call_id,
|
|
83
|
+
},
|
|
84
|
+
start_event="task_start",
|
|
85
|
+
end_event="task_end",
|
|
86
|
+
error_event="task_error",
|
|
87
|
+
log_fields={"agent_name": agent_name, "agent_role": agent_name},
|
|
88
|
+
start_details=start_details,
|
|
89
|
+
end_details=end_details,
|
|
90
|
+
):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DatabricksImportHook(_ImportInstrumenter):
|
|
95
|
+
"""Instrument Databricks Agent Bricks / Mosaic AI agents.
|
|
96
|
+
|
|
97
|
+
Databricks agents (Agent Bricks, the Mosaic AI Agent Framework) are authored
|
|
98
|
+
as MLflow agent subclasses -- ``mlflow.pyfunc.ResponsesAgent`` (the current
|
|
99
|
+
recommended interface) or ``mlflow.pyfunc.ChatAgent`` (its predecessor).
|
|
100
|
+
Both live in ``mlflow.pyfunc.model``, both declare ``predict`` as an
|
|
101
|
+
*abstract* method each agent implements, and both drive subclass validation
|
|
102
|
+
through ``__init_subclass__`` -- so, unlike a concrete framework class, there
|
|
103
|
+
is no single method on the base to wrap. Instead this hook intercepts
|
|
104
|
+
subclass creation (``__init_subclass__``) for each base and wraps each
|
|
105
|
+
concrete agent's ``predict`` as it is defined (and any subclasses already
|
|
106
|
+
defined when the module loads), composing on top of MLflow's own validation
|
|
107
|
+
wrapper. Calling ``agent.predict(...)`` -- directly or via MLflow serving --
|
|
108
|
+
then streams agent telemetry with zero changes to the agent code.
|
|
109
|
+
|
|
110
|
+
Each ``predict`` call emits agent_start/agent_end/agent_error. On top of that,
|
|
111
|
+
the tools a ``ResponsesAgent`` invokes -- recorded in its response as
|
|
112
|
+
``function_call`` items (with results in matching ``function_call_output``
|
|
113
|
+
items) -- are surfaced as nested task_start/task_end events, one per tool, so
|
|
114
|
+
a run's tool steps show up on the agent's timeline (see
|
|
115
|
+
:func:`_emit_tool_task_events`).
|
|
116
|
+
|
|
117
|
+
Note: only ``predict`` is wrapped (the required entry point). ``predict_stream``
|
|
118
|
+
is left alone to avoid double-counting agents whose ``predict`` aggregates
|
|
119
|
+
``predict_stream`` internally.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
WATCHED = {"mlflow.pyfunc.model": "_wrap_agent_bases"}
|
|
123
|
+
|
|
124
|
+
# otelzero's own marker so we never double-wrap (MLflow already sets __wrapped__).
|
|
125
|
+
_MARKER = "_otelzero_wrapped"
|
|
126
|
+
|
|
127
|
+
# The MLflow agent-authoring bases to instrument, both in mlflow.pyfunc.model.
|
|
128
|
+
_AGENT_BASES = ("ResponsesAgent", "ChatAgent")
|
|
129
|
+
|
|
130
|
+
def _wrap_agent_bases(self, module):
|
|
131
|
+
for base_name in self._AGENT_BASES:
|
|
132
|
+
agent_base = getattr(module, base_name, None)
|
|
133
|
+
if agent_base is not None:
|
|
134
|
+
self._instrument_base(agent_base)
|
|
135
|
+
|
|
136
|
+
def _instrument_base(self, agent_base):
|
|
137
|
+
telemetry = self.telemetry
|
|
138
|
+
|
|
139
|
+
@wrapt.decorator
|
|
140
|
+
def predict_wrapper(wrapped, instance, args, kwargs):
|
|
141
|
+
agent_name = type(instance).__name__
|
|
142
|
+
# ChatAgent.predict(messages, ...) receives the message list
|
|
143
|
+
# directly; ResponsesAgent.predict(request) wraps it in
|
|
144
|
+
# ``request.input`` -- unwrap that so both count correctly.
|
|
145
|
+
arg = args[0] if args else kwargs.get("messages", kwargs.get("request"))
|
|
146
|
+
payload = getattr(arg, "input", arg)
|
|
147
|
+
try:
|
|
148
|
+
num_messages = len(payload)
|
|
149
|
+
except TypeError:
|
|
150
|
+
num_messages = 0
|
|
151
|
+
agent_info = {"num_messages": num_messages}
|
|
152
|
+
|
|
153
|
+
with trace_agent_run(
|
|
154
|
+
telemetry,
|
|
155
|
+
semconv.span_name(semconv.OP_INVOKE_AGENT, agent_name),
|
|
156
|
+
{
|
|
157
|
+
semconv.GEN_AI_OPERATION_NAME: semconv.OP_INVOKE_AGENT,
|
|
158
|
+
semconv.GEN_AI_AGENT_NAME: agent_name,
|
|
159
|
+
semconv.OTELZERO_NUM_MESSAGES: num_messages,
|
|
160
|
+
},
|
|
161
|
+
start_event="agent_start",
|
|
162
|
+
end_event="agent_end",
|
|
163
|
+
error_event="agent_error",
|
|
164
|
+
log_fields={"agent_name": agent_name, "agent_role": agent_name},
|
|
165
|
+
start_details=agent_info,
|
|
166
|
+
) as run:
|
|
167
|
+
# ChatAgent messages ({role, content}) or ResponsesAgent input
|
|
168
|
+
# items (message / function_call / function_call_output).
|
|
169
|
+
run.set_input(_content.messages_from(payload))
|
|
170
|
+
response = wrapped(*args, **kwargs)
|
|
171
|
+
# Surface the tools this run called as nested task events. Emitted
|
|
172
|
+
# inside the agent span (so they attach to its trace/timeline)
|
|
173
|
+
# and after predict returns (the calls have already run).
|
|
174
|
+
_emit_tool_task_events(telemetry, agent_name, response)
|
|
175
|
+
# Responses carry OpenAI-style usage (input/output tokens plus
|
|
176
|
+
# ``*_tokens_details.reasoning_tokens`` when reported).
|
|
177
|
+
run.set_usage(extract_usage(_field(response, "usage")))
|
|
178
|
+
# ChatAgentResponse.messages, or ResponsesAgentResponse.output
|
|
179
|
+
# items (the same message / tool item shapes as the input).
|
|
180
|
+
run.set_output(
|
|
181
|
+
_content.messages_from(
|
|
182
|
+
_first_present(response, "messages", "output"),
|
|
183
|
+
default_role="assistant",
|
|
184
|
+
)
|
|
185
|
+
or _content.text_of(response)
|
|
186
|
+
)
|
|
187
|
+
return response
|
|
188
|
+
|
|
189
|
+
marker = self._MARKER
|
|
190
|
+
|
|
191
|
+
def _wrap_predict(cls):
|
|
192
|
+
predict = cls.__dict__.get("predict")
|
|
193
|
+
if (
|
|
194
|
+
predict is None
|
|
195
|
+
or getattr(predict, "__isabstractmethod__", False)
|
|
196
|
+
or getattr(predict, marker, False)
|
|
197
|
+
):
|
|
198
|
+
return
|
|
199
|
+
wrapped = predict_wrapper(predict)
|
|
200
|
+
setattr(wrapped, marker, True)
|
|
201
|
+
cls.predict = wrapped
|
|
202
|
+
|
|
203
|
+
# Wrap concrete subclasses that already exist when the module loads ...
|
|
204
|
+
def _walk(cls):
|
|
205
|
+
for sub in cls.__subclasses__():
|
|
206
|
+
_wrap_predict(sub)
|
|
207
|
+
_walk(sub)
|
|
208
|
+
|
|
209
|
+
_walk(agent_base)
|
|
210
|
+
|
|
211
|
+
# ... and intercept every future subclass definition. ``__init_subclass__``
|
|
212
|
+
# is implicitly a classmethod; chain to the previous one (MLflow installs
|
|
213
|
+
# its data-validation wrapper there) before applying ours on top.
|
|
214
|
+
prev_init_subclass = agent_base.__dict__.get("__init_subclass__")
|
|
215
|
+
|
|
216
|
+
def _patched_init_subclass(cls, **kwargs):
|
|
217
|
+
if prev_init_subclass is not None:
|
|
218
|
+
prev_init_subclass.__func__(cls, **kwargs)
|
|
219
|
+
else:
|
|
220
|
+
super(agent_base, cls).__init_subclass__(**kwargs)
|
|
221
|
+
_wrap_predict(cls)
|
|
222
|
+
|
|
223
|
+
agent_base.__init_subclass__ = classmethod(_patched_init_subclass)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def install_databricks_monitoring(
|
|
227
|
+
server_url: str = "http://localhost:8000",
|
|
228
|
+
user: Optional[str] = None,
|
|
229
|
+
api_key: Optional[str] = None,
|
|
230
|
+
session: Optional[str] = None,
|
|
231
|
+
name: Optional[str] = None,
|
|
232
|
+
):
|
|
233
|
+
"""
|
|
234
|
+
Install Databricks Agent Bricks / Mosaic AI monitoring instrumentation.
|
|
235
|
+
|
|
236
|
+
Instruments MLflow ``ResponsesAgent`` and ``ChatAgent`` subclasses -- the
|
|
237
|
+
authoring interfaces for Databricks Agent Bricks and the Mosaic AI Agent
|
|
238
|
+
Framework -- by wrapping each concrete agent's ``predict`` as it is defined.
|
|
239
|
+
Every ``agent.predict(...)`` call (direct or via MLflow serving) streams
|
|
240
|
+
agent_start/agent_end/agent_error telemetry with zero changes to the agent
|
|
241
|
+
code. Call this *before* ``mlflow.pyfunc`` is imported (i.e. before defining
|
|
242
|
+
your agent subclass).
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
server_url: URL of the monitoring server
|
|
246
|
+
user: Name of the executing user
|
|
247
|
+
api_key: Ingest key tying events to an owner (for the iOS app)
|
|
248
|
+
session: Per-run id used to group events in the app
|
|
249
|
+
name: Explicit session title (else falls back to a codename)
|
|
250
|
+
"""
|
|
251
|
+
return install_hook(
|
|
252
|
+
DatabricksImportHook,
|
|
253
|
+
framework="Databricks Agents",
|
|
254
|
+
label="Databricks Agent Bricks",
|
|
255
|
+
server_url=server_url,
|
|
256
|
+
user=user,
|
|
257
|
+
api_key=api_key,
|
|
258
|
+
session=session,
|
|
259
|
+
name=name,
|
|
260
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""LangChain instrumentation: switch on LangSmith's *own* OpenTelemetry export.
|
|
2
|
+
|
|
3
|
+
LangChain traces itself through LangSmith, and ``langsmith[otel]`` exports
|
|
4
|
+
those traces as OpenTelemetry spans. Every chain, runnable, agent, model and
|
|
5
|
+
tool call becomes a span named after the run, carrying ``langsmith.span.kind``,
|
|
6
|
+
``gen_ai.operation.name``, ``gen_ai.request.model`` and ``gen_ai.usage.*``.
|
|
7
|
+
That is more than wrapping ``invoke`` from the outside could see, so this hook
|
|
8
|
+
wraps nothing: it points the export at the registry and derives otelzero's
|
|
9
|
+
events from it. See :mod:`otelzero.hooks._langsmith` for the details, shared
|
|
10
|
+
with :mod:`otelzero.hooks.langgraph`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from otelzero.hooks import _langsmith
|
|
16
|
+
from otelzero.telemetry import TelemetryClient
|
|
17
|
+
|
|
18
|
+
FRAMEWORK = "LangChain"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def install_langchain_monitoring(
|
|
22
|
+
server_url: str = "http://localhost:8000",
|
|
23
|
+
user: Optional[str] = None,
|
|
24
|
+
api_key: Optional[str] = None,
|
|
25
|
+
session: Optional[str] = None,
|
|
26
|
+
name: Optional[str] = None,
|
|
27
|
+
) -> TelemetryClient:
|
|
28
|
+
"""
|
|
29
|
+
Install LangChain monitoring via LangSmith's native OpenTelemetry export.
|
|
30
|
+
|
|
31
|
+
Exports LangChain's own LangSmith spans to ``{server_url}/v1/traces`` (or
|
|
32
|
+
to ``OTEL_EXPORTER_OTLP_ENDPOINT`` when set), translated into the current
|
|
33
|
+
GenAI conventions on export (:mod:`otelzero.translate`), and
|
|
34
|
+
sends ``agent_start`` and ``agent_end`` (or ``agent_error``) for each
|
|
35
|
+
top-level run: a chain, a runnable, or a LangChain 1.0 ``create_agent``
|
|
36
|
+
agent (a LangGraph graph). May be called before or after ``langchain`` is
|
|
37
|
+
imported; requires ``langsmith[otel]`` and the ``otel`` extra.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
server_url: URL of the monitoring server
|
|
41
|
+
user: Name of the executing user
|
|
42
|
+
api_key: Ingest key tying events to an owner (for the iOS app)
|
|
43
|
+
session: Per-run id used to group events in the app; exported on every
|
|
44
|
+
span as ``gen_ai.conversation.id``
|
|
45
|
+
name: Explicit session title (else derived from the run's input)
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ImportError: ``langchain-core`` or ``langsmith`` is not installed.
|
|
49
|
+
RuntimeError: the OpenTelemetry SDK (the ``otel`` extra) is missing.
|
|
50
|
+
"""
|
|
51
|
+
return _langsmith.install(FRAMEWORK, server_url, user, api_key, session, name)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def uninstall_langchain_monitoring() -> None:
|
|
55
|
+
"""Stop the events; undo the LangSmith env vars the install set."""
|
|
56
|
+
_langsmith.uninstall()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""LangGraph instrumentation: switch on LangSmith's *own* OpenTelemetry export.
|
|
2
|
+
|
|
3
|
+
LangGraph traces itself through LangSmith like the rest of LangChain: a graph
|
|
4
|
+
run is a LangSmith run, each node and subgraph a child run, and every model and
|
|
5
|
+
tool call a run below those. With ``langsmith[otel]`` each becomes an
|
|
6
|
+
OpenTelemetry span. LangChain 1.0's ``create_agent`` and ``create_react_agent``
|
|
7
|
+
return such graphs. This hook wraps nothing: it points the export at the
|
|
8
|
+
registry and turns each graph run (the trace's root span) into
|
|
9
|
+
``agent_start`` / ``agent_end`` / ``agent_error``. Its nodes and subgraphs are
|
|
10
|
+
part of the run, not runs of their own. See :mod:`otelzero.hooks._langsmith`
|
|
11
|
+
for the details, shared with :mod:`otelzero.hooks.langchain`.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from otelzero.hooks import _langsmith
|
|
17
|
+
from otelzero.telemetry import TelemetryClient
|
|
18
|
+
|
|
19
|
+
FRAMEWORK = "LangGraph"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def install_langgraph_monitoring(
|
|
23
|
+
server_url: str = "http://localhost:8000",
|
|
24
|
+
user: Optional[str] = None,
|
|
25
|
+
api_key: Optional[str] = None,
|
|
26
|
+
session: Optional[str] = None,
|
|
27
|
+
name: Optional[str] = None,
|
|
28
|
+
) -> TelemetryClient:
|
|
29
|
+
"""
|
|
30
|
+
Install LangGraph monitoring via LangSmith's native OpenTelemetry export.
|
|
31
|
+
|
|
32
|
+
Exports LangGraph's own LangSmith spans to ``{server_url}/v1/traces`` (or
|
|
33
|
+
to ``OTEL_EXPORTER_OTLP_ENDPOINT`` when set), translated into the current
|
|
34
|
+
GenAI conventions on export (:mod:`otelzero.translate`), and
|
|
35
|
+
sends ``agent_start`` and ``agent_end`` (or ``agent_error``) for each graph
|
|
36
|
+
run. May be called before or after ``langgraph`` is imported; requires
|
|
37
|
+
``langsmith[otel]`` and the ``otel`` extra. Installing this and
|
|
38
|
+
``install_langchain_monitoring`` together still reports each run once.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
server_url: URL of the monitoring server
|
|
42
|
+
user: Name of the executing user
|
|
43
|
+
api_key: Ingest key tying events to an owner (for the iOS app)
|
|
44
|
+
session: Per-run id used to group events in the app; exported on every
|
|
45
|
+
span as ``gen_ai.conversation.id``
|
|
46
|
+
name: Explicit session title (else derived from the graph's input)
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ImportError: ``langchain-core`` or ``langsmith`` is not installed.
|
|
50
|
+
RuntimeError: the OpenTelemetry SDK (the ``otel`` extra) is missing.
|
|
51
|
+
"""
|
|
52
|
+
return _langsmith.install(FRAMEWORK, server_url, user, api_key, session, name)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def uninstall_langgraph_monitoring() -> None:
|
|
56
|
+
"""Stop the events; undo the LangSmith env vars the install set."""
|
|
57
|
+
_langsmith.uninstall()
|