spanforge 2.0.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.
- spanforge/__init__.py +695 -0
- spanforge/_batch_exporter.py +322 -0
- spanforge/_cli.py +3081 -0
- spanforge/_hooks.py +340 -0
- spanforge/_server.py +953 -0
- spanforge/_span.py +1015 -0
- spanforge/_store.py +287 -0
- spanforge/_stream.py +654 -0
- spanforge/_trace.py +334 -0
- spanforge/_tracer.py +253 -0
- spanforge/actor.py +141 -0
- spanforge/alerts.py +464 -0
- spanforge/auto.py +181 -0
- spanforge/baseline.py +336 -0
- spanforge/config.py +460 -0
- spanforge/consent.py +227 -0
- spanforge/consumer.py +379 -0
- spanforge/core/__init__.py +5 -0
- spanforge/core/compliance_mapping.py +1060 -0
- spanforge/cost.py +597 -0
- spanforge/debug.py +514 -0
- spanforge/drift.py +488 -0
- spanforge/egress.py +63 -0
- spanforge/eval.py +575 -0
- spanforge/event.py +1052 -0
- spanforge/exceptions.py +246 -0
- spanforge/explain.py +181 -0
- spanforge/export/__init__.py +50 -0
- spanforge/export/append_only.py +342 -0
- spanforge/export/cloud.py +349 -0
- spanforge/export/datadog.py +495 -0
- spanforge/export/grafana.py +331 -0
- spanforge/export/jsonl.py +198 -0
- spanforge/export/otel_bridge.py +291 -0
- spanforge/export/otlp.py +817 -0
- spanforge/export/otlp_bridge.py +231 -0
- spanforge/export/redis_backend.py +282 -0
- spanforge/export/webhook.py +302 -0
- spanforge/exporters/__init__.py +29 -0
- spanforge/exporters/console.py +271 -0
- spanforge/exporters/jsonl.py +144 -0
- spanforge/hitl.py +297 -0
- spanforge/inspect.py +429 -0
- spanforge/integrations/__init__.py +39 -0
- spanforge/integrations/_pricing.py +277 -0
- spanforge/integrations/anthropic.py +388 -0
- spanforge/integrations/bedrock.py +306 -0
- spanforge/integrations/crewai.py +251 -0
- spanforge/integrations/gemini.py +349 -0
- spanforge/integrations/groq.py +444 -0
- spanforge/integrations/langchain.py +349 -0
- spanforge/integrations/llamaindex.py +370 -0
- spanforge/integrations/ollama.py +286 -0
- spanforge/integrations/openai.py +370 -0
- spanforge/integrations/together.py +485 -0
- spanforge/metrics.py +393 -0
- spanforge/metrics_export.py +342 -0
- spanforge/migrate.py +278 -0
- spanforge/model_registry.py +282 -0
- spanforge/models.py +407 -0
- spanforge/namespaces/__init__.py +215 -0
- spanforge/namespaces/audit.py +253 -0
- spanforge/namespaces/cache.py +209 -0
- spanforge/namespaces/chain.py +74 -0
- spanforge/namespaces/confidence.py +69 -0
- spanforge/namespaces/consent.py +85 -0
- spanforge/namespaces/cost.py +175 -0
- spanforge/namespaces/decision.py +135 -0
- spanforge/namespaces/diff.py +146 -0
- spanforge/namespaces/drift.py +79 -0
- spanforge/namespaces/eval_.py +232 -0
- spanforge/namespaces/fence.py +180 -0
- spanforge/namespaces/guard.py +104 -0
- spanforge/namespaces/hitl.py +92 -0
- spanforge/namespaces/latency.py +69 -0
- spanforge/namespaces/prompt.py +185 -0
- spanforge/namespaces/redact.py +172 -0
- spanforge/namespaces/template.py +197 -0
- spanforge/namespaces/tool_call.py +76 -0
- spanforge/namespaces/trace.py +1006 -0
- spanforge/normalizer.py +183 -0
- spanforge/presidio_backend.py +149 -0
- spanforge/processor.py +258 -0
- spanforge/prompt_registry.py +415 -0
- spanforge/py.typed +0 -0
- spanforge/redact.py +780 -0
- spanforge/sampling.py +500 -0
- spanforge/schemas/v1.0/schema.json +170 -0
- spanforge/schemas/v2.0/schema.json +536 -0
- spanforge/signing.py +1152 -0
- spanforge/stream.py +559 -0
- spanforge/testing.py +376 -0
- spanforge/trace.py +199 -0
- spanforge/types.py +696 -0
- spanforge/ulid.py +304 -0
- spanforge/validate.py +383 -0
- spanforge-2.0.0.dist-info/METADATA +1777 -0
- spanforge-2.0.0.dist-info/RECORD +101 -0
- spanforge-2.0.0.dist-info/WHEEL +4 -0
- spanforge-2.0.0.dist-info/entry_points.txt +5 -0
- spanforge-2.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""spanforge.integrations.langchain — LangChain callback handler.
|
|
2
|
+
|
|
3
|
+
Provides :class:`LLMSchemaCallbackHandler`, a LangChain-compatible
|
|
4
|
+
callback handler that records LLM and tool-call activity as SpanForge events.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from spanforge.integrations.langchain import LLMSchemaCallbackHandler
|
|
9
|
+
|
|
10
|
+
handler = LLMSchemaCallbackHandler(source="my-app", org_id="org-1")
|
|
11
|
+
chain = SomeChain(callbacks=[handler])
|
|
12
|
+
chain.run("What is 2+2?")
|
|
13
|
+
|
|
14
|
+
for event in handler.events:
|
|
15
|
+
print(event.to_json())
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
from spanforge.event import Event
|
|
24
|
+
from spanforge.ulid import generate as gen_ulid
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from uuid import UUID
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"LLMSchemaCallbackHandler",
|
|
31
|
+
"is_patched",
|
|
32
|
+
"unpatch",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Module resolver
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _require_langchain() -> Any: # noqa: ANN401
|
|
41
|
+
"""Return the LangChain callbacks module from whichever package is installed.
|
|
42
|
+
|
|
43
|
+
Tries ``langchain_core.callbacks`` first (preferred, modern API), then
|
|
44
|
+
falls back to the legacy ``langchain.callbacks`` module.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ImportError: If neither ``langchain_core`` nor ``langchain`` is installed.
|
|
48
|
+
"""
|
|
49
|
+
# Try modern langchain_core first. Import the parent module first so that
|
|
50
|
+
# a None sentinel in sys.modules propagates as ImportError correctly.
|
|
51
|
+
try:
|
|
52
|
+
import sys # noqa: PLC0415
|
|
53
|
+
|
|
54
|
+
import langchain_core # noqa: PLC0415
|
|
55
|
+
import langchain_core.callbacks # noqa: PLC0415, F401
|
|
56
|
+
return sys.modules["langchain_core.callbacks"]
|
|
57
|
+
except ImportError:
|
|
58
|
+
pass
|
|
59
|
+
# Fall back to legacy langchain package.
|
|
60
|
+
try:
|
|
61
|
+
import sys # noqa: PLC0415
|
|
62
|
+
|
|
63
|
+
import langchain # noqa: PLC0415
|
|
64
|
+
import langchain.callbacks # noqa: PLC0415, F401
|
|
65
|
+
return sys.modules["langchain.callbacks"]
|
|
66
|
+
except ImportError:
|
|
67
|
+
pass
|
|
68
|
+
raise ImportError(
|
|
69
|
+
"LangChain package is required for the spanforge LangChain integration.\n"
|
|
70
|
+
"Install it with: pip install 'spanforge[langchain]'"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Callback handler
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class LLMSchemaCallbackHandler:
|
|
80
|
+
"""LangChain callback handler that emits SpanForge events.
|
|
81
|
+
|
|
82
|
+
Compatible with both ``langchain_core`` and legacy ``langchain`` SDK
|
|
83
|
+
versions. Events are accumulated in :attr:`events` and optionally
|
|
84
|
+
forwarded to an async exporter.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
source: Value for ``Event.source`` (e.g. ``"my-llm-app@1.0.0"``).
|
|
88
|
+
org_id: Optional organisation identifier.
|
|
89
|
+
exporter: Optional async exporter; must have an ``export(event)``
|
|
90
|
+
coroutine method. When the event loop is running, export is
|
|
91
|
+
scheduled as a task; otherwise the call is silently skipped.
|
|
92
|
+
|
|
93
|
+
Attributes:
|
|
94
|
+
events: List of all :class:`~spanforge.event.Event` objects emitted by
|
|
95
|
+
this handler in chronological order.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
source: str,
|
|
101
|
+
*,
|
|
102
|
+
org_id: str | None = None,
|
|
103
|
+
exporter: Any | None = None, # noqa: ANN401
|
|
104
|
+
) -> None:
|
|
105
|
+
self._source = source
|
|
106
|
+
self._org_id = org_id
|
|
107
|
+
self._exporter = exporter
|
|
108
|
+
self.events: list[Event] = []
|
|
109
|
+
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
# Internal helpers
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def _make_event(self, event_type: str, payload: dict[str, Any]) -> Event:
|
|
115
|
+
"""Create a SpanForge event and optionally schedule async export.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
event_type: Dotted event type string.
|
|
119
|
+
payload: Event payload dict.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
The newly created :class:`~spanforge.event.Event`.
|
|
123
|
+
"""
|
|
124
|
+
event = Event(
|
|
125
|
+
event_type=event_type,
|
|
126
|
+
source=self._source,
|
|
127
|
+
org_id=self._org_id,
|
|
128
|
+
payload=payload,
|
|
129
|
+
event_id=gen_ulid(),
|
|
130
|
+
)
|
|
131
|
+
self.events.append(event)
|
|
132
|
+
|
|
133
|
+
if self._exporter is not None:
|
|
134
|
+
try:
|
|
135
|
+
loop = asyncio.get_running_loop()
|
|
136
|
+
loop.create_task(self._exporter.export(event)) # noqa: RUF006
|
|
137
|
+
except RuntimeError:
|
|
138
|
+
pass # no running event loop
|
|
139
|
+
|
|
140
|
+
return event
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
# LangChain callback interface
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def on_llm_start(
|
|
147
|
+
self,
|
|
148
|
+
serialized: dict[str, Any],
|
|
149
|
+
prompts: list[str],
|
|
150
|
+
*,
|
|
151
|
+
run_id: UUID | None = None,
|
|
152
|
+
**kwargs: Any, # noqa: ANN401
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Called when an LLM invocation begins.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
serialized: Serialised LLM config dict (contains ``id`` list).
|
|
158
|
+
prompts: List of prompt strings being sent to the LLM.
|
|
159
|
+
run_id: LangChain run identifier.
|
|
160
|
+
**kwargs: Additional keyword arguments (ignored).
|
|
161
|
+
"""
|
|
162
|
+
llm_name = ""
|
|
163
|
+
if serialized and "id" in serialized and serialized["id"]:
|
|
164
|
+
llm_name = str(serialized["id"][-1])
|
|
165
|
+
self._make_event(
|
|
166
|
+
"llm.trace.span.started",
|
|
167
|
+
{
|
|
168
|
+
"llm_name": llm_name,
|
|
169
|
+
"prompt_count": len(prompts),
|
|
170
|
+
"run_id": str(run_id) if run_id is not None else None,
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def on_llm_end(
|
|
175
|
+
self,
|
|
176
|
+
response: Any, # noqa: ANN401
|
|
177
|
+
*,
|
|
178
|
+
run_id: UUID | None = None,
|
|
179
|
+
**kwargs: Any, # noqa: ANN401
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Called when an LLM invocation completes.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
response: LangChain ``LLMResult`` object with ``llm_output`` attribute.
|
|
185
|
+
run_id: LangChain run identifier.
|
|
186
|
+
**kwargs: Additional keyword arguments (ignored).
|
|
187
|
+
"""
|
|
188
|
+
llm_output = getattr(response, "llm_output", None)
|
|
189
|
+
prompt_tokens: int | None = None
|
|
190
|
+
completion_tokens: int | None = None
|
|
191
|
+
total_tokens: int | None = None
|
|
192
|
+
|
|
193
|
+
if isinstance(llm_output, dict):
|
|
194
|
+
token_usage = llm_output.get("token_usage") or {}
|
|
195
|
+
if isinstance(token_usage, dict):
|
|
196
|
+
prompt_tokens = token_usage.get("prompt_tokens")
|
|
197
|
+
completion_tokens = token_usage.get("completion_tokens")
|
|
198
|
+
total_tokens = token_usage.get("total_tokens")
|
|
199
|
+
|
|
200
|
+
self._make_event(
|
|
201
|
+
"llm.trace.span.completed",
|
|
202
|
+
{
|
|
203
|
+
"run_id": str(run_id) if run_id is not None else None,
|
|
204
|
+
"prompt_tokens": prompt_tokens,
|
|
205
|
+
"completion_tokens": completion_tokens,
|
|
206
|
+
"total_tokens": total_tokens,
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def on_llm_error(
|
|
211
|
+
self,
|
|
212
|
+
error: BaseException,
|
|
213
|
+
*,
|
|
214
|
+
run_id: UUID | None = None,
|
|
215
|
+
**kwargs: Any, # noqa: ANN401
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Called when an LLM invocation raises an error.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
error: The exception that was raised.
|
|
221
|
+
run_id: LangChain run identifier.
|
|
222
|
+
**kwargs: Additional keyword arguments (ignored).
|
|
223
|
+
"""
|
|
224
|
+
self._make_event(
|
|
225
|
+
"llm.trace.span.error",
|
|
226
|
+
{
|
|
227
|
+
"run_id": str(run_id) if run_id is not None else None,
|
|
228
|
+
"error": str(error),
|
|
229
|
+
"error_type": type(error).__name__,
|
|
230
|
+
},
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def on_tool_start(
|
|
234
|
+
self,
|
|
235
|
+
serialized: dict[str, Any],
|
|
236
|
+
input_str: str,
|
|
237
|
+
*,
|
|
238
|
+
run_id: UUID | None = None,
|
|
239
|
+
**kwargs: Any, # noqa: ANN401
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Called when a tool invocation begins.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
serialized: Serialised tool config dict (usually has ``"name"`` key).
|
|
245
|
+
input_str: String input passed to the tool.
|
|
246
|
+
run_id: LangChain run identifier.
|
|
247
|
+
**kwargs: Additional keyword arguments (ignored).
|
|
248
|
+
"""
|
|
249
|
+
tool_name = serialized.get("name", "") if serialized else ""
|
|
250
|
+
self._make_event(
|
|
251
|
+
"llm.trace.tool_call.started",
|
|
252
|
+
{
|
|
253
|
+
"tool_name": tool_name,
|
|
254
|
+
"input": input_str,
|
|
255
|
+
"run_id": str(run_id) if run_id is not None else None,
|
|
256
|
+
},
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def on_tool_end(
|
|
260
|
+
self,
|
|
261
|
+
output: str,
|
|
262
|
+
*,
|
|
263
|
+
run_id: UUID | None = None,
|
|
264
|
+
**kwargs: Any, # noqa: ANN401
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Called when a tool invocation completes.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
output: String output returned by the tool.
|
|
270
|
+
run_id: LangChain run identifier.
|
|
271
|
+
**kwargs: Additional keyword arguments (ignored).
|
|
272
|
+
"""
|
|
273
|
+
self._make_event(
|
|
274
|
+
"llm.trace.tool_call.completed",
|
|
275
|
+
{
|
|
276
|
+
"run_id": str(run_id) if run_id is not None else None,
|
|
277
|
+
"output": str(output)[:1024] if output else None,
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def on_tool_error(
|
|
282
|
+
self,
|
|
283
|
+
error: BaseException,
|
|
284
|
+
*,
|
|
285
|
+
run_id: UUID | None = None,
|
|
286
|
+
**kwargs: Any, # noqa: ANN401
|
|
287
|
+
) -> None:
|
|
288
|
+
"""Called when a tool invocation raises an error.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
error: The exception that was raised.
|
|
292
|
+
run_id: LangChain run identifier.
|
|
293
|
+
**kwargs: Additional keyword arguments (ignored).
|
|
294
|
+
"""
|
|
295
|
+
self._make_event(
|
|
296
|
+
"llm.trace.tool_call.error",
|
|
297
|
+
{
|
|
298
|
+
"run_id": str(run_id) if run_id is not None else None,
|
|
299
|
+
"error": str(error),
|
|
300
|
+
"error_type": type(error).__name__,
|
|
301
|
+
},
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def clear_events(self) -> None:
|
|
305
|
+
"""Remove all accumulated events from :attr:`events`."""
|
|
306
|
+
self.events.clear()
|
|
307
|
+
|
|
308
|
+
# ------------------------------------------------------------------
|
|
309
|
+
# dunder
|
|
310
|
+
# ------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
def __repr__(self) -> str:
|
|
313
|
+
return (
|
|
314
|
+
f"LLMSchemaCallbackHandler("
|
|
315
|
+
f"source={self._source!r}, "
|
|
316
|
+
f"events={len(self.events)})"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
# Module-level patch / unpatch API (for API consistency with other integrations)
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
def is_patched() -> bool:
|
|
325
|
+
"""Return ``True`` if the LangChain integration module is importable.
|
|
326
|
+
|
|
327
|
+
LangChain uses a callback-handler pattern rather than monkey-patching, so
|
|
328
|
+
there is no global state to check. This function simply verifies that the
|
|
329
|
+
LangChain package is available and the handler class is ready to use.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
``True`` when LangChain is installed and the handler can be created.
|
|
333
|
+
"""
|
|
334
|
+
try:
|
|
335
|
+
_require_langchain()
|
|
336
|
+
return True
|
|
337
|
+
except ImportError:
|
|
338
|
+
return False
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def unpatch() -> None:
|
|
342
|
+
"""No-op for the LangChain integration.
|
|
343
|
+
|
|
344
|
+
LangChain uses an explicit callback handler (not monkey-patching), so
|
|
345
|
+
there is nothing to undo globally. Remove the handler from any
|
|
346
|
+
``CallbackManager`` or chain you attached it to::
|
|
347
|
+
|
|
348
|
+
chain = SomeChain(callbacks=[]) # re-create without the handler
|
|
349
|
+
"""
|