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
spanforge/exceptions.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Typed exception hierarchy for spanforge.
|
|
2
|
+
|
|
3
|
+
All exceptions raised by spanforge inherit from :class:`LLMSchemaError` so
|
|
4
|
+
callers can catch the whole family with a single ``except LLMSchemaError``.
|
|
5
|
+
|
|
6
|
+
Design rules
|
|
7
|
+
------------
|
|
8
|
+
* Exceptions carry enough context to be actionable — field name, received value,
|
|
9
|
+
and an explanation of what was expected.
|
|
10
|
+
* HMAC keys and PII-tagged content are **never** embedded in exception messages
|
|
11
|
+
or ``__cause__`` chains.
|
|
12
|
+
* No bare ``raise`` — every raise site uses a typed subclass.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AuditStorageError",
|
|
19
|
+
"DeserializationError",
|
|
20
|
+
"EgressViolationError",
|
|
21
|
+
"EventTypeError",
|
|
22
|
+
"ExportError",
|
|
23
|
+
"LLMSchemaError",
|
|
24
|
+
"SchemaValidationError",
|
|
25
|
+
"SchemaVersionError",
|
|
26
|
+
"SerializationError",
|
|
27
|
+
"SigningError",
|
|
28
|
+
"ULIDError",
|
|
29
|
+
"VerificationError",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class LLMSchemaError(Exception):
|
|
34
|
+
"""Base class for all spanforge exceptions.
|
|
35
|
+
|
|
36
|
+
All public-facing exceptions derive from this class, enabling callers to
|
|
37
|
+
write a single broad ``except LLMSchemaError`` guard as a safety net while
|
|
38
|
+
still being able to catch specific sub-types for targeted handling.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SchemaValidationError(LLMSchemaError):
|
|
43
|
+
"""Raised when an :class:`~spanforge.event.Event` fails validation.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
field: The dotted field path that failed (e.g. ``"event_id"``).
|
|
47
|
+
received: The actual value that was provided (redacted if sensitive).
|
|
48
|
+
reason: Human-readable explanation of the constraint that was violated.
|
|
49
|
+
|
|
50
|
+
Example::
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
event.validate()
|
|
54
|
+
except SchemaValidationError as exc:
|
|
55
|
+
logger.error("Invalid event field=%s reason=%s", exc.field, exc.reason)
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, field: str, received: object, reason: str) -> None:
|
|
59
|
+
self.field = field
|
|
60
|
+
self.received = received
|
|
61
|
+
self.reason = reason
|
|
62
|
+
super().__init__(
|
|
63
|
+
f"Validation failed for field '{field}': {reason} "
|
|
64
|
+
f"(received type={type(received).__name__!r})"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ULIDError(LLMSchemaError):
|
|
69
|
+
"""Raised when ULID generation or parsing fails.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
detail: Human-readable description of the failure.
|
|
73
|
+
|
|
74
|
+
This exception is intentionally opaque about internal state to avoid
|
|
75
|
+
leaking timing information that could aid side-channel attacks.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, detail: str) -> None:
|
|
79
|
+
self.detail = detail
|
|
80
|
+
super().__init__(f"ULID error: {detail}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SerializationError(LLMSchemaError):
|
|
84
|
+
"""Raised when an :class:`~spanforge.event.Event` cannot be serialized.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
event_id: The ULID of the event that failed (safe to log).
|
|
88
|
+
reason: Human-readable description of the failure.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, event_id: str, reason: str) -> None:
|
|
92
|
+
self.event_id = event_id
|
|
93
|
+
self.reason = reason
|
|
94
|
+
super().__init__(
|
|
95
|
+
f"Serialization failed for event '{event_id}': {reason}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class DeserializationError(LLMSchemaError):
|
|
100
|
+
"""Raised when a JSON blob cannot be deserialized into an Event.
|
|
101
|
+
|
|
102
|
+
Attributes:
|
|
103
|
+
reason: Human-readable description of the failure.
|
|
104
|
+
source_hint: A short, non-PII hint about the source (e.g. filename).
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, reason: str, source_hint: str = "<unknown>") -> None:
|
|
108
|
+
self.reason = reason
|
|
109
|
+
self.source_hint = source_hint
|
|
110
|
+
super().__init__(
|
|
111
|
+
f"Deserialization failed (source={source_hint!r}): {reason}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class EventTypeError(LLMSchemaError):
|
|
116
|
+
"""Raised when an unknown or malformed event type string is encountered.
|
|
117
|
+
|
|
118
|
+
Attributes:
|
|
119
|
+
event_type: The offending event type string.
|
|
120
|
+
reason: Human-readable description of the failure.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, event_type: str, reason: str) -> None:
|
|
124
|
+
self.event_type = event_type
|
|
125
|
+
self.reason = reason
|
|
126
|
+
super().__init__(
|
|
127
|
+
f"Invalid event type '{event_type}': {reason}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class SigningError(LLMSchemaError):
|
|
132
|
+
"""Raised when HMAC event signing fails.
|
|
133
|
+
|
|
134
|
+
Security: the ``org_secret`` value is **never** included in the message.
|
|
135
|
+
|
|
136
|
+
Attributes:
|
|
137
|
+
reason: Human-readable description of why signing failed.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(self, reason: str) -> None:
|
|
141
|
+
self.reason = reason
|
|
142
|
+
super().__init__(f"Signing failed: {reason}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class VerificationError(LLMSchemaError):
|
|
146
|
+
"""Raised when an event fails cryptographic verification.
|
|
147
|
+
|
|
148
|
+
Raised by :func:`~spanforge.signing.assert_verified` on verification failure.
|
|
149
|
+
|
|
150
|
+
Attributes:
|
|
151
|
+
event_id: The ULID of the event that failed (safe to log).
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, event_id: str) -> None:
|
|
155
|
+
self.event_id = event_id
|
|
156
|
+
super().__init__(
|
|
157
|
+
f"Event '{event_id}' failed cryptographic verification. "
|
|
158
|
+
"The event may have been tampered with or the wrong key was used."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ExportError(LLMSchemaError):
|
|
163
|
+
"""Raised when exporting events to an external backend fails.
|
|
164
|
+
|
|
165
|
+
Attributes:
|
|
166
|
+
backend: Short identifier for the backend (e.g. ``"otlp"``,
|
|
167
|
+
``"webhook"``, ``"jsonl"``).
|
|
168
|
+
reason: Human-readable description of the failure.
|
|
169
|
+
event_id: The ULID of the event that failed, or ``""`` for batch
|
|
170
|
+
failures where no single event is responsible.
|
|
171
|
+
|
|
172
|
+
Security: HMAC secrets and PII-tagged payloads are **never** embedded in
|
|
173
|
+
the message or ``__cause__``.
|
|
174
|
+
|
|
175
|
+
Example::
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
await exporter.export(event)
|
|
179
|
+
except ExportError as exc:
|
|
180
|
+
logger.error("backend=%s reason=%s", exc.backend, exc.reason)
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def __init__(self, backend: str, reason: str, event_id: str = "") -> None:
|
|
184
|
+
self.backend = backend
|
|
185
|
+
self.reason = reason
|
|
186
|
+
self.event_id = event_id
|
|
187
|
+
msg = f"Export to '{backend}' failed: {reason}"
|
|
188
|
+
if event_id:
|
|
189
|
+
msg += f" (event_id={event_id!r})"
|
|
190
|
+
super().__init__(msg)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class SchemaVersionError(LLMSchemaError):
|
|
194
|
+
"""Raised when an event carries an unsupported ``schema_version`` value.
|
|
195
|
+
|
|
196
|
+
RFC-0001 §15.5 specifies that v2.0 consumers MUST accept events with
|
|
197
|
+
``schema_version`` ``"1.0"`` or ``"2.0"`` and MUST raise this error
|
|
198
|
+
for any other value.
|
|
199
|
+
|
|
200
|
+
Attributes:
|
|
201
|
+
version: The unsupported schema version string that was encountered.
|
|
202
|
+
|
|
203
|
+
Example::
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
consumer.ingest(event)
|
|
207
|
+
except SchemaVersionError as exc:
|
|
208
|
+
logger.warning("Skipping event with unknown version %s", exc.version)
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def __init__(self, version: str) -> None:
|
|
212
|
+
self.version = version
|
|
213
|
+
super().__init__(
|
|
214
|
+
f"Unsupported schema_version {version!r}. "
|
|
215
|
+
"Accepted values: '1.0', '2.0' (RFC-0001 §15.5)."
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class EgressViolationError(LLMSchemaError):
|
|
220
|
+
"""Raised when an exporter attempts a network call in no-egress mode.
|
|
221
|
+
|
|
222
|
+
Attributes:
|
|
223
|
+
backend: Short identifier for the backend that violated the policy.
|
|
224
|
+
endpoint: The endpoint URL that was blocked (may be ``""`` if unknown).
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
def __init__(self, backend: str, endpoint: str = "") -> None:
|
|
228
|
+
self.backend = backend
|
|
229
|
+
self.endpoint = endpoint
|
|
230
|
+
msg = f"Egress violation: exporter '{backend}' attempted a network call"
|
|
231
|
+
if endpoint:
|
|
232
|
+
msg += f" to {endpoint!r}"
|
|
233
|
+
msg += " but no_egress mode is active"
|
|
234
|
+
super().__init__(msg)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class AuditStorageError(LLMSchemaError):
|
|
238
|
+
"""Raised when the audit log storage layer detects a violation.
|
|
239
|
+
|
|
240
|
+
Attributes:
|
|
241
|
+
reason: Human-readable description of the storage violation.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
def __init__(self, reason: str) -> None:
|
|
245
|
+
self.reason = reason
|
|
246
|
+
super().__init__(f"Audit storage error: {reason}")
|
spanforge/explain.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""spanforge.explain — Explainability record generation for AI compliance.
|
|
2
|
+
|
|
3
|
+
Aggregates decision drivers from span payloads into human-readable
|
|
4
|
+
explanations compliant with EU AI Act transparency requirements
|
|
5
|
+
(Articles 13-14) and the T.R.U.S.T. Framework Transparency pillar.
|
|
6
|
+
|
|
7
|
+
Emits ``explanation.generated`` events into the HMAC audit chain.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
from spanforge.explain import ExplainabilityRecord
|
|
12
|
+
|
|
13
|
+
record = ExplainabilityRecord(
|
|
14
|
+
trace_id="01HQZF...",
|
|
15
|
+
agent_id="support-agent@1.0",
|
|
16
|
+
decision_id="01HQZF...",
|
|
17
|
+
factors=[
|
|
18
|
+
{"factor_name": "user_intent", "weight": 0.6,
|
|
19
|
+
"contribution": 0.42, "evidence": "keyword match",
|
|
20
|
+
"confidence": 0.85},
|
|
21
|
+
],
|
|
22
|
+
summary="Routed to billing team based on keyword match.",
|
|
23
|
+
)
|
|
24
|
+
print(record.to_text())
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"ExplainabilityRecord",
|
|
35
|
+
"generate_explanation",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ExplainabilityRecord:
|
|
41
|
+
"""A human-readable explanation of one or more AI decisions.
|
|
42
|
+
|
|
43
|
+
Designed to satisfy EU AI Act Art. 13 transparency obligations.
|
|
44
|
+
Each record can be serialised to plain text, JSON, or dict for
|
|
45
|
+
audit trail inclusion.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
trace_id: str
|
|
49
|
+
agent_id: str
|
|
50
|
+
decision_id: str
|
|
51
|
+
factors: list[dict[str, Any]]
|
|
52
|
+
summary: str
|
|
53
|
+
model_id: str | None = None
|
|
54
|
+
confidence: float | None = None
|
|
55
|
+
risk_tier: str | None = None
|
|
56
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
if not self.trace_id:
|
|
60
|
+
raise ValueError("ExplainabilityRecord.trace_id must be non-empty")
|
|
61
|
+
if not self.agent_id:
|
|
62
|
+
raise ValueError("ExplainabilityRecord.agent_id must be non-empty")
|
|
63
|
+
if not self.decision_id:
|
|
64
|
+
raise ValueError("ExplainabilityRecord.decision_id must be non-empty")
|
|
65
|
+
if not self.summary:
|
|
66
|
+
raise ValueError("ExplainabilityRecord.summary must be non-empty")
|
|
67
|
+
if self.confidence is not None and not (0.0 <= self.confidence <= 1.0):
|
|
68
|
+
raise ValueError(
|
|
69
|
+
"ExplainabilityRecord.confidence must be in [0.0, 1.0]"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> dict[str, Any]:
|
|
73
|
+
"""Serialise to a plain dict."""
|
|
74
|
+
d: dict[str, Any] = {
|
|
75
|
+
"trace_id": self.trace_id,
|
|
76
|
+
"agent_id": self.agent_id,
|
|
77
|
+
"decision_id": self.decision_id,
|
|
78
|
+
"factors": self.factors,
|
|
79
|
+
"summary": self.summary,
|
|
80
|
+
}
|
|
81
|
+
if self.model_id is not None:
|
|
82
|
+
d["model_id"] = self.model_id
|
|
83
|
+
if self.confidence is not None:
|
|
84
|
+
d["confidence"] = self.confidence
|
|
85
|
+
if self.risk_tier is not None:
|
|
86
|
+
d["risk_tier"] = self.risk_tier
|
|
87
|
+
if self.metadata:
|
|
88
|
+
d["metadata"] = self.metadata
|
|
89
|
+
return d
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_dict(cls, data: dict[str, Any]) -> ExplainabilityRecord:
|
|
93
|
+
"""Reconstruct from a plain dict."""
|
|
94
|
+
return cls(
|
|
95
|
+
trace_id=data["trace_id"],
|
|
96
|
+
agent_id=data["agent_id"],
|
|
97
|
+
decision_id=data["decision_id"],
|
|
98
|
+
factors=data.get("factors", []),
|
|
99
|
+
summary=data["summary"],
|
|
100
|
+
model_id=data.get("model_id"),
|
|
101
|
+
confidence=data.get("confidence"),
|
|
102
|
+
risk_tier=data.get("risk_tier"),
|
|
103
|
+
metadata=data.get("metadata", {}),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def to_json(self) -> str:
|
|
107
|
+
"""Serialise to JSON string."""
|
|
108
|
+
return json.dumps(self.to_dict(), default=str)
|
|
109
|
+
|
|
110
|
+
def to_text(self) -> str:
|
|
111
|
+
"""Generate a human-readable explanation text.
|
|
112
|
+
|
|
113
|
+
Returns a multi-line string suitable for end-user display or
|
|
114
|
+
compliance documentation.
|
|
115
|
+
"""
|
|
116
|
+
lines: list[str] = []
|
|
117
|
+
lines.append(f"Explanation for decision {self.decision_id}")
|
|
118
|
+
lines.append(f" Agent: {self.agent_id}")
|
|
119
|
+
lines.append(f" Trace: {self.trace_id}")
|
|
120
|
+
if self.model_id:
|
|
121
|
+
lines.append(f" Model: {self.model_id}")
|
|
122
|
+
if self.confidence is not None:
|
|
123
|
+
lines.append(f" Confidence: {self.confidence:.2%}")
|
|
124
|
+
if self.risk_tier:
|
|
125
|
+
lines.append(f" Risk tier: {self.risk_tier}")
|
|
126
|
+
lines.append(f" Summary: {self.summary}")
|
|
127
|
+
if self.factors:
|
|
128
|
+
lines.append(" Contributing factors:")
|
|
129
|
+
for f in self.factors:
|
|
130
|
+
name = f.get("factor_name", "unknown")
|
|
131
|
+
weight = f.get("weight", 0)
|
|
132
|
+
evidence = f.get("evidence", "")
|
|
133
|
+
lines.append(f" - {name} (weight={weight:.2f}): {evidence}")
|
|
134
|
+
return "\n".join(lines)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def generate_explanation(
|
|
138
|
+
trace_id: str,
|
|
139
|
+
agent_id: str,
|
|
140
|
+
decision_id: str,
|
|
141
|
+
factors: list[dict[str, Any]],
|
|
142
|
+
summary: str,
|
|
143
|
+
*,
|
|
144
|
+
model_id: str | None = None,
|
|
145
|
+
confidence: float | None = None,
|
|
146
|
+
risk_tier: str | None = None,
|
|
147
|
+
auto_emit: bool = True,
|
|
148
|
+
metadata: dict[str, Any] | None = None,
|
|
149
|
+
) -> ExplainabilityRecord:
|
|
150
|
+
"""Create an :class:`ExplainabilityRecord` and optionally emit an event.
|
|
151
|
+
|
|
152
|
+
This is the primary convenience function for the explainability module.
|
|
153
|
+
"""
|
|
154
|
+
record = ExplainabilityRecord(
|
|
155
|
+
trace_id=trace_id,
|
|
156
|
+
agent_id=agent_id,
|
|
157
|
+
decision_id=decision_id,
|
|
158
|
+
factors=factors,
|
|
159
|
+
summary=summary,
|
|
160
|
+
model_id=model_id,
|
|
161
|
+
confidence=confidence,
|
|
162
|
+
risk_tier=risk_tier,
|
|
163
|
+
metadata=metadata or {},
|
|
164
|
+
)
|
|
165
|
+
if auto_emit:
|
|
166
|
+
_emit_explanation(record)
|
|
167
|
+
return record
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _emit_explanation(record: ExplainabilityRecord) -> None:
|
|
171
|
+
"""Emit an explanation.generated event into the HMAC audit chain."""
|
|
172
|
+
try:
|
|
173
|
+
from spanforge._stream import emit_rfc_event # noqa: PLC0415
|
|
174
|
+
from spanforge.types import EventType # noqa: PLC0415
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
emit_rfc_event(EventType.EXPLANATION_GENERATED, record.to_dict())
|
|
178
|
+
except Exception: # noqa: BLE001
|
|
179
|
+
pass
|
|
180
|
+
except ImportError:
|
|
181
|
+
pass
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""RFC-0001 export backends for spanforge SDK events.
|
|
2
|
+
|
|
3
|
+
All exporters are **opt-in** — importing this package does not open any network
|
|
4
|
+
connections or file handles. Instantiate an exporter explicitly to activate it.
|
|
5
|
+
|
|
6
|
+
Core exporters (RFC-0001 §14)
|
|
7
|
+
------------------------------
|
|
8
|
+
* :class:`~spanforge.export.otlp.OTLPExporter` — OTLP/JSON HTTP exporter
|
|
9
|
+
(zero dependencies; builds OTLP wire format from stdlib).
|
|
10
|
+
* :class:`~spanforge.export.otel_bridge.OTelBridgeExporter` — OTel SDK bridge
|
|
11
|
+
that emits real OTel spans via a configured ``TracerProvider``.
|
|
12
|
+
Requires ``pip install "spanforge[otel]"``.
|
|
13
|
+
* :class:`~spanforge.export.webhook.WebhookExporter` — HTTP webhook with
|
|
14
|
+
HMAC-SHA256 request signing.
|
|
15
|
+
* :class:`~spanforge.export.jsonl.JSONLExporter` — NDJSON for local development
|
|
16
|
+
and audit trail persistence.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from spanforge.export.append_only import (
|
|
22
|
+
AppendOnlyJSONLExporter,
|
|
23
|
+
WORMBackend,
|
|
24
|
+
WORMUploadResult,
|
|
25
|
+
)
|
|
26
|
+
from spanforge.export.datadog import DatadogExporter, DatadogResourceAttributes
|
|
27
|
+
from spanforge.export.grafana import GrafanaLokiExporter
|
|
28
|
+
from spanforge.export.jsonl import JSONLExporter
|
|
29
|
+
from spanforge.export.otlp import OTLPExporter, ResourceAttributes
|
|
30
|
+
from spanforge.export.webhook import WebhookExporter
|
|
31
|
+
|
|
32
|
+
# OTelBridgeExporter is an optional import — requires opentelemetry-sdk
|
|
33
|
+
try:
|
|
34
|
+
from spanforge.export.otel_bridge import OTelBridgeExporter
|
|
35
|
+
except ImportError:
|
|
36
|
+
OTelBridgeExporter = None # type: ignore[assignment,misc]
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"AppendOnlyJSONLExporter",
|
|
40
|
+
"DatadogExporter",
|
|
41
|
+
"DatadogResourceAttributes",
|
|
42
|
+
"GrafanaLokiExporter",
|
|
43
|
+
"JSONLExporter",
|
|
44
|
+
"OTLPExporter",
|
|
45
|
+
"OTelBridgeExporter",
|
|
46
|
+
"ResourceAttributes",
|
|
47
|
+
"WORMBackend",
|
|
48
|
+
"WORMUploadResult",
|
|
49
|
+
"WebhookExporter",
|
|
50
|
+
]
|