sledtrace 0.7.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.
- raglens/__init__.py +23 -0
- raglens/chunks.py +499 -0
- raglens/client.py +0 -0
- raglens/models.py +73 -0
- raglens/trace.py +367 -0
- sledtrace/__init__.py +24 -0
- sledtrace/cli.py +100 -0
- sledtrace-0.7.0.dist-info/METADATA +159 -0
- sledtrace-0.7.0.dist-info/RECORD +13 -0
- sledtrace-0.7.0.dist-info/WHEEL +5 -0
- sledtrace-0.7.0.dist-info/entry_points.txt +2 -0
- sledtrace-0.7.0.dist-info/licenses/LICENSE +21 -0
- sledtrace-0.7.0.dist-info/top_level.txt +2 -0
raglens/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
from .trace import trace, RAGLensTrace, SledTraceTrace
|
|
4
|
+
from .chunks import ChunkNormalizationError, normalize_chunk, normalize_chunks
|
|
5
|
+
|
|
6
|
+
__version__ = "0.7.0"
|
|
7
|
+
|
|
8
|
+
warnings.warn(
|
|
9
|
+
"The 'raglens' package is deprecated and kept only for temporary compatibility. "
|
|
10
|
+
"Please import from 'sledtrace' instead.",
|
|
11
|
+
DeprecationWarning,
|
|
12
|
+
stacklevel=2,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"__version__",
|
|
17
|
+
"trace",
|
|
18
|
+
"SledTraceTrace",
|
|
19
|
+
"RAGLensTrace",
|
|
20
|
+
"ChunkNormalizationError",
|
|
21
|
+
"normalize_chunk",
|
|
22
|
+
"normalize_chunks",
|
|
23
|
+
]
|
raglens/chunks.py
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chunk normalization helpers for SledTrace.
|
|
3
|
+
|
|
4
|
+
RAG frameworks return retrieved chunks in many shapes:
|
|
5
|
+
|
|
6
|
+
- Standard dict:
|
|
7
|
+
{"text": "...", "source": "...", "score": 0.9}
|
|
8
|
+
|
|
9
|
+
- LangChain-like:
|
|
10
|
+
{"page_content": "...", "metadata": {"source": "..."}}
|
|
11
|
+
|
|
12
|
+
- Haystack-like:
|
|
13
|
+
{"content": "...", "meta": {"source": "..."}}
|
|
14
|
+
|
|
15
|
+
- LlamaIndex-like:
|
|
16
|
+
{"node": {"text": "...", "metadata": {"file_name": "..."}}, "score": 0.8}
|
|
17
|
+
|
|
18
|
+
- Tuple result:
|
|
19
|
+
(document, score)
|
|
20
|
+
|
|
21
|
+
- Bare string:
|
|
22
|
+
"chunk text..."
|
|
23
|
+
|
|
24
|
+
SledTrace only needs a normalized chunk payload:
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
"id": "...",
|
|
28
|
+
"text": "...",
|
|
29
|
+
"source": "...",
|
|
30
|
+
"score": 0.9,
|
|
31
|
+
"rank": 1,
|
|
32
|
+
"metadata": {...}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Minimum diagnostic contract:
|
|
36
|
+
- text is required
|
|
37
|
+
- source is recommended, falls back to "unknown"
|
|
38
|
+
- score is optional
|
|
39
|
+
- rank is optional but auto-filled by normalize_chunks
|
|
40
|
+
- metadata is optional
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import hashlib
|
|
46
|
+
from collections.abc import Mapping
|
|
47
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Union
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
TextExtractor = Union[str, Callable[[Any], Optional[str]]]
|
|
51
|
+
ValueExtractor = Union[str, Callable[[Any], Any]]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ChunkNormalizationError(ValueError):
|
|
55
|
+
"""Raised when a retrieved item cannot be normalized into a SledTrace chunk."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def normalize_chunk(
|
|
59
|
+
raw: Any,
|
|
60
|
+
rank: Optional[int] = None,
|
|
61
|
+
*,
|
|
62
|
+
text: Optional[TextExtractor] = None,
|
|
63
|
+
source: Optional[ValueExtractor] = None,
|
|
64
|
+
score: Optional[ValueExtractor] = None,
|
|
65
|
+
chunk_id: Optional[ValueExtractor] = None,
|
|
66
|
+
metadata: Optional[ValueExtractor] = None,
|
|
67
|
+
default_source: str = "unknown",
|
|
68
|
+
) -> Dict[str, Any]:
|
|
69
|
+
"""
|
|
70
|
+
Normalize one raw retrieved item into the SledTrace chunk contract.
|
|
71
|
+
|
|
72
|
+
Parameters:
|
|
73
|
+
raw:
|
|
74
|
+
A raw retrieved item. Can be a dict, object, tuple, string, etc.
|
|
75
|
+
|
|
76
|
+
rank:
|
|
77
|
+
Optional 1-based rank. If omitted, rank is left as None.
|
|
78
|
+
|
|
79
|
+
text/source/score/chunk_id/metadata:
|
|
80
|
+
Optional explicit extractors. Each can be:
|
|
81
|
+
- a dotted path string, such as "metadata.source" or "node.text"
|
|
82
|
+
- a callable, such as lambda d: d.page_content
|
|
83
|
+
|
|
84
|
+
default_source:
|
|
85
|
+
Used when no source-like field can be found.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
A dict with:
|
|
89
|
+
- id
|
|
90
|
+
- text
|
|
91
|
+
- source
|
|
92
|
+
- score
|
|
93
|
+
- rank
|
|
94
|
+
- metadata
|
|
95
|
+
"""
|
|
96
|
+
item, tuple_score = _unwrap_tuple_result(raw)
|
|
97
|
+
|
|
98
|
+
extracted_text = _extract_text(item, explicit=text)
|
|
99
|
+
if not extracted_text:
|
|
100
|
+
raise ChunkNormalizationError(
|
|
101
|
+
"Could not normalize retrieved chunk because no text/content field was found. "
|
|
102
|
+
"Provide text='...' or pass an object with text/page_content/content/node.text."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
extracted_metadata = _extract_metadata(item, explicit=metadata)
|
|
106
|
+
extracted_source = _extract_source(item, extracted_metadata, explicit=source)
|
|
107
|
+
extracted_score = _extract_score(item, tuple_score=tuple_score, explicit=score)
|
|
108
|
+
extracted_id = _extract_id(item, explicit=chunk_id)
|
|
109
|
+
|
|
110
|
+
if not extracted_id:
|
|
111
|
+
extracted_id = _stable_chunk_id(extracted_text, extracted_source or default_source)
|
|
112
|
+
|
|
113
|
+
normalized_metadata = dict(extracted_metadata)
|
|
114
|
+
normalized_metadata.setdefault("normalized_by", "sledtrace.normalize_chunk")
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"id": extracted_id,
|
|
118
|
+
"text": extracted_text,
|
|
119
|
+
"source": extracted_source or default_source,
|
|
120
|
+
"score": extracted_score,
|
|
121
|
+
"rank": rank,
|
|
122
|
+
"metadata": normalized_metadata,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def normalize_chunks(
|
|
127
|
+
raw_chunks: Iterable[Any],
|
|
128
|
+
*,
|
|
129
|
+
text: Optional[TextExtractor] = None,
|
|
130
|
+
source: Optional[ValueExtractor] = None,
|
|
131
|
+
score: Optional[ValueExtractor] = None,
|
|
132
|
+
chunk_id: Optional[ValueExtractor] = None,
|
|
133
|
+
metadata: Optional[ValueExtractor] = None,
|
|
134
|
+
default_source: str = "unknown",
|
|
135
|
+
start_rank: int = 1,
|
|
136
|
+
skip_invalid: bool = False,
|
|
137
|
+
) -> List[Dict[str, Any]]:
|
|
138
|
+
"""
|
|
139
|
+
Normalize many raw retrieved items into SledTrace chunks.
|
|
140
|
+
|
|
141
|
+
By default, invalid items raise ChunkNormalizationError.
|
|
142
|
+
Set skip_invalid=True to drop invalid items.
|
|
143
|
+
"""
|
|
144
|
+
normalized: List[Dict[str, Any]] = []
|
|
145
|
+
|
|
146
|
+
for index, raw in enumerate(raw_chunks):
|
|
147
|
+
rank = start_rank + index
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
normalized.append(
|
|
151
|
+
normalize_chunk(
|
|
152
|
+
raw,
|
|
153
|
+
rank=rank,
|
|
154
|
+
text=text,
|
|
155
|
+
source=source,
|
|
156
|
+
score=score,
|
|
157
|
+
chunk_id=chunk_id,
|
|
158
|
+
metadata=metadata,
|
|
159
|
+
default_source=default_source,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
except ChunkNormalizationError:
|
|
163
|
+
if not skip_invalid:
|
|
164
|
+
raise
|
|
165
|
+
|
|
166
|
+
return normalized
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _unwrap_tuple_result(raw: Any) -> tuple[Any, Any]:
|
|
170
|
+
"""
|
|
171
|
+
Common vector-store pattern:
|
|
172
|
+
(document, score)
|
|
173
|
+
|
|
174
|
+
Also supports:
|
|
175
|
+
[document, score]
|
|
176
|
+
"""
|
|
177
|
+
if isinstance(raw, tuple) and len(raw) == 2:
|
|
178
|
+
return raw[0], raw[1]
|
|
179
|
+
|
|
180
|
+
if isinstance(raw, list) and len(raw) == 2 and not _looks_like_chunk_list(raw):
|
|
181
|
+
return raw[0], raw[1]
|
|
182
|
+
|
|
183
|
+
return raw, None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _looks_like_chunk_list(value: list[Any]) -> bool:
|
|
187
|
+
if not value:
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
return all(isinstance(item, (str, Mapping)) for item in value)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _extract_text(item: Any, explicit: Optional[TextExtractor]) -> str:
|
|
194
|
+
if isinstance(item, str):
|
|
195
|
+
return item.strip()
|
|
196
|
+
|
|
197
|
+
if explicit is not None:
|
|
198
|
+
value = _extract_with_spec(item, explicit)
|
|
199
|
+
return _to_clean_string(value)
|
|
200
|
+
|
|
201
|
+
candidates = [
|
|
202
|
+
"text",
|
|
203
|
+
"page_content",
|
|
204
|
+
"content",
|
|
205
|
+
"body",
|
|
206
|
+
"chunk",
|
|
207
|
+
"node.text",
|
|
208
|
+
"node.content",
|
|
209
|
+
"node.page_content",
|
|
210
|
+
"node.text_resource.text",
|
|
211
|
+
"document.text",
|
|
212
|
+
"document.page_content",
|
|
213
|
+
"document.content",
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
for path in candidates:
|
|
217
|
+
value = _get_path(item, path)
|
|
218
|
+
cleaned = _to_clean_string(value)
|
|
219
|
+
|
|
220
|
+
if cleaned:
|
|
221
|
+
return cleaned
|
|
222
|
+
|
|
223
|
+
# Object fallback for LangChain-style Document.
|
|
224
|
+
for attr in ("page_content", "text", "content"):
|
|
225
|
+
value = getattr(item, attr, None)
|
|
226
|
+
cleaned = _to_clean_string(value)
|
|
227
|
+
|
|
228
|
+
if cleaned:
|
|
229
|
+
return cleaned
|
|
230
|
+
|
|
231
|
+
# LlamaIndex-style node sometimes exposes get_content().
|
|
232
|
+
get_content = getattr(item, "get_content", None)
|
|
233
|
+
if callable(get_content):
|
|
234
|
+
cleaned = _to_clean_string(get_content())
|
|
235
|
+
if cleaned:
|
|
236
|
+
return cleaned
|
|
237
|
+
|
|
238
|
+
node = getattr(item, "node", None)
|
|
239
|
+
if node is not None:
|
|
240
|
+
get_content = getattr(node, "get_content", None)
|
|
241
|
+
if callable(get_content):
|
|
242
|
+
cleaned = _to_clean_string(get_content())
|
|
243
|
+
if cleaned:
|
|
244
|
+
return cleaned
|
|
245
|
+
|
|
246
|
+
return ""
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _extract_metadata(
|
|
250
|
+
item: Any,
|
|
251
|
+
explicit: Optional[ValueExtractor],
|
|
252
|
+
) -> Dict[str, Any]:
|
|
253
|
+
if explicit is not None:
|
|
254
|
+
value = _extract_with_spec(item, explicit)
|
|
255
|
+
return _to_metadata_dict(value)
|
|
256
|
+
|
|
257
|
+
candidates = [
|
|
258
|
+
"metadata",
|
|
259
|
+
"meta",
|
|
260
|
+
"extra_info",
|
|
261
|
+
"node.metadata",
|
|
262
|
+
"node.meta",
|
|
263
|
+
"document.metadata",
|
|
264
|
+
"document.meta",
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
merged: Dict[str, Any] = {}
|
|
268
|
+
|
|
269
|
+
for path in candidates:
|
|
270
|
+
value = _get_path(item, path)
|
|
271
|
+
if isinstance(value, Mapping):
|
|
272
|
+
merged.update(dict(value))
|
|
273
|
+
|
|
274
|
+
# Object fallback.
|
|
275
|
+
for attr in ("metadata", "meta", "extra_info"):
|
|
276
|
+
value = getattr(item, attr, None)
|
|
277
|
+
if isinstance(value, Mapping):
|
|
278
|
+
merged.update(dict(value))
|
|
279
|
+
|
|
280
|
+
node = getattr(item, "node", None)
|
|
281
|
+
if node is not None:
|
|
282
|
+
for attr in ("metadata", "meta", "extra_info"):
|
|
283
|
+
value = getattr(node, attr, None)
|
|
284
|
+
if isinstance(value, Mapping):
|
|
285
|
+
merged.update(dict(value))
|
|
286
|
+
|
|
287
|
+
return merged
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _extract_source(
|
|
291
|
+
item: Any,
|
|
292
|
+
metadata: Mapping[str, Any],
|
|
293
|
+
explicit: Optional[ValueExtractor],
|
|
294
|
+
) -> str:
|
|
295
|
+
if explicit is not None:
|
|
296
|
+
return _to_clean_string(_extract_with_spec(item, explicit))
|
|
297
|
+
|
|
298
|
+
direct_candidates = [
|
|
299
|
+
"source",
|
|
300
|
+
"file_name",
|
|
301
|
+
"filename",
|
|
302
|
+
"doc_id",
|
|
303
|
+
"document_id",
|
|
304
|
+
"uri",
|
|
305
|
+
"url",
|
|
306
|
+
"path",
|
|
307
|
+
"title",
|
|
308
|
+
"name",
|
|
309
|
+
"node.source",
|
|
310
|
+
"node.file_name",
|
|
311
|
+
"node.id_",
|
|
312
|
+
"document.source",
|
|
313
|
+
"document.file_name",
|
|
314
|
+
"document.id",
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
metadata_candidates = [
|
|
318
|
+
"source",
|
|
319
|
+
"file_name",
|
|
320
|
+
"filename",
|
|
321
|
+
"doc_id",
|
|
322
|
+
"document_id",
|
|
323
|
+
"file_id",
|
|
324
|
+
"uri",
|
|
325
|
+
"url",
|
|
326
|
+
"path",
|
|
327
|
+
"title",
|
|
328
|
+
"name",
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
for path in direct_candidates:
|
|
332
|
+
value = _to_clean_string(_get_path(item, path))
|
|
333
|
+
if value:
|
|
334
|
+
return value
|
|
335
|
+
|
|
336
|
+
for key in metadata_candidates:
|
|
337
|
+
value = _to_clean_string(metadata.get(key))
|
|
338
|
+
if value:
|
|
339
|
+
return value
|
|
340
|
+
|
|
341
|
+
return ""
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _extract_score(
|
|
345
|
+
item: Any,
|
|
346
|
+
*,
|
|
347
|
+
tuple_score: Any,
|
|
348
|
+
explicit: Optional[ValueExtractor],
|
|
349
|
+
) -> Optional[float]:
|
|
350
|
+
if explicit is not None:
|
|
351
|
+
return _to_float_or_none(_extract_with_spec(item, explicit))
|
|
352
|
+
|
|
353
|
+
tuple_score_float = _to_float_or_none(tuple_score)
|
|
354
|
+
if tuple_score_float is not None:
|
|
355
|
+
return tuple_score_float
|
|
356
|
+
|
|
357
|
+
candidates = [
|
|
358
|
+
"score",
|
|
359
|
+
"similarity",
|
|
360
|
+
"similarity_score",
|
|
361
|
+
"rerank_score",
|
|
362
|
+
"relevance_score",
|
|
363
|
+
"distance",
|
|
364
|
+
"metadata.score",
|
|
365
|
+
"metadata.similarity",
|
|
366
|
+
"metadata.similarity_score",
|
|
367
|
+
"metadata.rerank_score",
|
|
368
|
+
"meta.score",
|
|
369
|
+
"meta.similarity",
|
|
370
|
+
"node.score",
|
|
371
|
+
]
|
|
372
|
+
|
|
373
|
+
for path in candidates:
|
|
374
|
+
value = _to_float_or_none(_get_path(item, path))
|
|
375
|
+
if value is not None:
|
|
376
|
+
return value
|
|
377
|
+
|
|
378
|
+
for attr in (
|
|
379
|
+
"score",
|
|
380
|
+
"similarity",
|
|
381
|
+
"similarity_score",
|
|
382
|
+
"rerank_score",
|
|
383
|
+
"relevance_score",
|
|
384
|
+
"distance",
|
|
385
|
+
):
|
|
386
|
+
value = _to_float_or_none(getattr(item, attr, None))
|
|
387
|
+
if value is not None:
|
|
388
|
+
return value
|
|
389
|
+
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _extract_id(
|
|
394
|
+
item: Any,
|
|
395
|
+
explicit: Optional[ValueExtractor],
|
|
396
|
+
) -> str:
|
|
397
|
+
if explicit is not None:
|
|
398
|
+
return _to_clean_string(_extract_with_spec(item, explicit))
|
|
399
|
+
|
|
400
|
+
candidates = [
|
|
401
|
+
"id",
|
|
402
|
+
"chunk_id",
|
|
403
|
+
"doc_id",
|
|
404
|
+
"document_id",
|
|
405
|
+
"node_id",
|
|
406
|
+
"id_",
|
|
407
|
+
"node.id",
|
|
408
|
+
"node.id_",
|
|
409
|
+
"node.node_id",
|
|
410
|
+
"document.id",
|
|
411
|
+
"metadata.id",
|
|
412
|
+
"metadata.chunk_id",
|
|
413
|
+
"metadata.doc_id",
|
|
414
|
+
"metadata.document_id",
|
|
415
|
+
"meta.id",
|
|
416
|
+
"meta.chunk_id",
|
|
417
|
+
]
|
|
418
|
+
|
|
419
|
+
for path in candidates:
|
|
420
|
+
value = _to_clean_string(_get_path(item, path))
|
|
421
|
+
if value:
|
|
422
|
+
return value
|
|
423
|
+
|
|
424
|
+
for attr in ("id", "id_", "chunk_id", "doc_id", "document_id", "node_id"):
|
|
425
|
+
value = _to_clean_string(getattr(item, attr, None))
|
|
426
|
+
if value:
|
|
427
|
+
return value
|
|
428
|
+
|
|
429
|
+
return ""
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _extract_with_spec(item: Any, spec: Union[str, Callable[[Any], Any]]) -> Any:
|
|
433
|
+
if callable(spec):
|
|
434
|
+
return spec(item)
|
|
435
|
+
|
|
436
|
+
return _get_path(item, spec)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _get_path(item: Any, path: str) -> Any:
|
|
440
|
+
current = item
|
|
441
|
+
|
|
442
|
+
for part in path.split("."):
|
|
443
|
+
if current is None:
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
if isinstance(current, Mapping):
|
|
447
|
+
current = current.get(part)
|
|
448
|
+
continue
|
|
449
|
+
|
|
450
|
+
current = getattr(current, part, None)
|
|
451
|
+
|
|
452
|
+
return current
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _to_clean_string(value: Any) -> str:
|
|
456
|
+
if value is None:
|
|
457
|
+
return ""
|
|
458
|
+
|
|
459
|
+
if isinstance(value, str):
|
|
460
|
+
return value.strip()
|
|
461
|
+
|
|
462
|
+
if isinstance(value, (int, float)):
|
|
463
|
+
return str(value)
|
|
464
|
+
|
|
465
|
+
return ""
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _to_float_or_none(value: Any) -> Optional[float]:
|
|
469
|
+
if value is None:
|
|
470
|
+
return None
|
|
471
|
+
|
|
472
|
+
if isinstance(value, bool):
|
|
473
|
+
return None
|
|
474
|
+
|
|
475
|
+
if isinstance(value, (int, float)):
|
|
476
|
+
return float(value)
|
|
477
|
+
|
|
478
|
+
if isinstance(value, str) and value.strip():
|
|
479
|
+
try:
|
|
480
|
+
return float(value.strip())
|
|
481
|
+
except ValueError:
|
|
482
|
+
return None
|
|
483
|
+
|
|
484
|
+
return None
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _to_metadata_dict(value: Any) -> Dict[str, Any]:
|
|
488
|
+
if value is None:
|
|
489
|
+
return {}
|
|
490
|
+
|
|
491
|
+
if isinstance(value, Mapping):
|
|
492
|
+
return dict(value)
|
|
493
|
+
|
|
494
|
+
return {"raw_metadata": value}
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _stable_chunk_id(text: str, source: str) -> str:
|
|
498
|
+
digest = hashlib.sha1(f"{source}\n{text}".encode("utf-8")).hexdigest()[:12]
|
|
499
|
+
return f"chunk_{digest}"
|
raglens/client.py
ADDED
|
File without changes
|
raglens/models.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field, asdict
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
JsonDict = Dict[str, Any]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def utc_now_iso() -> str:
|
|
14
|
+
"""Return current UTC time in ISO-8601 format."""
|
|
15
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def now_ms() -> float:
|
|
19
|
+
"""Return monotonic time in milliseconds."""
|
|
20
|
+
return time.perf_counter() * 1000
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def new_id(prefix: str) -> str:
|
|
24
|
+
"""Generate a simple prefixed UUID ID."""
|
|
25
|
+
return f"{prefix}_{uuid.uuid4().hex}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Span:
|
|
30
|
+
span_id: str
|
|
31
|
+
trace_id: str
|
|
32
|
+
parent_span_id: Optional[str]
|
|
33
|
+
type: str
|
|
34
|
+
name: str
|
|
35
|
+
status: str
|
|
36
|
+
input: JsonDict = field(default_factory=dict)
|
|
37
|
+
output: JsonDict = field(default_factory=dict)
|
|
38
|
+
metadata: JsonDict = field(default_factory=dict)
|
|
39
|
+
started_at: str = field(default_factory=utc_now_iso)
|
|
40
|
+
ended_at: Optional[str] = None
|
|
41
|
+
duration_ms: Optional[int] = None
|
|
42
|
+
error: Optional[JsonDict] = None
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> JsonDict:
|
|
45
|
+
return asdict(self)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class TraceRecord:
|
|
50
|
+
trace_id: str
|
|
51
|
+
name: str
|
|
52
|
+
status: str
|
|
53
|
+
input: JsonDict = field(default_factory=dict)
|
|
54
|
+
output: JsonDict = field(default_factory=dict)
|
|
55
|
+
metadata: JsonDict = field(default_factory=dict)
|
|
56
|
+
started_at: str = field(default_factory=utc_now_iso)
|
|
57
|
+
ended_at: Optional[str] = None
|
|
58
|
+
duration_ms: Optional[int] = None
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> JsonDict:
|
|
61
|
+
return asdict(self)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class TracePayload:
|
|
66
|
+
trace: TraceRecord
|
|
67
|
+
spans: List[Span] = field(default_factory=list)
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> JsonDict:
|
|
70
|
+
return {
|
|
71
|
+
"trace": self.trace.to_dict(),
|
|
72
|
+
"spans": [span.to_dict() for span in self.spans],
|
|
73
|
+
}
|
raglens/trace.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import traceback
|
|
8
|
+
import urllib.request
|
|
9
|
+
import urllib.error
|
|
10
|
+
|
|
11
|
+
from .models import (
|
|
12
|
+
JsonDict,
|
|
13
|
+
Span,
|
|
14
|
+
TraceRecord,
|
|
15
|
+
TracePayload,
|
|
16
|
+
new_id,
|
|
17
|
+
now_ms,
|
|
18
|
+
utc_now_iso,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RAGLensTrace:
|
|
23
|
+
"""
|
|
24
|
+
A lightweight trace context manager for recording one RAG request.
|
|
25
|
+
|
|
26
|
+
Public API example:
|
|
27
|
+
|
|
28
|
+
from sledtrace import trace
|
|
29
|
+
|
|
30
|
+
with trace("refund-policy-qa") as t:
|
|
31
|
+
t.retrieval(query="...", chunks=[...])
|
|
32
|
+
t.llm(model="...", prompt="...", response="...")
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
name: str,
|
|
38
|
+
query: Optional[str] = None,
|
|
39
|
+
metadata: Optional[JsonDict] = None,
|
|
40
|
+
collector_url: Optional[str] = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.trace_id = new_id("trace")
|
|
43
|
+
self.name = name
|
|
44
|
+
self.query = query
|
|
45
|
+
self.metadata = metadata or {}
|
|
46
|
+
self.collector_url = resolve_collector_url(collector_url)
|
|
47
|
+
|
|
48
|
+
self._started_at: Optional[str] = None
|
|
49
|
+
self._ended_at: Optional[str] = None
|
|
50
|
+
self._start_ms: Optional[float] = None
|
|
51
|
+
self._duration_ms: Optional[int] = None
|
|
52
|
+
|
|
53
|
+
self._status = "ok"
|
|
54
|
+
self._output: JsonDict = {}
|
|
55
|
+
self._spans: List[Span] = []
|
|
56
|
+
self._error: Optional[JsonDict] = None
|
|
57
|
+
|
|
58
|
+
def __enter__(self) -> "RAGLensTrace":
|
|
59
|
+
self._started_at = utc_now_iso()
|
|
60
|
+
self._start_ms = now_ms()
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool:
|
|
64
|
+
self._ended_at = utc_now_iso()
|
|
65
|
+
|
|
66
|
+
if self._start_ms is not None:
|
|
67
|
+
self._duration_ms = int(now_ms() - self._start_ms)
|
|
68
|
+
|
|
69
|
+
if exc is not None:
|
|
70
|
+
self._status = "error"
|
|
71
|
+
self._error = {
|
|
72
|
+
"type": exc_type.__name__ if exc_type else "Error",
|
|
73
|
+
"message": str(exc),
|
|
74
|
+
"stack": "".join(traceback.format_exception(exc_type, exc, tb)),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Do not suppress exceptions.
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def retrieval(
|
|
81
|
+
self,
|
|
82
|
+
query: str,
|
|
83
|
+
chunks: List[JsonDict],
|
|
84
|
+
name: str = "retrieval",
|
|
85
|
+
top_k: Optional[int] = None,
|
|
86
|
+
metadata: Optional[JsonDict] = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Record a retrieval span.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
query: Query sent to retriever.
|
|
93
|
+
chunks: Retrieved chunks.
|
|
94
|
+
name: Human-readable span name.
|
|
95
|
+
top_k: Number of requested chunks.
|
|
96
|
+
metadata: Retriever metadata.
|
|
97
|
+
"""
|
|
98
|
+
start = now_ms()
|
|
99
|
+
started_at = utc_now_iso()
|
|
100
|
+
|
|
101
|
+
normalized_chunks = self._normalize_chunks(chunks)
|
|
102
|
+
|
|
103
|
+
span_input: JsonDict = {
|
|
104
|
+
"query": query,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if top_k is not None:
|
|
108
|
+
span_input["top_k"] = top_k
|
|
109
|
+
|
|
110
|
+
span = Span(
|
|
111
|
+
span_id=new_id("span"),
|
|
112
|
+
trace_id=self.trace_id,
|
|
113
|
+
parent_span_id=None,
|
|
114
|
+
type="retrieval",
|
|
115
|
+
name=name,
|
|
116
|
+
status="ok",
|
|
117
|
+
input=span_input,
|
|
118
|
+
output={
|
|
119
|
+
"chunks": normalized_chunks,
|
|
120
|
+
},
|
|
121
|
+
metadata=metadata or {},
|
|
122
|
+
started_at=started_at,
|
|
123
|
+
ended_at=utc_now_iso(),
|
|
124
|
+
duration_ms=int(now_ms() - start),
|
|
125
|
+
error=None,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self._spans.append(span)
|
|
129
|
+
|
|
130
|
+
if self.query is None:
|
|
131
|
+
self.query = query
|
|
132
|
+
|
|
133
|
+
def llm(
|
|
134
|
+
self,
|
|
135
|
+
model: str,
|
|
136
|
+
prompt: Optional[str] = None,
|
|
137
|
+
response: Optional[str] = None,
|
|
138
|
+
messages: Optional[List[JsonDict]] = None,
|
|
139
|
+
name: str = "llm",
|
|
140
|
+
provider: Optional[str] = None,
|
|
141
|
+
input_tokens: Optional[int] = None,
|
|
142
|
+
output_tokens: Optional[int] = None,
|
|
143
|
+
latency_ms: Optional[int] = None,
|
|
144
|
+
metadata: Optional[JsonDict] = None,
|
|
145
|
+
) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Record an LLM span.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
model: Model name.
|
|
151
|
+
prompt: Prompt text.
|
|
152
|
+
response: Model response text.
|
|
153
|
+
messages: Optional chat messages.
|
|
154
|
+
name: Human-readable span name.
|
|
155
|
+
provider: LLM provider.
|
|
156
|
+
input_tokens: Input token count.
|
|
157
|
+
output_tokens: Output token count.
|
|
158
|
+
latency_ms: LLM call latency.
|
|
159
|
+
metadata: Additional metadata.
|
|
160
|
+
"""
|
|
161
|
+
start = now_ms()
|
|
162
|
+
started_at = utc_now_iso()
|
|
163
|
+
|
|
164
|
+
span_input: JsonDict = {
|
|
165
|
+
"model": model,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if prompt is not None:
|
|
169
|
+
span_input["prompt"] = prompt
|
|
170
|
+
|
|
171
|
+
if messages is not None:
|
|
172
|
+
span_input["messages"] = messages
|
|
173
|
+
|
|
174
|
+
span_output: JsonDict = {}
|
|
175
|
+
|
|
176
|
+
if response is not None:
|
|
177
|
+
span_output["response"] = response
|
|
178
|
+
self._output["answer"] = response
|
|
179
|
+
|
|
180
|
+
span_metadata: JsonDict = metadata.copy() if metadata else {}
|
|
181
|
+
|
|
182
|
+
if provider is not None:
|
|
183
|
+
span_metadata["provider"] = provider
|
|
184
|
+
|
|
185
|
+
if input_tokens is not None:
|
|
186
|
+
span_metadata["input_tokens"] = input_tokens
|
|
187
|
+
|
|
188
|
+
if output_tokens is not None:
|
|
189
|
+
span_metadata["output_tokens"] = output_tokens
|
|
190
|
+
|
|
191
|
+
if input_tokens is not None and output_tokens is not None:
|
|
192
|
+
span_metadata["total_tokens"] = input_tokens + output_tokens
|
|
193
|
+
|
|
194
|
+
if latency_ms is not None:
|
|
195
|
+
span_metadata["latency_ms"] = latency_ms
|
|
196
|
+
|
|
197
|
+
span = Span(
|
|
198
|
+
span_id=new_id("span"),
|
|
199
|
+
trace_id=self.trace_id,
|
|
200
|
+
parent_span_id=None,
|
|
201
|
+
type="llm",
|
|
202
|
+
name=name,
|
|
203
|
+
status="ok",
|
|
204
|
+
input=span_input,
|
|
205
|
+
output=span_output,
|
|
206
|
+
metadata=span_metadata,
|
|
207
|
+
started_at=started_at,
|
|
208
|
+
ended_at=utc_now_iso(),
|
|
209
|
+
duration_ms=latency_ms if latency_ms is not None else int(now_ms() - start),
|
|
210
|
+
error=None,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
self._spans.append(span)
|
|
214
|
+
|
|
215
|
+
def log_answer(self, answer: str) -> None:
|
|
216
|
+
"""Record the final answer at trace level."""
|
|
217
|
+
self._output["answer"] = answer
|
|
218
|
+
|
|
219
|
+
def to_payload(self) -> TracePayload:
|
|
220
|
+
trace_input: JsonDict = {}
|
|
221
|
+
|
|
222
|
+
if self.query is not None:
|
|
223
|
+
trace_input["query"] = self.query
|
|
224
|
+
|
|
225
|
+
trace_metadata = {
|
|
226
|
+
"sdk_language": "python",
|
|
227
|
+
"sdk_version": "0.7.0",
|
|
228
|
+
**self.metadata,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if self._error is not None:
|
|
232
|
+
trace_metadata["error"] = self._error
|
|
233
|
+
|
|
234
|
+
record = TraceRecord(
|
|
235
|
+
trace_id=self.trace_id,
|
|
236
|
+
name=self.name,
|
|
237
|
+
status=self._status,
|
|
238
|
+
input=trace_input,
|
|
239
|
+
output=self._output,
|
|
240
|
+
metadata=trace_metadata,
|
|
241
|
+
started_at=self._started_at or utc_now_iso(),
|
|
242
|
+
ended_at=self._ended_at,
|
|
243
|
+
duration_ms=self._duration_ms,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return TracePayload(trace=record, spans=self._spans)
|
|
247
|
+
|
|
248
|
+
def to_dict(self) -> JsonDict:
|
|
249
|
+
return self.to_payload().to_dict()
|
|
250
|
+
|
|
251
|
+
def to_json(self, indent: int = 2) -> str:
|
|
252
|
+
return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)
|
|
253
|
+
|
|
254
|
+
def print_json(self) -> None:
|
|
255
|
+
print(self.to_json())
|
|
256
|
+
|
|
257
|
+
def flush(self, collector_url: Optional[str] = None, timeout: float = 5.0) -> JsonDict:
|
|
258
|
+
"""
|
|
259
|
+
Send the trace payload to the local SledTrace collector.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
collector_url: Optional collector base URL. Defaults to self.collector_url.
|
|
263
|
+
timeout: HTTP timeout in seconds.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Collector JSON response.
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
RuntimeError: If the collector request fails.
|
|
270
|
+
"""
|
|
271
|
+
base_url = (collector_url or self.collector_url).rstrip("/")
|
|
272
|
+
url = f"{base_url}/api/traces"
|
|
273
|
+
|
|
274
|
+
data = json.dumps(self.to_dict()).encode("utf-8")
|
|
275
|
+
|
|
276
|
+
request = urllib.request.Request(
|
|
277
|
+
url=url,
|
|
278
|
+
data=data,
|
|
279
|
+
method="POST",
|
|
280
|
+
headers={
|
|
281
|
+
"Content-Type": "application/json",
|
|
282
|
+
"User-Agent": "sledtrace-python-sdk/0.7.0",
|
|
283
|
+
},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
288
|
+
response_body = response.read().decode("utf-8")
|
|
289
|
+
if not response_body:
|
|
290
|
+
return {}
|
|
291
|
+
|
|
292
|
+
return json.loads(response_body)
|
|
293
|
+
|
|
294
|
+
except urllib.error.HTTPError as exc:
|
|
295
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
296
|
+
raise RuntimeError(
|
|
297
|
+
f"SledTrace collector returned HTTP {exc.code}: {body}"
|
|
298
|
+
) from exc
|
|
299
|
+
|
|
300
|
+
except urllib.error.URLError as exc:
|
|
301
|
+
raise RuntimeError(
|
|
302
|
+
f"Failed to connect to SledTrace collector at {url}: {exc.reason}"
|
|
303
|
+
) from exc
|
|
304
|
+
|
|
305
|
+
def _normalize_chunks(self, chunks: List[JsonDict]) -> List[JsonDict]:
|
|
306
|
+
normalized: List[JsonDict] = []
|
|
307
|
+
|
|
308
|
+
for index, chunk in enumerate(chunks):
|
|
309
|
+
normalized_chunk = dict(chunk)
|
|
310
|
+
|
|
311
|
+
if "rank" not in normalized_chunk:
|
|
312
|
+
normalized_chunk["rank"] = index + 1
|
|
313
|
+
|
|
314
|
+
if "metadata" not in normalized_chunk or normalized_chunk["metadata"] is None:
|
|
315
|
+
normalized_chunk["metadata"] = {}
|
|
316
|
+
|
|
317
|
+
normalized.append(normalized_chunk)
|
|
318
|
+
|
|
319
|
+
return normalized
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def trace(
|
|
323
|
+
name: str,
|
|
324
|
+
query: Optional[str] = None,
|
|
325
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
326
|
+
collector_url: Optional[str] = None,
|
|
327
|
+
) -> RAGLensTrace:
|
|
328
|
+
"""
|
|
329
|
+
Create a SledTrace trace context manager.
|
|
330
|
+
"""
|
|
331
|
+
return RAGLensTrace(name=name, query=query, metadata=metadata, collector_url=collector_url,)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
SledTraceTrace = RAGLensTrace
|
|
335
|
+
|
|
336
|
+
_DEFAULT_COLLECTOR_URL = "http://localhost:4319"
|
|
337
|
+
_LEGACY_COLLECTOR_ENV = "RAGLENS_COLLECTOR_URL"
|
|
338
|
+
_NEW_COLLECTOR_ENV = "SLEDTRACE_COLLECTOR_URL"
|
|
339
|
+
_legacy_env_notice_emitted = False
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def resolve_collector_url(explicit_url: Optional[str]) -> str:
|
|
343
|
+
if explicit_url:
|
|
344
|
+
return explicit_url
|
|
345
|
+
|
|
346
|
+
new_value = os.getenv(_NEW_COLLECTOR_ENV)
|
|
347
|
+
if new_value:
|
|
348
|
+
return new_value
|
|
349
|
+
|
|
350
|
+
legacy_value = os.getenv(_LEGACY_COLLECTOR_ENV)
|
|
351
|
+
if legacy_value:
|
|
352
|
+
emit_legacy_env_warning_once()
|
|
353
|
+
return legacy_value
|
|
354
|
+
|
|
355
|
+
return _DEFAULT_COLLECTOR_URL
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def emit_legacy_env_warning_once() -> None:
|
|
359
|
+
global _legacy_env_notice_emitted
|
|
360
|
+
if _legacy_env_notice_emitted:
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
_legacy_env_notice_emitted = True
|
|
364
|
+
print(
|
|
365
|
+
"DEPRECATED: RAGLENS_COLLECTOR_URL is deprecated; use SLEDTRACE_COLLECTOR_URL instead.",
|
|
366
|
+
file=sys.stderr,
|
|
367
|
+
)
|
sledtrace/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
with warnings.catch_warnings():
|
|
4
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning, module=r"raglens.*")
|
|
5
|
+
from raglens import (
|
|
6
|
+
ChunkNormalizationError,
|
|
7
|
+
RAGLensTrace,
|
|
8
|
+
SledTraceTrace,
|
|
9
|
+
normalize_chunk,
|
|
10
|
+
normalize_chunks,
|
|
11
|
+
trace,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__version__ = "0.7.0"
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"__version__",
|
|
18
|
+
"trace",
|
|
19
|
+
"SledTraceTrace",
|
|
20
|
+
"RAGLensTrace",
|
|
21
|
+
"ChunkNormalizationError",
|
|
22
|
+
"normalize_chunk",
|
|
23
|
+
"normalize_chunks",
|
|
24
|
+
]
|
sledtrace/cli.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterable, Optional
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
REPO_MARKERS = (
|
|
13
|
+
Path("AGENTS.md"),
|
|
14
|
+
Path("docker-compose.yml"),
|
|
15
|
+
Path("scripts") / "start-sledtrace.py",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
SERVE_CHECKOUT_ERROR = (
|
|
19
|
+
"sledtrace serve currently requires a SledTrace source checkout. "
|
|
20
|
+
"Run it from the repository, or use Docker Compose from the repository root. "
|
|
21
|
+
"Standalone wheel-installed serving is not supported by this package."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _candidate_directories(start: Path) -> Iterable[Path]:
|
|
26
|
+
current = start.resolve()
|
|
27
|
+
if current.is_file():
|
|
28
|
+
current = current.parent
|
|
29
|
+
|
|
30
|
+
yield current
|
|
31
|
+
yield from current.parents
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def find_repo_root(start: Optional[Path] = None) -> Optional[Path]:
|
|
35
|
+
"""Find a SledTrace source checkout at or above the starting directory."""
|
|
36
|
+
for candidate in _candidate_directories(start or Path.cwd()):
|
|
37
|
+
if all((candidate / marker).exists() for marker in REPO_MARKERS):
|
|
38
|
+
return candidate
|
|
39
|
+
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def serve() -> int:
|
|
44
|
+
repo_root = find_repo_root()
|
|
45
|
+
if repo_root is None:
|
|
46
|
+
print(SERVE_CHECKOUT_ERROR, file=sys.stderr)
|
|
47
|
+
return 1
|
|
48
|
+
|
|
49
|
+
startup_script = repo_root / "scripts" / "start-sledtrace.py"
|
|
50
|
+
|
|
51
|
+
print("Starting SledTrace local stack...")
|
|
52
|
+
return subprocess.call([sys.executable, str(startup_script)])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def show_version() -> int:
|
|
56
|
+
print(__version__)
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
61
|
+
parser = argparse.ArgumentParser(
|
|
62
|
+
prog="sledtrace",
|
|
63
|
+
description="Inspect and run SledTrace local developer tooling.",
|
|
64
|
+
)
|
|
65
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
66
|
+
|
|
67
|
+
serve_parser = subparsers.add_parser(
|
|
68
|
+
"serve",
|
|
69
|
+
help="Start the collector and dashboard from a SledTrace source checkout",
|
|
70
|
+
description=(
|
|
71
|
+
"Start the local collector and dashboard. This command must be run "
|
|
72
|
+
"from inside a SledTrace source checkout; standalone wheel-installed "
|
|
73
|
+
"serving is not supported by this package."
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
serve_parser.set_defaults(func=lambda _args: serve())
|
|
77
|
+
|
|
78
|
+
version_parser = subparsers.add_parser(
|
|
79
|
+
"version",
|
|
80
|
+
help="Print the installed SledTrace package version",
|
|
81
|
+
description="Print the installed SledTrace package version.",
|
|
82
|
+
)
|
|
83
|
+
version_parser.set_defaults(func=lambda _args: show_version())
|
|
84
|
+
|
|
85
|
+
return parser
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
89
|
+
parser = build_parser()
|
|
90
|
+
args = parser.parse_args(argv)
|
|
91
|
+
|
|
92
|
+
if not hasattr(args, "func"):
|
|
93
|
+
parser.print_help()
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
return args.func(args)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sledtrace
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Local-first tracing and debugging SDK for RAG pipelines
|
|
5
|
+
Author: SledTrace Contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Schromeo/SledTrace
|
|
8
|
+
Project-URL: Documentation, https://github.com/Schromeo/SledTrace#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/Schromeo/SledTrace
|
|
10
|
+
Project-URL: Issues, https://github.com/Schromeo/SledTrace/issues
|
|
11
|
+
Keywords: rag,observability,tracing,llm,ai,debugging
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# SledTrace Python SDK
|
|
30
|
+
|
|
31
|
+
SledTrace is a local-first observability and debugging SDK for RAG pipelines.
|
|
32
|
+
|
|
33
|
+
Current release: **0.7.0 — External Developer Readiness**
|
|
34
|
+
|
|
35
|
+
Project and visual overview: [github.com/Schromeo/SledTrace](https://github.com/Schromeo/SledTrace)
|
|
36
|
+
|
|
37
|
+
## Distribution status
|
|
38
|
+
|
|
39
|
+
Install the released SDK and CLI from production PyPI:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python -m pip install sledtrace==0.7.0
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The immutable `0.7.0rc1` publication candidate remains available on [TestPyPI](https://test.pypi.org/project/sledtrace/0.7.0rc1/) for release-history purposes.
|
|
46
|
+
|
|
47
|
+
## Install from source for development
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cd sdk/python
|
|
51
|
+
python -m pip install -e .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Build a local wheel or sdist
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cd sdk/python
|
|
58
|
+
python -m pip install --upgrade pip
|
|
59
|
+
python -m pip install build
|
|
60
|
+
python -m build
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This produces wheel and source-distribution artifacts in `dist/`.
|
|
64
|
+
|
|
65
|
+
## Install the built wheel
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python -m pip install dist/*.whl
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## CLI
|
|
72
|
+
|
|
73
|
+
Editable and wheel installations provide:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
sledtrace --help
|
|
77
|
+
sledtrace serve --help
|
|
78
|
+
sledtrace version
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`sledtrace version` reports `0.7.0` for this release.
|
|
82
|
+
|
|
83
|
+
`sledtrace serve` must be run from inside a SledTrace source checkout. It locates the repository from the current working directory and delegates to `scripts/start-sledtrace.py`. The wheel does not bundle the Collector, Dashboard, Docker assets, or a standalone serving runtime; outside a checkout, `serve` exits with actionable guidance.
|
|
84
|
+
|
|
85
|
+
## Basic usage
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from sledtrace import trace
|
|
89
|
+
|
|
90
|
+
with trace("example") as t:
|
|
91
|
+
t.retrieval(
|
|
92
|
+
query="What is the refund policy?",
|
|
93
|
+
chunks=[
|
|
94
|
+
{
|
|
95
|
+
"id": "chunk-1",
|
|
96
|
+
"text": "Refunds are accepted within 30 days with proof of purchase.",
|
|
97
|
+
"score": 0.92,
|
|
98
|
+
"metadata": {"source": "refund_policy.md"},
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
top_k=1,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
t.llm(
|
|
105
|
+
model="demo-model",
|
|
106
|
+
prompt="Question: What is the refund policy?",
|
|
107
|
+
response="Refunds are accepted within 30 days with proof of purchase.",
|
|
108
|
+
provider="local-demo",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
t.flush()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Collector URL configuration
|
|
115
|
+
|
|
116
|
+
The default collector URL is `http://localhost:4319`.
|
|
117
|
+
|
|
118
|
+
Use the SledTrace environment variable:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
export SLEDTRACE_COLLECTOR_URL=http://localhost:4319
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
PowerShell:
|
|
125
|
+
|
|
126
|
+
```powershell
|
|
127
|
+
$env:SLEDTRACE_COLLECTOR_URL="http://localhost:4319"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Legacy compatibility remains temporarily supported for migration:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
export RAGLENS_COLLECTOR_URL=http://localhost:4319
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The precedence is:
|
|
137
|
+
|
|
138
|
+
1. `SLEDTRACE_COLLECTOR_URL`
|
|
139
|
+
2. `RAGLENS_COLLECTOR_URL`
|
|
140
|
+
3. `http://localhost:4319`
|
|
141
|
+
|
|
142
|
+
## Legacy compatibility note
|
|
143
|
+
|
|
144
|
+
Legacy `raglens` imports remain temporarily supported during migration, but new code should use the SledTrace package path:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from sledtrace import trace
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## More docs
|
|
151
|
+
|
|
152
|
+
- [Full project README and screenshots](https://github.com/Schromeo/SledTrace#readme)
|
|
153
|
+
- [User onboarding guide](https://github.com/Schromeo/SledTrace/blob/main/docs/product/USER_ONBOARDING.md)
|
|
154
|
+
- [Python SDK integration guide](https://github.com/Schromeo/SledTrace/blob/main/docs/integrations/PYTHON_SDK_GUIDE.md)
|
|
155
|
+
- [v0.7.0 release](https://github.com/Schromeo/SledTrace/releases/tag/v0.7.0)
|
|
156
|
+
|
|
157
|
+
Repository examples such as `examples.custom_pipeline_demo` are local developer examples and not a separate public SDK surface.
|
|
158
|
+
|
|
159
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
raglens/__init__.py,sha256=Z1XUqDRZZGWmQekzsrvyv2V-Ht5PdRVdGHxTq3vUHBY,534
|
|
2
|
+
raglens/chunks.py,sha256=aya7qsXMHfyZB1sCP5q1byhleYahnESqm2JHurwKlPg,12400
|
|
3
|
+
raglens/client.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
raglens/models.py,sha256=3dFtLmj2eI29kWihFsoYaHsv7jnuBJlzcZI8V83Yk8E,1837
|
|
5
|
+
raglens/trace.py,sha256=soxr-Sxcs_USGgdrnkQj34y7uvDio1ZhNAmIp1HuNOQ,10479
|
|
6
|
+
sledtrace/__init__.py,sha256=go1T0aqaDKtfxsWML7_m720eqNyJJpfFq1mxtEegZ14,504
|
|
7
|
+
sledtrace/cli.py,sha256=VDOJ5d3cqLweyNar0F2vrckJQCVoLfYvBDlyyIx8kNA,2758
|
|
8
|
+
sledtrace-0.7.0.dist-info/licenses/LICENSE,sha256=BNRnIjkX59ltbQ3sxGUnO6nXWVZYJRCe83qZcnnnNvU,1079
|
|
9
|
+
sledtrace-0.7.0.dist-info/METADATA,sha256=wmB0aAG2eaqXaD7-kFcoSYG0ICEze6yGSnU_Ba2ijQc,4529
|
|
10
|
+
sledtrace-0.7.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
11
|
+
sledtrace-0.7.0.dist-info/entry_points.txt,sha256=sYh3wzoWaFLUWUnclK-5eUjygRblXvypCG6PR9tN2CQ,49
|
|
12
|
+
sledtrace-0.7.0.dist-info/top_level.txt,sha256=lGkXnSSWTdqDYLx4vNPEtle-wL5YBjmR_7ZMraOdrhY,18
|
|
13
|
+
sledtrace-0.7.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SledTrace contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|