spanforge 1.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 +815 -0
- spanforge/_ansi.py +93 -0
- spanforge/_batch_exporter.py +409 -0
- spanforge/_cli.py +2094 -0
- spanforge/_cli_audit.py +639 -0
- spanforge/_cli_compliance.py +711 -0
- spanforge/_cli_cost.py +243 -0
- spanforge/_cli_ops.py +791 -0
- spanforge/_cli_phase11.py +356 -0
- spanforge/_hooks.py +337 -0
- spanforge/_server.py +1708 -0
- spanforge/_span.py +1036 -0
- spanforge/_store.py +288 -0
- spanforge/_stream.py +664 -0
- spanforge/_trace.py +335 -0
- spanforge/_tracer.py +254 -0
- spanforge/actor.py +141 -0
- spanforge/alerts.py +469 -0
- spanforge/auto.py +464 -0
- spanforge/baseline.py +335 -0
- spanforge/cache.py +635 -0
- spanforge/compliance.py +325 -0
- spanforge/config.py +532 -0
- spanforge/consent.py +228 -0
- spanforge/consumer.py +377 -0
- spanforge/core/__init__.py +5 -0
- spanforge/core/compliance_mapping.py +1254 -0
- spanforge/cost.py +600 -0
- spanforge/debug.py +548 -0
- spanforge/deprecations.py +205 -0
- spanforge/drift.py +482 -0
- spanforge/egress.py +58 -0
- spanforge/eval.py +648 -0
- spanforge/event.py +1064 -0
- spanforge/exceptions.py +240 -0
- spanforge/explain.py +178 -0
- spanforge/export/__init__.py +69 -0
- spanforge/export/append_only.py +337 -0
- spanforge/export/cloud.py +357 -0
- spanforge/export/datadog.py +497 -0
- spanforge/export/grafana.py +320 -0
- spanforge/export/jsonl.py +195 -0
- spanforge/export/openinference.py +158 -0
- spanforge/export/otel_bridge.py +294 -0
- spanforge/export/otlp.py +811 -0
- spanforge/export/otlp_bridge.py +233 -0
- spanforge/export/redis_backend.py +282 -0
- spanforge/export/siem_schema.py +98 -0
- spanforge/export/siem_splunk.py +264 -0
- spanforge/export/siem_syslog.py +212 -0
- spanforge/export/webhook.py +299 -0
- spanforge/exporters/__init__.py +30 -0
- spanforge/exporters/console.py +271 -0
- spanforge/exporters/jsonl.py +144 -0
- spanforge/exporters/sqlite.py +142 -0
- spanforge/gate.py +1150 -0
- spanforge/governance.py +181 -0
- spanforge/hitl.py +295 -0
- spanforge/http.py +187 -0
- spanforge/inspect.py +427 -0
- spanforge/integrations/__init__.py +45 -0
- spanforge/integrations/_pricing.py +280 -0
- spanforge/integrations/anthropic.py +388 -0
- spanforge/integrations/azure_openai.py +133 -0
- spanforge/integrations/bedrock.py +292 -0
- spanforge/integrations/crewai.py +251 -0
- spanforge/integrations/gemini.py +351 -0
- spanforge/integrations/groq.py +442 -0
- spanforge/integrations/langchain.py +349 -0
- spanforge/integrations/langgraph.py +306 -0
- spanforge/integrations/llamaindex.py +373 -0
- spanforge/integrations/ollama.py +287 -0
- spanforge/integrations/openai.py +368 -0
- spanforge/integrations/together.py +483 -0
- spanforge/io.py +214 -0
- spanforge/lint.py +322 -0
- spanforge/metrics.py +417 -0
- spanforge/metrics_export.py +343 -0
- spanforge/migrate.py +402 -0
- spanforge/model_registry.py +278 -0
- spanforge/models.py +389 -0
- spanforge/namespaces/__init__.py +254 -0
- spanforge/namespaces/audit.py +256 -0
- spanforge/namespaces/cache.py +237 -0
- spanforge/namespaces/chain.py +77 -0
- spanforge/namespaces/confidence.py +72 -0
- spanforge/namespaces/consent.py +92 -0
- spanforge/namespaces/cost.py +179 -0
- spanforge/namespaces/decision.py +143 -0
- spanforge/namespaces/diff.py +157 -0
- spanforge/namespaces/drift.py +80 -0
- spanforge/namespaces/eval_.py +251 -0
- spanforge/namespaces/feedback.py +241 -0
- spanforge/namespaces/fence.py +193 -0
- spanforge/namespaces/guard.py +105 -0
- spanforge/namespaces/hitl.py +91 -0
- spanforge/namespaces/latency.py +72 -0
- spanforge/namespaces/prompt.py +190 -0
- spanforge/namespaces/redact.py +173 -0
- spanforge/namespaces/retrieval.py +379 -0
- spanforge/namespaces/runtime_governance.py +494 -0
- spanforge/namespaces/template.py +208 -0
- spanforge/namespaces/tool_call.py +77 -0
- spanforge/namespaces/trace.py +1029 -0
- spanforge/normalizer.py +171 -0
- spanforge/plugins.py +82 -0
- spanforge/presidio_backend.py +349 -0
- spanforge/processor.py +258 -0
- spanforge/prompt_registry.py +418 -0
- spanforge/py.typed +0 -0
- spanforge/redact.py +914 -0
- spanforge/regression.py +192 -0
- spanforge/runtime_policy.py +159 -0
- spanforge/sampling.py +511 -0
- spanforge/schema.py +183 -0
- spanforge/schemas/v1.0/schema.json +170 -0
- spanforge/schemas/v2.0/schema.json +536 -0
- spanforge/sdk/__init__.py +625 -0
- spanforge/sdk/_base.py +584 -0
- spanforge/sdk/_base.pyi +71 -0
- spanforge/sdk/_exceptions.py +1096 -0
- spanforge/sdk/_types.py +2184 -0
- spanforge/sdk/alert.py +1514 -0
- spanforge/sdk/alert.pyi +56 -0
- spanforge/sdk/audit.py +1196 -0
- spanforge/sdk/audit.pyi +67 -0
- spanforge/sdk/cec.py +1215 -0
- spanforge/sdk/cec.pyi +37 -0
- spanforge/sdk/config.py +641 -0
- spanforge/sdk/config.pyi +55 -0
- spanforge/sdk/enterprise.py +714 -0
- spanforge/sdk/enterprise.pyi +79 -0
- spanforge/sdk/explain.py +170 -0
- spanforge/sdk/fallback.py +432 -0
- spanforge/sdk/feedback.py +351 -0
- spanforge/sdk/gate.py +874 -0
- spanforge/sdk/gate.pyi +51 -0
- spanforge/sdk/identity.py +2114 -0
- spanforge/sdk/identity.pyi +47 -0
- spanforge/sdk/lineage.py +175 -0
- spanforge/sdk/observe.py +1065 -0
- spanforge/sdk/observe.pyi +50 -0
- spanforge/sdk/operator.py +338 -0
- spanforge/sdk/pii.py +1473 -0
- spanforge/sdk/pii.pyi +119 -0
- spanforge/sdk/pipelines.py +458 -0
- spanforge/sdk/pipelines.pyi +39 -0
- spanforge/sdk/policy.py +930 -0
- spanforge/sdk/rag.py +594 -0
- spanforge/sdk/rbac.py +280 -0
- spanforge/sdk/registry.py +430 -0
- spanforge/sdk/registry.pyi +46 -0
- spanforge/sdk/scope.py +279 -0
- spanforge/sdk/secrets.py +293 -0
- spanforge/sdk/secrets.pyi +25 -0
- spanforge/sdk/security.py +560 -0
- spanforge/sdk/security.pyi +57 -0
- spanforge/sdk/trust.py +472 -0
- spanforge/sdk/trust.pyi +41 -0
- spanforge/secrets.py +799 -0
- spanforge/signing.py +1179 -0
- spanforge/stats.py +100 -0
- spanforge/stream.py +560 -0
- spanforge/testing.py +378 -0
- spanforge/testing_mocks.py +1052 -0
- spanforge/trace.py +199 -0
- spanforge/types.py +696 -0
- spanforge/ulid.py +300 -0
- spanforge/validate.py +379 -0
- spanforge-1.0.0.dist-info/METADATA +1509 -0
- spanforge-1.0.0.dist-info/RECORD +174 -0
- spanforge-1.0.0.dist-info/WHEEL +4 -0
- spanforge-1.0.0.dist-info/entry_points.txt +5 -0
- spanforge-1.0.0.dist-info/licenses/LICENSE +128 -0
spanforge/auto.py
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"""spanforge.auto — Automatic integration discovery and patching.
|
|
2
|
+
|
|
3
|
+
Call :func:`setup` to automatically detect and patch all SpanForge-supported
|
|
4
|
+
LLM libraries that are installed in the current environment. This eliminates
|
|
5
|
+
the need to manually import each integration module.
|
|
6
|
+
|
|
7
|
+
Usage \u2014 fastest path to value::
|
|
8
|
+
|
|
9
|
+
import spanforge.auto
|
|
10
|
+
spanforge.auto.setup() # patches everything installed
|
|
11
|
+
|
|
12
|
+
Or call explicitly for programmatic control::
|
|
13
|
+
|
|
14
|
+
from spanforge.auto import setup
|
|
15
|
+
patched = setup(verbose=True)
|
|
16
|
+
# patched = {"openai", "anthropic"}
|
|
17
|
+
|
|
18
|
+
Note:
|
|
19
|
+
----
|
|
20
|
+
:func:`setup` is **not** called automatically on import. You must call it
|
|
21
|
+
explicitly so that importing :mod:`spanforge` never silently monkey-patches
|
|
22
|
+
third-party libraries without your consent.
|
|
23
|
+
|
|
24
|
+
Supported libraries (patched when installed):
|
|
25
|
+
* **openai** — :mod:`spanforge.integrations.openai`
|
|
26
|
+
* **anthropic** — :mod:`spanforge.integrations.anthropic`
|
|
27
|
+
* **groq** — :mod:`spanforge.integrations.groq`
|
|
28
|
+
* **ollama** — :mod:`spanforge.integrations.ollama`
|
|
29
|
+
* **together** — :mod:`spanforge.integrations.together`
|
|
30
|
+
|
|
31
|
+
Callback-based integrations (register manually):
|
|
32
|
+
* **LangChain** — use :class:`~spanforge.integrations.langchain.LLMSchemaCallbackHandler`
|
|
33
|
+
* **LlamaIndex** — use :class:`~spanforge.integrations.llamaindex.LLMSchemaEventHandler`
|
|
34
|
+
* **CrewAI** — use :func:`~spanforge.integrations.crewai.patch`
|
|
35
|
+
|
|
36
|
+
Security note
|
|
37
|
+
-------------
|
|
38
|
+
Monkey-patching is only applied when the target library is already installed.
|
|
39
|
+
The patching flag ``_spanforge_patched`` prevents double-patching. Each
|
|
40
|
+
integration is wrapped in a ``try/except`` so a broken integration never
|
|
41
|
+
prevents the others from loading.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import importlib.util
|
|
47
|
+
import threading
|
|
48
|
+
import warnings
|
|
49
|
+
|
|
50
|
+
__all__ = ["patched_integrations", "setup", "teardown", "trace_rag"]
|
|
51
|
+
|
|
52
|
+
# Internal registry of successfully patched integrations (module name → patch fn).
|
|
53
|
+
_PATCHED: set[str] = set()
|
|
54
|
+
_PATCHED_LOCK = threading.Lock()
|
|
55
|
+
|
|
56
|
+
# Map of library import name → (integration module path, patch fn name, unpatch fn name)
|
|
57
|
+
_INTEGRATIONS: list[tuple[str, str, str, str]] = [
|
|
58
|
+
("openai", "spanforge.integrations.openai", "patch", "unpatch"),
|
|
59
|
+
("anthropic", "spanforge.integrations.anthropic", "patch", "unpatch"),
|
|
60
|
+
("groq", "spanforge.integrations.groq", "patch", "unpatch"),
|
|
61
|
+
("ollama", "spanforge.integrations.ollama", "patch", "unpatch"),
|
|
62
|
+
("together", "spanforge.integrations.together", "patch", "unpatch"),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# RAG auto-patch state (F-20)
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
# Stores original methods keyed by "<module>.<Class>.<method>" so teardown
|
|
70
|
+
# can restore them precisely.
|
|
71
|
+
_RAG_PATCHED: set[str] = set()
|
|
72
|
+
_RAG_PATCHED_LOCK = threading.Lock()
|
|
73
|
+
_RAG_ORIGINALS: dict[str, object] = {}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _patch_rag_llama_index() -> bool:
|
|
77
|
+
"""Monkey-patch LlamaIndex ``VectorIndexRetriever.retrieve`` (F-20)."""
|
|
78
|
+
_key = "llama_index.VectorIndexRetriever.retrieve"
|
|
79
|
+
try:
|
|
80
|
+
with warnings.catch_warnings():
|
|
81
|
+
warnings.simplefilter("ignore")
|
|
82
|
+
from llama_index.core.retrievers import VectorIndexRetriever # type: ignore[import]
|
|
83
|
+
except ImportError:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
with _RAG_PATCHED_LOCK:
|
|
87
|
+
if "llama_index" in _RAG_PATCHED:
|
|
88
|
+
return False
|
|
89
|
+
original = VectorIndexRetriever.retrieve
|
|
90
|
+
_RAG_ORIGINALS[_key] = original
|
|
91
|
+
|
|
92
|
+
def _sf_retrieve(self_, str_or_query_bundle, **kw): # type: ignore[override]
|
|
93
|
+
import time as _t
|
|
94
|
+
|
|
95
|
+
query_text = str(str_or_query_bundle)
|
|
96
|
+
session_id: str | None = None
|
|
97
|
+
try:
|
|
98
|
+
from spanforge.sdk import sf_rag
|
|
99
|
+
|
|
100
|
+
session_id = sf_rag.trace_query(
|
|
101
|
+
query=query_text,
|
|
102
|
+
retriever_name="VectorIndexRetriever",
|
|
103
|
+
)
|
|
104
|
+
except Exception: # nosec B110
|
|
105
|
+
pass
|
|
106
|
+
t0 = _t.monotonic()
|
|
107
|
+
result = original(self_, str_or_query_bundle, **kw)
|
|
108
|
+
latency_ms = (_t.monotonic() - t0) * 1000
|
|
109
|
+
if session_id is not None:
|
|
110
|
+
try:
|
|
111
|
+
from spanforge.sdk import sf_rag
|
|
112
|
+
|
|
113
|
+
chunk_count = len(result) if isinstance(result, (list, tuple)) else 0
|
|
114
|
+
sf_rag.trace_retrieval(
|
|
115
|
+
session_id=session_id,
|
|
116
|
+
chunks=[
|
|
117
|
+
{"chunk_id": str(i), "score": 0.0}
|
|
118
|
+
for i in range(chunk_count)
|
|
119
|
+
],
|
|
120
|
+
latency_ms=float(latency_ms),
|
|
121
|
+
)
|
|
122
|
+
except Exception: # nosec B110
|
|
123
|
+
pass
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
VectorIndexRetriever.retrieve = _sf_retrieve # type: ignore[method-assign]
|
|
127
|
+
_RAG_PATCHED.add("llama_index")
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _unpatch_rag_llama_index() -> bool:
|
|
132
|
+
"""Restore LlamaIndex ``VectorIndexRetriever.retrieve`` to original."""
|
|
133
|
+
_key = "llama_index.VectorIndexRetriever.retrieve"
|
|
134
|
+
try:
|
|
135
|
+
with warnings.catch_warnings():
|
|
136
|
+
warnings.simplefilter("ignore")
|
|
137
|
+
from llama_index.core.retrievers import VectorIndexRetriever # type: ignore[import]
|
|
138
|
+
except ImportError:
|
|
139
|
+
return False
|
|
140
|
+
with _RAG_PATCHED_LOCK:
|
|
141
|
+
if "llama_index" not in _RAG_PATCHED:
|
|
142
|
+
return False
|
|
143
|
+
original = _RAG_ORIGINALS.pop(_key, None)
|
|
144
|
+
if original is not None:
|
|
145
|
+
VectorIndexRetriever.retrieve = original # type: ignore[method-assign]
|
|
146
|
+
_RAG_PATCHED.discard("llama_index")
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _patch_rag_langchain() -> bool:
|
|
151
|
+
"""Monkey-patch LangChain ``BaseRetriever.invoke`` (F-20)."""
|
|
152
|
+
_key = "langchain_core.BaseRetriever.invoke"
|
|
153
|
+
try:
|
|
154
|
+
with warnings.catch_warnings():
|
|
155
|
+
warnings.simplefilter("ignore")
|
|
156
|
+
from langchain_core.retrievers import BaseRetriever # type: ignore[import]
|
|
157
|
+
except ImportError:
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
with _RAG_PATCHED_LOCK:
|
|
161
|
+
if "langchain" in _RAG_PATCHED:
|
|
162
|
+
return False
|
|
163
|
+
original = BaseRetriever.invoke
|
|
164
|
+
_RAG_ORIGINALS[_key] = original
|
|
165
|
+
|
|
166
|
+
def _sf_invoke(self_, input, config=None, **kw): # type: ignore[override]
|
|
167
|
+
import time as _t
|
|
168
|
+
|
|
169
|
+
query_text = str(input)
|
|
170
|
+
session_id: str | None = None
|
|
171
|
+
try:
|
|
172
|
+
from spanforge.sdk import sf_rag
|
|
173
|
+
|
|
174
|
+
session_id = sf_rag.trace_query(
|
|
175
|
+
query=query_text,
|
|
176
|
+
retriever_name=type(self_).__name__,
|
|
177
|
+
)
|
|
178
|
+
except Exception: # nosec B110
|
|
179
|
+
pass
|
|
180
|
+
t0 = _t.monotonic()
|
|
181
|
+
result = original(self_, input, config, **kw)
|
|
182
|
+
latency_ms = (_t.monotonic() - t0) * 1000
|
|
183
|
+
if session_id is not None:
|
|
184
|
+
try:
|
|
185
|
+
from spanforge.sdk import sf_rag
|
|
186
|
+
|
|
187
|
+
chunk_count = len(result) if isinstance(result, (list, tuple)) else 0
|
|
188
|
+
sf_rag.trace_retrieval(
|
|
189
|
+
session_id=session_id,
|
|
190
|
+
chunks=[
|
|
191
|
+
{"chunk_id": str(i), "score": 0.0}
|
|
192
|
+
for i in range(chunk_count)
|
|
193
|
+
],
|
|
194
|
+
latency_ms=float(latency_ms),
|
|
195
|
+
)
|
|
196
|
+
except Exception: # nosec B110
|
|
197
|
+
pass
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
BaseRetriever.invoke = _sf_invoke # type: ignore[method-assign]
|
|
201
|
+
_RAG_PATCHED.add("langchain")
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _unpatch_rag_langchain() -> bool:
|
|
206
|
+
"""Restore LangChain ``BaseRetriever.invoke`` to original."""
|
|
207
|
+
_key = "langchain_core.BaseRetriever.invoke"
|
|
208
|
+
try:
|
|
209
|
+
with warnings.catch_warnings():
|
|
210
|
+
warnings.simplefilter("ignore")
|
|
211
|
+
from langchain_core.retrievers import BaseRetriever # type: ignore[import]
|
|
212
|
+
except ImportError:
|
|
213
|
+
return False
|
|
214
|
+
with _RAG_PATCHED_LOCK:
|
|
215
|
+
if "langchain" not in _RAG_PATCHED:
|
|
216
|
+
return False
|
|
217
|
+
original = _RAG_ORIGINALS.pop(_key, None)
|
|
218
|
+
if original is not None:
|
|
219
|
+
BaseRetriever.invoke = original # type: ignore[method-assign]
|
|
220
|
+
_RAG_PATCHED.discard("langchain")
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _try_patch_integration(
|
|
225
|
+
lib_name: str, integration_module: str, patch_fn: str, verbose: bool
|
|
226
|
+
) -> bool:
|
|
227
|
+
"""Attempt to patch one integration; returns True if newly patched."""
|
|
228
|
+
try:
|
|
229
|
+
mod = importlib.import_module(integration_module)
|
|
230
|
+
getattr(mod, patch_fn)()
|
|
231
|
+
_PATCHED.add(lib_name)
|
|
232
|
+
if verbose:
|
|
233
|
+
print(f" {lib_name}: patched \u2713")
|
|
234
|
+
except Exception as exc:
|
|
235
|
+
warnings.warn(
|
|
236
|
+
f"spanforge.auto: failed to patch {lib_name!r}: {exc}",
|
|
237
|
+
UserWarning,
|
|
238
|
+
stacklevel=3,
|
|
239
|
+
)
|
|
240
|
+
if verbose:
|
|
241
|
+
print(f" {lib_name}: patch failed — {exc}")
|
|
242
|
+
return False
|
|
243
|
+
else:
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def setup(*, verbose: bool = False) -> set[str]:
|
|
248
|
+
"""Detect and patch all installed SpanForge-supported LLM libraries.
|
|
249
|
+
|
|
250
|
+
Iterates over supported integrations and calls their ``patch()`` function
|
|
251
|
+
if the underlying library is installed. Already-patched integrations are
|
|
252
|
+
skipped silently (idempotent).
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
verbose: When ``True``, print a status line for each integration
|
|
256
|
+
attempted.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Set of library names that were newly patched in this call (does not
|
|
260
|
+
include libraries already patched in previous calls).
|
|
261
|
+
|
|
262
|
+
Example::
|
|
263
|
+
|
|
264
|
+
from spanforge.auto import setup
|
|
265
|
+
patched = setup(verbose=True)
|
|
266
|
+
# openai patched ✓
|
|
267
|
+
# anthropic not installed, skipped
|
|
268
|
+
|
|
269
|
+
Note:
|
|
270
|
+
Callback-based integrations (LangChain, LlamaIndex, CrewAI) are not
|
|
271
|
+
auto-patched because they require manual handler registration. See
|
|
272
|
+
their respective integration guides.
|
|
273
|
+
"""
|
|
274
|
+
newly_patched: set[str] = set()
|
|
275
|
+
|
|
276
|
+
for lib_name, integration_module, patch_fn, _unpatch_fn in _INTEGRATIONS:
|
|
277
|
+
if lib_name in _PATCHED:
|
|
278
|
+
if verbose:
|
|
279
|
+
print(f" {lib_name}: already patched, skipped")
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
if importlib.util.find_spec(lib_name) is None:
|
|
283
|
+
if verbose:
|
|
284
|
+
print(f" {lib_name}: not installed, skipped")
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
if _try_patch_integration(lib_name, integration_module, patch_fn, verbose):
|
|
288
|
+
newly_patched.add(lib_name)
|
|
289
|
+
|
|
290
|
+
# Attempt RAG auto-patches (F-20) — best-effort, never raise.
|
|
291
|
+
for _rag_lib, _rag_patch_fn in [
|
|
292
|
+
("llama_index", _patch_rag_llama_index),
|
|
293
|
+
("langchain_core", _patch_rag_langchain),
|
|
294
|
+
]:
|
|
295
|
+
if importlib.util.find_spec(_rag_lib) is None:
|
|
296
|
+
if verbose:
|
|
297
|
+
print(f" {_rag_lib} (rag): not installed, skipped")
|
|
298
|
+
continue
|
|
299
|
+
try:
|
|
300
|
+
patched = _rag_patch_fn()
|
|
301
|
+
if patched:
|
|
302
|
+
newly_patched.add(_rag_lib + ":rag")
|
|
303
|
+
if verbose:
|
|
304
|
+
print(f" {_rag_lib} (rag): patched \u2713")
|
|
305
|
+
elif verbose:
|
|
306
|
+
print(f" {_rag_lib} (rag): already patched, skipped")
|
|
307
|
+
except Exception as exc:
|
|
308
|
+
warnings.warn(
|
|
309
|
+
f"spanforge.auto: failed to patch {_rag_lib!r} RAG: {exc}",
|
|
310
|
+
UserWarning,
|
|
311
|
+
stacklevel=2,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return newly_patched
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def teardown(*, verbose: bool = False) -> set[str]:
|
|
318
|
+
"""Unpatch all auto-patched integrations and reset the auto-patch registry.
|
|
319
|
+
|
|
320
|
+
Calls ``unpatch()`` on every integration that was patched via
|
|
321
|
+
:func:`setup`. Safe to call even if :func:`setup` was never called.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
verbose: When ``True``, print a status line for each integration.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Set of library names that were unpatched.
|
|
328
|
+
"""
|
|
329
|
+
unpatched: set[str] = set()
|
|
330
|
+
|
|
331
|
+
for lib_name, integration_module, _patch_fn, unpatch_fn in _INTEGRATIONS:
|
|
332
|
+
with _PATCHED_LOCK:
|
|
333
|
+
if lib_name not in _PATCHED:
|
|
334
|
+
continue
|
|
335
|
+
try:
|
|
336
|
+
mod = importlib.import_module(integration_module)
|
|
337
|
+
getattr(mod, unpatch_fn)()
|
|
338
|
+
with _PATCHED_LOCK:
|
|
339
|
+
_PATCHED.discard(lib_name)
|
|
340
|
+
unpatched.add(lib_name)
|
|
341
|
+
if verbose:
|
|
342
|
+
print(f" {lib_name}: unpatched \u2713")
|
|
343
|
+
except Exception as exc:
|
|
344
|
+
warnings.warn(
|
|
345
|
+
f"spanforge.auto: failed to unpatch {lib_name!r}: {exc}",
|
|
346
|
+
UserWarning,
|
|
347
|
+
stacklevel=2,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# Unpatch RAG integrations (F-20).
|
|
351
|
+
for _rag_lib, _rag_unpatch_fn in [
|
|
352
|
+
("llama_index", _unpatch_rag_llama_index),
|
|
353
|
+
("langchain_core", _unpatch_rag_langchain),
|
|
354
|
+
]:
|
|
355
|
+
try:
|
|
356
|
+
if _rag_unpatch_fn():
|
|
357
|
+
unpatched.add(_rag_lib + ":rag")
|
|
358
|
+
if verbose:
|
|
359
|
+
print(f" {_rag_lib} (rag): unpatched \u2713")
|
|
360
|
+
except Exception as exc:
|
|
361
|
+
warnings.warn(
|
|
362
|
+
f"spanforge.auto: failed to unpatch {_rag_lib!r} RAG: {exc}",
|
|
363
|
+
UserWarning,
|
|
364
|
+
stacklevel=2,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
return unpatched
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def patched_integrations() -> set[str]:
|
|
371
|
+
"""Return the set of library names currently patched via :func:`setup`.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Snapshot of the currently patched integration names.
|
|
375
|
+
"""
|
|
376
|
+
with _PATCHED_LOCK:
|
|
377
|
+
return set(_PATCHED)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# NOTE: setup() is NOT called automatically on import.
|
|
381
|
+
# Call spanforge.auto.setup() explicitly to patch installed integrations.
|
|
382
|
+
# This is intentional: importing spanforge should never monkey-patch
|
|
383
|
+
# third-party libraries without explicit user consent.
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ---------------------------------------------------------------------------
|
|
387
|
+
# trace_rag — decorator for manual RAG tracing (F-20)
|
|
388
|
+
# ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def trace_rag(func):
|
|
392
|
+
"""Decorator that automatically traces a RAG retrieval function via sf-rag.
|
|
393
|
+
|
|
394
|
+
Wraps any callable that accepts a query string as its first argument and
|
|
395
|
+
emits ``llm.rag.query`` + ``llm.rag.retrieved`` spans via
|
|
396
|
+
:mod:`spanforge.sdk.rag`. The raw query text is **never stored**; only
|
|
397
|
+
its SHA-256 hash is recorded.
|
|
398
|
+
|
|
399
|
+
Usage::
|
|
400
|
+
|
|
401
|
+
from spanforge.auto import trace_rag
|
|
402
|
+
|
|
403
|
+
@trace_rag
|
|
404
|
+
def my_retriever(query: str) -> list[dict]:
|
|
405
|
+
return vector_db.search(query)
|
|
406
|
+
|
|
407
|
+
# The decorator emits RAG spans on every call.
|
|
408
|
+
results = my_retriever("What is SpanForge?")
|
|
409
|
+
|
|
410
|
+
The decorator is fail-safe: if sf-rag is unavailable (e.g. in tests),
|
|
411
|
+
the wrapped function executes normally without raising.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
func: The retrieval callable to decorate. Its first positional
|
|
415
|
+
argument must be the query string.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
The wrapped function with identical signature and return type.
|
|
419
|
+
"""
|
|
420
|
+
import functools
|
|
421
|
+
import time as _time
|
|
422
|
+
|
|
423
|
+
@functools.wraps(func)
|
|
424
|
+
def _wrapper(*args, **kwargs):
|
|
425
|
+
# Treat the first positional arg (or 'query' kwarg) as the query text.
|
|
426
|
+
query_text = ""
|
|
427
|
+
if args:
|
|
428
|
+
query_text = str(args[0])
|
|
429
|
+
query_text = str(kwargs.get("query", query_text)) or query_text
|
|
430
|
+
|
|
431
|
+
session_id: str | None = None
|
|
432
|
+
try:
|
|
433
|
+
from spanforge.sdk import sf_rag # type: ignore[import]
|
|
434
|
+
|
|
435
|
+
session_id = sf_rag.trace_query(
|
|
436
|
+
query=query_text,
|
|
437
|
+
retriever_name=func.__qualname__,
|
|
438
|
+
)
|
|
439
|
+
except Exception: # nosec B110 — never let tracing break the application
|
|
440
|
+
pass
|
|
441
|
+
|
|
442
|
+
t0 = _time.monotonic()
|
|
443
|
+
result = func(*args, **kwargs)
|
|
444
|
+
latency_ms = (_time.monotonic() - t0) * 1000
|
|
445
|
+
|
|
446
|
+
if session_id is not None:
|
|
447
|
+
try:
|
|
448
|
+
from spanforge.sdk import sf_rag # type: ignore[import]
|
|
449
|
+
|
|
450
|
+
chunk_count = len(result) if isinstance(result, (list, tuple)) else 0
|
|
451
|
+
sf_rag.trace_retrieval(
|
|
452
|
+
session_id=session_id,
|
|
453
|
+
chunks=[
|
|
454
|
+
{"chunk_id": str(i), "score": 0.0}
|
|
455
|
+
for i in range(chunk_count)
|
|
456
|
+
],
|
|
457
|
+
latency_ms=float(latency_ms),
|
|
458
|
+
)
|
|
459
|
+
except Exception: # nosec B110
|
|
460
|
+
pass
|
|
461
|
+
|
|
462
|
+
return result
|
|
463
|
+
|
|
464
|
+
return _wrapper
|