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
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""spanforge.namespaces.retrieval — RAG retrieval namespace payload types.
|
|
2
|
+
|
|
3
|
+
Provides dataclasses for the ``llm.rag.*`` event namespace, covering all
|
|
4
|
+
phases of a Retrieval-Augmented Generation pipeline:
|
|
5
|
+
|
|
6
|
+
1. **Query** — the user query forwarded to the retriever.
|
|
7
|
+
2. **Retrieval** — the documents / chunks fetched from the vector store.
|
|
8
|
+
3. **Generation** — the LLM generation that consumes retrieved context.
|
|
9
|
+
4. **Session** — end-to-end RAG session summary.
|
|
10
|
+
|
|
11
|
+
Classes
|
|
12
|
+
-------
|
|
13
|
+
RetrievalQueryPayload
|
|
14
|
+
``llm.rag.query`` events — user query + retriever config.
|
|
15
|
+
RetrievalResultPayload
|
|
16
|
+
``llm.rag.retrieved`` events — retrieved chunks with scores.
|
|
17
|
+
RAGSpanPayload
|
|
18
|
+
``llm.rag.generated`` events — LLM generation span over retrieved context.
|
|
19
|
+
RAGSessionPayload
|
|
20
|
+
``llm.rag.session`` events — root summary for a complete RAG interaction.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any, Literal
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"RAGSessionPayload",
|
|
30
|
+
"RAGSpanPayload",
|
|
31
|
+
"RetrievalQueryPayload",
|
|
32
|
+
"RetrievalResultPayload",
|
|
33
|
+
"RetrievedChunk",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
_VALID_STATUSES: frozenset[str] = frozenset({"ok", "error", "timeout", "partial"})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Value objects
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class RetrievedChunk:
|
|
46
|
+
"""A single retrieved document chunk with its relevance score.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
chunk_id: Unique identifier for the chunk within the document store.
|
|
50
|
+
content_hash: SHA-256 hex digest of the chunk text (raw text NOT stored).
|
|
51
|
+
score: Relevance / similarity score in [0.0, 1.0].
|
|
52
|
+
source: Document source identifier (e.g. URI, filename, database key).
|
|
53
|
+
metadata: Arbitrary key-value metadata attached to the chunk.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
chunk_id: str
|
|
57
|
+
content_hash: str
|
|
58
|
+
score: float
|
|
59
|
+
source: str = ""
|
|
60
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
def __post_init__(self) -> None:
|
|
63
|
+
if not self.chunk_id:
|
|
64
|
+
raise ValueError("RetrievedChunk.chunk_id must be non-empty")
|
|
65
|
+
if not (0.0 <= self.score <= 1.0):
|
|
66
|
+
raise ValueError(f"RetrievedChunk.score must be in [0, 1]; got {self.score}")
|
|
67
|
+
|
|
68
|
+
def to_dict(self) -> dict[str, Any]:
|
|
69
|
+
"""Serialise to a plain dict."""
|
|
70
|
+
return {
|
|
71
|
+
"chunk_id": self.chunk_id,
|
|
72
|
+
"content_hash": self.content_hash,
|
|
73
|
+
"score": self.score,
|
|
74
|
+
"source": self.source,
|
|
75
|
+
"metadata": self.metadata,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_dict(cls, data: dict[str, Any]) -> RetrievedChunk:
|
|
80
|
+
"""Deserialise from a plain dict."""
|
|
81
|
+
return cls(
|
|
82
|
+
chunk_id=str(data["chunk_id"]),
|
|
83
|
+
content_hash=str(data.get("content_hash", "")),
|
|
84
|
+
score=float(data["score"]),
|
|
85
|
+
source=str(data.get("source", "")),
|
|
86
|
+
metadata=dict(data.get("metadata", {})),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Payload dataclasses
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class RetrievalQueryPayload:
|
|
97
|
+
"""Payload for ``llm.rag.query`` events.
|
|
98
|
+
|
|
99
|
+
Captures the user query and the retriever configuration at the time of
|
|
100
|
+
the query without storing raw query text.
|
|
101
|
+
|
|
102
|
+
Attributes:
|
|
103
|
+
session_id: RAG session this query belongs to.
|
|
104
|
+
query_hash: SHA-256 hex digest of the query text (text NOT stored).
|
|
105
|
+
top_k: Number of chunks requested from the retriever.
|
|
106
|
+
retriever_name: Name / identifier of the vector store or retriever.
|
|
107
|
+
embedding_model: Embedding model used to encode the query.
|
|
108
|
+
namespace: Optional vector store namespace / collection.
|
|
109
|
+
latency_ms: Time taken to submit the query (ms).
|
|
110
|
+
filters: Metadata filters applied to the retrieval query.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
session_id: str
|
|
114
|
+
query_hash: str
|
|
115
|
+
top_k: int = 5
|
|
116
|
+
retriever_name: str = ""
|
|
117
|
+
embedding_model: str = ""
|
|
118
|
+
namespace: str = ""
|
|
119
|
+
latency_ms: float = 0.0
|
|
120
|
+
filters: dict[str, Any] = field(default_factory=dict)
|
|
121
|
+
|
|
122
|
+
def __post_init__(self) -> None:
|
|
123
|
+
if not self.session_id:
|
|
124
|
+
raise ValueError("RetrievalQueryPayload.session_id must be non-empty")
|
|
125
|
+
if self.top_k < 1:
|
|
126
|
+
raise ValueError(f"RetrievalQueryPayload.top_k must be >= 1; got {self.top_k}")
|
|
127
|
+
if self.latency_ms < 0:
|
|
128
|
+
raise ValueError("RetrievalQueryPayload.latency_ms must be >= 0")
|
|
129
|
+
|
|
130
|
+
def to_dict(self) -> dict[str, Any]:
|
|
131
|
+
"""Serialise to a plain dict."""
|
|
132
|
+
return {
|
|
133
|
+
"session_id": self.session_id,
|
|
134
|
+
"query_hash": self.query_hash,
|
|
135
|
+
"top_k": self.top_k,
|
|
136
|
+
"retriever_name": self.retriever_name,
|
|
137
|
+
"embedding_model": self.embedding_model,
|
|
138
|
+
"namespace": self.namespace,
|
|
139
|
+
"latency_ms": self.latency_ms,
|
|
140
|
+
"filters": self.filters,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_dict(cls, data: dict[str, Any]) -> RetrievalQueryPayload:
|
|
145
|
+
"""Deserialise from a plain dict."""
|
|
146
|
+
return cls(
|
|
147
|
+
session_id=str(data["session_id"]),
|
|
148
|
+
query_hash=str(data.get("query_hash", "")),
|
|
149
|
+
top_k=int(data.get("top_k", 5)),
|
|
150
|
+
retriever_name=str(data.get("retriever_name", "")),
|
|
151
|
+
embedding_model=str(data.get("embedding_model", "")),
|
|
152
|
+
namespace=str(data.get("namespace", "")),
|
|
153
|
+
latency_ms=float(data.get("latency_ms", 0.0)),
|
|
154
|
+
filters=dict(data.get("filters", {})),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class RetrievalResultPayload:
|
|
160
|
+
"""Payload for ``llm.rag.retrieved`` events.
|
|
161
|
+
|
|
162
|
+
Attributes:
|
|
163
|
+
session_id: RAG session this result belongs to.
|
|
164
|
+
query_hash: SHA-256 hex digest of the triggering query.
|
|
165
|
+
chunks: Ordered list of retrieved chunks.
|
|
166
|
+
total_found: Total number of matching chunks before ``top_k`` truncation.
|
|
167
|
+
latency_ms: Time taken for the retrieval (ms).
|
|
168
|
+
status: Retrieval status: ``"ok"``, ``"partial"``, ``"error"``,
|
|
169
|
+
or ``"timeout"``.
|
|
170
|
+
error_message: Present when *status* is ``"error"`` or ``"timeout"``.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
session_id: str
|
|
174
|
+
query_hash: str
|
|
175
|
+
chunks: list[RetrievedChunk] = field(default_factory=list)
|
|
176
|
+
total_found: int = 0
|
|
177
|
+
latency_ms: float = 0.0
|
|
178
|
+
status: Literal["ok", "partial", "error", "timeout"] = "ok"
|
|
179
|
+
error_message: str | None = None
|
|
180
|
+
|
|
181
|
+
def __post_init__(self) -> None:
|
|
182
|
+
if not self.session_id:
|
|
183
|
+
raise ValueError("RetrievalResultPayload.session_id must be non-empty")
|
|
184
|
+
if self.status not in _VALID_STATUSES:
|
|
185
|
+
raise ValueError(
|
|
186
|
+
f"RetrievalResultPayload.status must be one of {sorted(_VALID_STATUSES)}"
|
|
187
|
+
)
|
|
188
|
+
if self.latency_ms < 0:
|
|
189
|
+
raise ValueError("RetrievalResultPayload.latency_ms must be >= 0")
|
|
190
|
+
|
|
191
|
+
def to_dict(self) -> dict[str, Any]:
|
|
192
|
+
"""Serialise to a plain dict."""
|
|
193
|
+
d: dict[str, Any] = {
|
|
194
|
+
"session_id": self.session_id,
|
|
195
|
+
"query_hash": self.query_hash,
|
|
196
|
+
"chunks": [c.to_dict() for c in self.chunks],
|
|
197
|
+
"total_found": self.total_found,
|
|
198
|
+
"latency_ms": self.latency_ms,
|
|
199
|
+
"status": self.status,
|
|
200
|
+
}
|
|
201
|
+
if self.error_message is not None:
|
|
202
|
+
d["error_message"] = self.error_message
|
|
203
|
+
return d
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def from_dict(cls, data: dict[str, Any]) -> RetrievalResultPayload:
|
|
207
|
+
"""Deserialise from a plain dict."""
|
|
208
|
+
return cls(
|
|
209
|
+
session_id=str(data["session_id"]),
|
|
210
|
+
query_hash=str(data.get("query_hash", "")),
|
|
211
|
+
chunks=[RetrievedChunk.from_dict(c) for c in data.get("chunks", [])],
|
|
212
|
+
total_found=int(data.get("total_found", 0)),
|
|
213
|
+
latency_ms=float(data.get("latency_ms", 0.0)),
|
|
214
|
+
status=data.get("status", "ok"),
|
|
215
|
+
error_message=data.get("error_message"),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass
|
|
220
|
+
class RAGSpanPayload:
|
|
221
|
+
"""Payload for ``llm.rag.generated`` events.
|
|
222
|
+
|
|
223
|
+
Represents the LLM generation step that consumes retrieved context.
|
|
224
|
+
|
|
225
|
+
Attributes:
|
|
226
|
+
session_id: RAG session this span belongs to.
|
|
227
|
+
span_name: Human-readable label for the generation step.
|
|
228
|
+
model: Model identifier (e.g. ``"gpt-4o"``).
|
|
229
|
+
chunk_ids_used: Identifiers of the chunks included in the context window.
|
|
230
|
+
context_tokens: Number of tokens consumed by the retrieved context.
|
|
231
|
+
prompt_tokens: Number of tokens in the full prompt (context + instruction).
|
|
232
|
+
output_tokens: Number of tokens in the generated response.
|
|
233
|
+
latency_ms: Total generation latency in milliseconds.
|
|
234
|
+
status: Generation status.
|
|
235
|
+
grounding_score: Optional 0.0–1.0 score measuring how well the output
|
|
236
|
+
is grounded in the retrieved context.
|
|
237
|
+
error_message: Present when *status* is not ``"ok"``.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
session_id: str
|
|
241
|
+
span_name: str
|
|
242
|
+
model: str
|
|
243
|
+
chunk_ids_used: list[str] = field(default_factory=list)
|
|
244
|
+
context_tokens: int = 0
|
|
245
|
+
prompt_tokens: int = 0
|
|
246
|
+
output_tokens: int = 0
|
|
247
|
+
latency_ms: float = 0.0
|
|
248
|
+
status: Literal["ok", "error", "timeout"] = "ok"
|
|
249
|
+
grounding_score: float | None = None
|
|
250
|
+
error_message: str | None = None
|
|
251
|
+
|
|
252
|
+
def __post_init__(self) -> None:
|
|
253
|
+
if not self.session_id:
|
|
254
|
+
raise ValueError("RAGSpanPayload.session_id must be non-empty")
|
|
255
|
+
if not self.model:
|
|
256
|
+
raise ValueError("RAGSpanPayload.model must be non-empty")
|
|
257
|
+
if self.status not in {"ok", "error", "timeout"}:
|
|
258
|
+
raise ValueError(
|
|
259
|
+
f"RAGSpanPayload.status must be 'ok', 'error', or 'timeout'; got {self.status!r}"
|
|
260
|
+
)
|
|
261
|
+
if self.latency_ms < 0:
|
|
262
|
+
raise ValueError("RAGSpanPayload.latency_ms must be >= 0")
|
|
263
|
+
if self.grounding_score is not None and not (0.0 <= self.grounding_score <= 1.0):
|
|
264
|
+
raise ValueError(
|
|
265
|
+
f"RAGSpanPayload.grounding_score must be in [0, 1]; got {self.grounding_score}"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def to_dict(self) -> dict[str, Any]:
|
|
269
|
+
"""Serialise to a plain dict."""
|
|
270
|
+
d: dict[str, Any] = {
|
|
271
|
+
"session_id": self.session_id,
|
|
272
|
+
"span_name": self.span_name,
|
|
273
|
+
"model": self.model,
|
|
274
|
+
"chunk_ids_used": self.chunk_ids_used,
|
|
275
|
+
"context_tokens": self.context_tokens,
|
|
276
|
+
"prompt_tokens": self.prompt_tokens,
|
|
277
|
+
"output_tokens": self.output_tokens,
|
|
278
|
+
"latency_ms": self.latency_ms,
|
|
279
|
+
"status": self.status,
|
|
280
|
+
}
|
|
281
|
+
if self.grounding_score is not None:
|
|
282
|
+
d["grounding_score"] = self.grounding_score
|
|
283
|
+
if self.error_message is not None:
|
|
284
|
+
d["error_message"] = self.error_message
|
|
285
|
+
return d
|
|
286
|
+
|
|
287
|
+
@classmethod
|
|
288
|
+
def from_dict(cls, data: dict[str, Any]) -> RAGSpanPayload:
|
|
289
|
+
"""Deserialise from a plain dict."""
|
|
290
|
+
gs = data.get("grounding_score")
|
|
291
|
+
return cls(
|
|
292
|
+
session_id=str(data["session_id"]),
|
|
293
|
+
span_name=str(data.get("span_name", "")),
|
|
294
|
+
model=str(data["model"]),
|
|
295
|
+
chunk_ids_used=list(data.get("chunk_ids_used", [])),
|
|
296
|
+
context_tokens=int(data.get("context_tokens", 0)),
|
|
297
|
+
prompt_tokens=int(data.get("prompt_tokens", 0)),
|
|
298
|
+
output_tokens=int(data.get("output_tokens", 0)),
|
|
299
|
+
latency_ms=float(data.get("latency_ms", 0.0)),
|
|
300
|
+
status=data.get("status", "ok"),
|
|
301
|
+
grounding_score=float(gs) if gs is not None else None,
|
|
302
|
+
error_message=data.get("error_message"),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@dataclass
|
|
307
|
+
class RAGSessionPayload:
|
|
308
|
+
"""Payload for ``llm.rag.session`` events.
|
|
309
|
+
|
|
310
|
+
Root summary for a complete Retrieval-Augmented Generation interaction
|
|
311
|
+
from initial user query through to final generated response.
|
|
312
|
+
|
|
313
|
+
Attributes:
|
|
314
|
+
session_id: Unique identifier for this RAG session.
|
|
315
|
+
total_queries: Number of retrieval queries issued in the session.
|
|
316
|
+
total_chunks_used: Total distinct chunk IDs consumed across all generations.
|
|
317
|
+
total_input_tokens: Sum of all prompt tokens across generation spans.
|
|
318
|
+
total_output_tokens: Sum of all output tokens across generation spans.
|
|
319
|
+
avg_grounding_score: Mean grounding score across all ``llm.rag.generated``
|
|
320
|
+
spans; ``None`` if no grounding scores were recorded.
|
|
321
|
+
total_latency_ms: Total wall-clock time for the session (ms).
|
|
322
|
+
status: Overall session status.
|
|
323
|
+
retriever_name: Name of the primary retriever used in this session.
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
session_id: str
|
|
327
|
+
total_queries: int = 0
|
|
328
|
+
total_chunks_used: int = 0
|
|
329
|
+
total_input_tokens: int = 0
|
|
330
|
+
total_output_tokens: int = 0
|
|
331
|
+
avg_grounding_score: float | None = None
|
|
332
|
+
total_latency_ms: float = 0.0
|
|
333
|
+
status: Literal["ok", "partial", "error"] = "ok"
|
|
334
|
+
retriever_name: str = ""
|
|
335
|
+
|
|
336
|
+
def __post_init__(self) -> None:
|
|
337
|
+
if not self.session_id:
|
|
338
|
+
raise ValueError("RAGSessionPayload.session_id must be non-empty")
|
|
339
|
+
if self.status not in {"ok", "partial", "error"}:
|
|
340
|
+
raise ValueError(
|
|
341
|
+
f"RAGSessionPayload.status must be 'ok', 'partial', or 'error'; got {self.status!r}"
|
|
342
|
+
)
|
|
343
|
+
if self.avg_grounding_score is not None and not (0.0 <= self.avg_grounding_score <= 1.0):
|
|
344
|
+
raise ValueError(
|
|
345
|
+
f"RAGSessionPayload.avg_grounding_score must be in [0, 1]; "
|
|
346
|
+
f"got {self.avg_grounding_score}"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def to_dict(self) -> dict[str, Any]:
|
|
350
|
+
"""Serialise to a plain dict."""
|
|
351
|
+
d: dict[str, Any] = {
|
|
352
|
+
"session_id": self.session_id,
|
|
353
|
+
"total_queries": self.total_queries,
|
|
354
|
+
"total_chunks_used": self.total_chunks_used,
|
|
355
|
+
"total_input_tokens": self.total_input_tokens,
|
|
356
|
+
"total_output_tokens": self.total_output_tokens,
|
|
357
|
+
"total_latency_ms": self.total_latency_ms,
|
|
358
|
+
"status": self.status,
|
|
359
|
+
"retriever_name": self.retriever_name,
|
|
360
|
+
}
|
|
361
|
+
if self.avg_grounding_score is not None:
|
|
362
|
+
d["avg_grounding_score"] = self.avg_grounding_score
|
|
363
|
+
return d
|
|
364
|
+
|
|
365
|
+
@classmethod
|
|
366
|
+
def from_dict(cls, data: dict[str, Any]) -> RAGSessionPayload:
|
|
367
|
+
"""Deserialise from a plain dict."""
|
|
368
|
+
ags = data.get("avg_grounding_score")
|
|
369
|
+
return cls(
|
|
370
|
+
session_id=str(data["session_id"]),
|
|
371
|
+
total_queries=int(data.get("total_queries", 0)),
|
|
372
|
+
total_chunks_used=int(data.get("total_chunks_used", 0)),
|
|
373
|
+
total_input_tokens=int(data.get("total_input_tokens", 0)),
|
|
374
|
+
total_output_tokens=int(data.get("total_output_tokens", 0)),
|
|
375
|
+
avg_grounding_score=float(ags) if ags is not None else None,
|
|
376
|
+
total_latency_ms=float(data.get("total_latency_ms", 0.0)),
|
|
377
|
+
status=data.get("status", "ok"),
|
|
378
|
+
retriever_name=str(data.get("retriever_name", "")),
|
|
379
|
+
)
|