raglens-sdk 0.1.0__tar.gz
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_sdk-0.1.0/PKG-INFO +65 -0
- raglens_sdk-0.1.0/README.md +52 -0
- raglens_sdk-0.1.0/pyproject.toml +18 -0
- raglens_sdk-0.1.0/raglens/__init__.py +9 -0
- raglens_sdk-0.1.0/raglens/client.py +44 -0
- raglens_sdk-0.1.0/raglens/helpers.py +32 -0
- raglens_sdk-0.1.0/raglens/serializers.py +20 -0
- raglens_sdk-0.1.0/raglens/span.py +45 -0
- raglens_sdk-0.1.0/raglens/trace.py +72 -0
- raglens_sdk-0.1.0/raglens_sdk.egg-info/PKG-INFO +65 -0
- raglens_sdk-0.1.0/raglens_sdk.egg-info/SOURCES.txt +14 -0
- raglens_sdk-0.1.0/raglens_sdk.egg-info/dependency_links.txt +1 -0
- raglens_sdk-0.1.0/raglens_sdk.egg-info/requires.txt +6 -0
- raglens_sdk-0.1.0/raglens_sdk.egg-info/top_level.txt +1 -0
- raglens_sdk-0.1.0/setup.cfg +4 -0
- raglens_sdk-0.1.0/tests/test_sdk.py +80 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: raglens-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight tracing for RAG pipelines
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: httpx<1,>=0.27
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
11
|
+
Requires-Dist: build; extra == "dev"
|
|
12
|
+
Requires-Dist: twine; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# RAGLens Python SDK
|
|
15
|
+
|
|
16
|
+
The full copyable walkthrough is in the repository's
|
|
17
|
+
[quick-start guide](../../docs/QUICKSTART.md). It covers every SDK helper and
|
|
18
|
+
the real Qdrant/OpenRouter example.
|
|
19
|
+
|
|
20
|
+
Install from this repository: `pip install -e packages/sdk-python` (Python 3.9+).
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from raglens import RAGLens, log_generation
|
|
24
|
+
|
|
25
|
+
client = RAGLens() # reads RAGLENS_API_KEY and RAGLENS_BASE_URL
|
|
26
|
+
with client.trace("answer", input={"query": "What is RAG?"}) as trace:
|
|
27
|
+
with trace.span("llm", "generate") as span:
|
|
28
|
+
answer = "Retrieval-augmented generation"
|
|
29
|
+
log_generation(span, answer, {"input_tokens": 12, "output_tokens": 4}, model="mock")
|
|
30
|
+
trace.set_output({"answer": answer})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Spans nest automatically within a trace using context-local state. Use `span.span()`
|
|
34
|
+
for an explicit parent. Helpers `log_retrieval`, `log_context`, `log_prompt`, and
|
|
35
|
+
`log_generation` accept an existing span; matching `trace.log_*` methods create
|
|
36
|
+
instantaneous spans. Wrap the actual operation in a span for useful latency data.
|
|
37
|
+
Retrieval records require `chunk_id`, `document_id`, `document_name`, `content`, and
|
|
38
|
+
`score`; `rank` and `retrieval_method` are filled automatically. Set `selected` for
|
|
39
|
+
chunks used in context. `log_context` accepts `max_tokens` and a `chunks` list.
|
|
40
|
+
|
|
41
|
+
Set `RAGLENS_ENABLED=false` to disable delivery. Defaults: API URL
|
|
42
|
+
`http://localhost:8000`, enabled true, HTTP timeout two seconds. Constructor values
|
|
43
|
+
override environment variables. Delivery is synchronous on trace exit, without
|
|
44
|
+
retries (the ingestion API is not idempotent). Network, HTTP and serialization
|
|
45
|
+
failures warn without replacing the application's result or exception. Application
|
|
46
|
+
exceptions are recorded and propagate normally. Supply JSON-compatible dictionaries
|
|
47
|
+
for inputs, outputs, metrics and metadata; datetime and exception values serialize
|
|
48
|
+
into strings and structured errors. Avoid recording secrets or sensitive content.
|
|
49
|
+
|
|
50
|
+
After delivery `trace.id` contains the server-assigned ID. Set `project_id` and
|
|
51
|
+
optionally `web_url` on the client to populate `trace.url`.
|
|
52
|
+
|
|
53
|
+
## Tests and release
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
pip install -e 'packages/sdk-python[dev]'
|
|
57
|
+
pytest packages/sdk-python/tests
|
|
58
|
+
python -m build packages/sdk-python
|
|
59
|
+
python -m twine check packages/sdk-python/dist/*
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Version is maintained in `pyproject.toml`; `raglens.__version__` reads the installed
|
|
63
|
+
metadata. For a release, bump that version, test, build into a clean `dist/`, then
|
|
64
|
+
upload the reviewed wheel and sdist using `python -m twine upload ...` with your
|
|
65
|
+
registry credentials. No package has been published by this implementation.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# RAGLens Python SDK
|
|
2
|
+
|
|
3
|
+
The full copyable walkthrough is in the repository's
|
|
4
|
+
[quick-start guide](../../docs/QUICKSTART.md). It covers every SDK helper and
|
|
5
|
+
the real Qdrant/OpenRouter example.
|
|
6
|
+
|
|
7
|
+
Install from this repository: `pip install -e packages/sdk-python` (Python 3.9+).
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from raglens import RAGLens, log_generation
|
|
11
|
+
|
|
12
|
+
client = RAGLens() # reads RAGLENS_API_KEY and RAGLENS_BASE_URL
|
|
13
|
+
with client.trace("answer", input={"query": "What is RAG?"}) as trace:
|
|
14
|
+
with trace.span("llm", "generate") as span:
|
|
15
|
+
answer = "Retrieval-augmented generation"
|
|
16
|
+
log_generation(span, answer, {"input_tokens": 12, "output_tokens": 4}, model="mock")
|
|
17
|
+
trace.set_output({"answer": answer})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Spans nest automatically within a trace using context-local state. Use `span.span()`
|
|
21
|
+
for an explicit parent. Helpers `log_retrieval`, `log_context`, `log_prompt`, and
|
|
22
|
+
`log_generation` accept an existing span; matching `trace.log_*` methods create
|
|
23
|
+
instantaneous spans. Wrap the actual operation in a span for useful latency data.
|
|
24
|
+
Retrieval records require `chunk_id`, `document_id`, `document_name`, `content`, and
|
|
25
|
+
`score`; `rank` and `retrieval_method` are filled automatically. Set `selected` for
|
|
26
|
+
chunks used in context. `log_context` accepts `max_tokens` and a `chunks` list.
|
|
27
|
+
|
|
28
|
+
Set `RAGLENS_ENABLED=false` to disable delivery. Defaults: API URL
|
|
29
|
+
`http://localhost:8000`, enabled true, HTTP timeout two seconds. Constructor values
|
|
30
|
+
override environment variables. Delivery is synchronous on trace exit, without
|
|
31
|
+
retries (the ingestion API is not idempotent). Network, HTTP and serialization
|
|
32
|
+
failures warn without replacing the application's result or exception. Application
|
|
33
|
+
exceptions are recorded and propagate normally. Supply JSON-compatible dictionaries
|
|
34
|
+
for inputs, outputs, metrics and metadata; datetime and exception values serialize
|
|
35
|
+
into strings and structured errors. Avoid recording secrets or sensitive content.
|
|
36
|
+
|
|
37
|
+
After delivery `trace.id` contains the server-assigned ID. Set `project_id` and
|
|
38
|
+
optionally `web_url` on the client to populate `trace.url`.
|
|
39
|
+
|
|
40
|
+
## Tests and release
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
pip install -e 'packages/sdk-python[dev]'
|
|
44
|
+
pytest packages/sdk-python/tests
|
|
45
|
+
python -m build packages/sdk-python
|
|
46
|
+
python -m twine check packages/sdk-python/dist/*
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Version is maintained in `pyproject.toml`; `raglens.__version__` reads the installed
|
|
50
|
+
metadata. For a release, bump that version, test, build into a clean `dist/`, then
|
|
51
|
+
upload the reviewed wheel and sdist using `python -m twine upload ...` with your
|
|
52
|
+
registry credentials. No package has been published by this implementation.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name ="raglens-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lightweight tracing for RAG pipelines"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
dependencies = ["httpx>=0.27,<1"]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
dev = ["pytest>=8", "build", "twine"]
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
include = ["raglens*"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from importlib.metadata import version
|
|
2
|
+
|
|
3
|
+
from .client import RAGLens
|
|
4
|
+
from .helpers import log_context, log_generation, log_prompt, log_retrieval
|
|
5
|
+
from .span import Span
|
|
6
|
+
from .trace import Trace
|
|
7
|
+
|
|
8
|
+
__version__ = version("raglens")
|
|
9
|
+
__all__ = ["RAGLens", "Trace", "Span", "log_retrieval", "log_context", "log_prompt", "log_generation"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger("raglens")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RAGLens:
|
|
10
|
+
"""Synchronous, best-effort trace delivery with a bounded HTTP timeout."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, api_key=None, base_url=None, enabled=None, timeout=2.0,
|
|
13
|
+
project_id=None, web_url="http://localhost:3000", transport=None):
|
|
14
|
+
self.api_key = api_key if api_key is not None else os.getenv("RAGLENS_API_KEY", "")
|
|
15
|
+
self.base_url = (base_url or os.getenv("RAGLENS_BASE_URL", "http://localhost:8000")).rstrip("/")
|
|
16
|
+
self.enabled = (os.getenv("RAGLENS_ENABLED", "true").lower() not in
|
|
17
|
+
{"0", "false", "no", "off"}) if enabled is None else enabled
|
|
18
|
+
self.project_id = project_id
|
|
19
|
+
self.web_url = web_url.rstrip("/")
|
|
20
|
+
self.timeout = timeout
|
|
21
|
+
self.transport = transport
|
|
22
|
+
|
|
23
|
+
def trace(self, name, **kwargs):
|
|
24
|
+
from .trace import Trace
|
|
25
|
+
return Trace(self, name, **kwargs)
|
|
26
|
+
|
|
27
|
+
def send_trace(self, trace):
|
|
28
|
+
if not self.enabled:
|
|
29
|
+
return None
|
|
30
|
+
try:
|
|
31
|
+
if not self.api_key:
|
|
32
|
+
logger.warning("Trace delivery skipped: RAGLENS_API_KEY is not configured")
|
|
33
|
+
return None
|
|
34
|
+
from .serializers import serialize_trace
|
|
35
|
+
payload = serialize_trace(trace)
|
|
36
|
+
with httpx.Client(timeout=self.timeout, transport=self.transport) as client:
|
|
37
|
+
response = client.post(self.base_url + "/v1/traces", json=payload,
|
|
38
|
+
headers={"Authorization": "Bearer " + self.api_key})
|
|
39
|
+
response.raise_for_status()
|
|
40
|
+
return response.json()["trace_id"]
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
# Avoid logging payloads, credentials or exception URLs.
|
|
43
|
+
logger.warning("Trace delivery failed (%s)", type(exc).__name__)
|
|
44
|
+
return None
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Log structured data into existing spans so timing includes the actual work."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def log_retrieval(span, results, method="cosine", query=None):
|
|
5
|
+
span.retrieval_results = [dict(result, rank=result.get("rank", i),
|
|
6
|
+
retrieval_method=result.get("retrieval_method", method))
|
|
7
|
+
for i, result in enumerate(results, 1)]
|
|
8
|
+
span.set_attributes({"retrieval_method": method, "top_k": len(results)})
|
|
9
|
+
if query is not None:
|
|
10
|
+
span.input = {"query": query}
|
|
11
|
+
span.set_output({"results": span.retrieval_results})
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def log_context(span, context, tokens, max_tokens=None, chunks=None):
|
|
15
|
+
span.set_output({"context": context, "chunks": chunks or []})
|
|
16
|
+
span.set_attributes({"token_count": tokens, "max_tokens": max_tokens})
|
|
17
|
+
span.trace.metrics["context_tokens"] = span.trace.metrics.get("context_tokens", 0) + tokens
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def log_prompt(span, prompt, tokens=None, template_variables=None):
|
|
21
|
+
span.set_output({"messages" if isinstance(prompt, list) else "prompt": prompt})
|
|
22
|
+
span.set_attributes({"token_count": tokens, "template_variables": template_variables or {}})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def log_generation(span, response, tokens, model, provider="mock", **attributes):
|
|
26
|
+
usage = {"input_tokens": tokens, "output_tokens": 0} if isinstance(tokens, int) else dict(tokens)
|
|
27
|
+
span.set_output({"content": response})
|
|
28
|
+
span.set_attributes({"model": model, "provider": provider, **usage, **attributes})
|
|
29
|
+
for key in ("input_tokens", "output_tokens"):
|
|
30
|
+
span.trace.metrics[key] = span.trace.metrics.get(key, 0) + usage.get(key, 0)
|
|
31
|
+
span.trace.metrics["total_tokens"] = (span.trace.metrics.get("input_tokens", 0) +
|
|
32
|
+
span.trace.metrics.get("output_tokens", 0))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import date, datetime
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _default(value):
|
|
6
|
+
if isinstance(value, (datetime, date)):
|
|
7
|
+
return value.isoformat()
|
|
8
|
+
if isinstance(value, BaseException):
|
|
9
|
+
return {"type": type(value).__name__, "message": str(value)}
|
|
10
|
+
raise TypeError(f"Unsupported trace value: {type(value).__name__}")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def serialize_trace(trace):
|
|
14
|
+
fields = ("name", "session_id", "user_id", "started_at", "ended_at", "duration_ms",
|
|
15
|
+
"status", "input", "output", "metrics", "metadata")
|
|
16
|
+
payload = {key: getattr(trace, key) for key in fields}
|
|
17
|
+
span_fields = ("id", "parent_span_id", "type", "name", "started_at", "ended_at",
|
|
18
|
+
"duration_ms", "status", "input", "output", "attributes", "retrieval_results")
|
|
19
|
+
payload["spans"] = [{key: getattr(span, key) for key in span_fields} for span in trace.spans]
|
|
20
|
+
return json.loads(json.dumps(payload, default=_default, allow_nan=False))
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from time import perf_counter
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Span:
|
|
7
|
+
def __init__(self, trace, type, name, input=None, attributes=None, parent_span_id=None):
|
|
8
|
+
self.trace = trace
|
|
9
|
+
self.id = "span_" + uuid4().hex
|
|
10
|
+
self.type, self.name = type, name
|
|
11
|
+
self.parent_span_id = parent_span_id
|
|
12
|
+
self.input, self.output = input, None
|
|
13
|
+
self.attributes = dict(attributes or {})
|
|
14
|
+
self.retrieval_results = []
|
|
15
|
+
self.status = "success"
|
|
16
|
+
self.started_at = self.ended_at = None
|
|
17
|
+
self.duration_ms = 0
|
|
18
|
+
|
|
19
|
+
def __enter__(self):
|
|
20
|
+
self.started_at = datetime.now(timezone.utc)
|
|
21
|
+
self._start = perf_counter()
|
|
22
|
+
parent = self.trace._active_span.get()
|
|
23
|
+
if self.parent_span_id is None and parent is not None:
|
|
24
|
+
self.parent_span_id = parent.id
|
|
25
|
+
self._token = self.trace._active_span.set(self)
|
|
26
|
+
self.trace.spans.append(self)
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
def __exit__(self, exc_type, exc, tb):
|
|
30
|
+
self.ended_at = datetime.now(timezone.utc)
|
|
31
|
+
self.duration_ms = max(0, int((perf_counter() - self._start) * 1000))
|
|
32
|
+
if exc is not None:
|
|
33
|
+
self.status = "error"
|
|
34
|
+
self.attributes["error"] = {"type": exc_type.__name__, "message": str(exc)}
|
|
35
|
+
self.trace._active_span.reset(self._token)
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
def set_output(self, output):
|
|
39
|
+
self.output = output
|
|
40
|
+
|
|
41
|
+
def set_attributes(self, attributes):
|
|
42
|
+
self.attributes.update(attributes)
|
|
43
|
+
|
|
44
|
+
def span(self, type, name, **kwargs):
|
|
45
|
+
return self.trace.span(type=type, name=name, parent_span_id=self.id, **kwargs)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from time import perf_counter
|
|
4
|
+
|
|
5
|
+
from .span import Span
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Trace:
|
|
9
|
+
def __init__(self, client, name, input=None, metadata=None, session_id=None, user_id=None):
|
|
10
|
+
self.client, self.name = client, name
|
|
11
|
+
self.input, self.output = input, None
|
|
12
|
+
self.metadata = dict(metadata or {})
|
|
13
|
+
self.session_id, self.user_id = session_id, user_id
|
|
14
|
+
self.metrics, self.spans = {}, []
|
|
15
|
+
self.status = "success"
|
|
16
|
+
self.started_at = self.ended_at = None
|
|
17
|
+
self.duration_ms = 0
|
|
18
|
+
self.id = None
|
|
19
|
+
self._active_span = ContextVar("raglens_span", default=None)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def url(self):
|
|
23
|
+
if self.id and self.client.project_id:
|
|
24
|
+
return f"{self.client.web_url}/projects/{self.client.project_id}/traces/{self.id}"
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
def __enter__(self):
|
|
28
|
+
self.started_at = datetime.now(timezone.utc)
|
|
29
|
+
self._start = perf_counter()
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
def __exit__(self, exc_type, exc, tb):
|
|
33
|
+
self.ended_at = datetime.now(timezone.utc)
|
|
34
|
+
self.duration_ms = max(0, int((perf_counter() - self._start) * 1000))
|
|
35
|
+
if exc is not None:
|
|
36
|
+
self.status = "error"
|
|
37
|
+
self.metadata["error"] = {"type": exc_type.__name__, "message": str(exc)}
|
|
38
|
+
self.id = self.client.send_trace(self)
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
def span(self, type, name, **kwargs):
|
|
42
|
+
return Span(self, type, name, **kwargs)
|
|
43
|
+
|
|
44
|
+
def set_output(self, output):
|
|
45
|
+
self.output = output
|
|
46
|
+
|
|
47
|
+
def set_metrics(self, metrics):
|
|
48
|
+
self.metrics.update(metrics)
|
|
49
|
+
|
|
50
|
+
def log_retrieval(self, **kwargs):
|
|
51
|
+
from .helpers import log_retrieval
|
|
52
|
+
with self.span("retrieval", "retrieval") as span:
|
|
53
|
+
log_retrieval(span, **kwargs)
|
|
54
|
+
return span
|
|
55
|
+
|
|
56
|
+
def log_context(self, **kwargs):
|
|
57
|
+
from .helpers import log_context
|
|
58
|
+
with self.span("context", "context") as span:
|
|
59
|
+
log_context(span, **kwargs)
|
|
60
|
+
return span
|
|
61
|
+
|
|
62
|
+
def log_prompt(self, **kwargs):
|
|
63
|
+
from .helpers import log_prompt
|
|
64
|
+
with self.span("prompt", "prompt") as span:
|
|
65
|
+
log_prompt(span, **kwargs)
|
|
66
|
+
return span
|
|
67
|
+
|
|
68
|
+
def log_generation(self, **kwargs):
|
|
69
|
+
from .helpers import log_generation
|
|
70
|
+
with self.span("llm", "generation") as span:
|
|
71
|
+
log_generation(span, **kwargs)
|
|
72
|
+
return span
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: raglens-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight tracing for RAG pipelines
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: httpx<1,>=0.27
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
11
|
+
Requires-Dist: build; extra == "dev"
|
|
12
|
+
Requires-Dist: twine; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# RAGLens Python SDK
|
|
15
|
+
|
|
16
|
+
The full copyable walkthrough is in the repository's
|
|
17
|
+
[quick-start guide](../../docs/QUICKSTART.md). It covers every SDK helper and
|
|
18
|
+
the real Qdrant/OpenRouter example.
|
|
19
|
+
|
|
20
|
+
Install from this repository: `pip install -e packages/sdk-python` (Python 3.9+).
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from raglens import RAGLens, log_generation
|
|
24
|
+
|
|
25
|
+
client = RAGLens() # reads RAGLENS_API_KEY and RAGLENS_BASE_URL
|
|
26
|
+
with client.trace("answer", input={"query": "What is RAG?"}) as trace:
|
|
27
|
+
with trace.span("llm", "generate") as span:
|
|
28
|
+
answer = "Retrieval-augmented generation"
|
|
29
|
+
log_generation(span, answer, {"input_tokens": 12, "output_tokens": 4}, model="mock")
|
|
30
|
+
trace.set_output({"answer": answer})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Spans nest automatically within a trace using context-local state. Use `span.span()`
|
|
34
|
+
for an explicit parent. Helpers `log_retrieval`, `log_context`, `log_prompt`, and
|
|
35
|
+
`log_generation` accept an existing span; matching `trace.log_*` methods create
|
|
36
|
+
instantaneous spans. Wrap the actual operation in a span for useful latency data.
|
|
37
|
+
Retrieval records require `chunk_id`, `document_id`, `document_name`, `content`, and
|
|
38
|
+
`score`; `rank` and `retrieval_method` are filled automatically. Set `selected` for
|
|
39
|
+
chunks used in context. `log_context` accepts `max_tokens` and a `chunks` list.
|
|
40
|
+
|
|
41
|
+
Set `RAGLENS_ENABLED=false` to disable delivery. Defaults: API URL
|
|
42
|
+
`http://localhost:8000`, enabled true, HTTP timeout two seconds. Constructor values
|
|
43
|
+
override environment variables. Delivery is synchronous on trace exit, without
|
|
44
|
+
retries (the ingestion API is not idempotent). Network, HTTP and serialization
|
|
45
|
+
failures warn without replacing the application's result or exception. Application
|
|
46
|
+
exceptions are recorded and propagate normally. Supply JSON-compatible dictionaries
|
|
47
|
+
for inputs, outputs, metrics and metadata; datetime and exception values serialize
|
|
48
|
+
into strings and structured errors. Avoid recording secrets or sensitive content.
|
|
49
|
+
|
|
50
|
+
After delivery `trace.id` contains the server-assigned ID. Set `project_id` and
|
|
51
|
+
optionally `web_url` on the client to populate `trace.url`.
|
|
52
|
+
|
|
53
|
+
## Tests and release
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
pip install -e 'packages/sdk-python[dev]'
|
|
57
|
+
pytest packages/sdk-python/tests
|
|
58
|
+
python -m build packages/sdk-python
|
|
59
|
+
python -m twine check packages/sdk-python/dist/*
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Version is maintained in `pyproject.toml`; `raglens.__version__` reads the installed
|
|
63
|
+
metadata. For a release, bump that version, test, build into a clean `dist/`, then
|
|
64
|
+
upload the reviewed wheel and sdist using `python -m twine upload ...` with your
|
|
65
|
+
registry credentials. No package has been published by this implementation.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
raglens/__init__.py
|
|
4
|
+
raglens/client.py
|
|
5
|
+
raglens/helpers.py
|
|
6
|
+
raglens/serializers.py
|
|
7
|
+
raglens/span.py
|
|
8
|
+
raglens/trace.py
|
|
9
|
+
raglens_sdk.egg-info/PKG-INFO
|
|
10
|
+
raglens_sdk.egg-info/SOURCES.txt
|
|
11
|
+
raglens_sdk.egg-info/dependency_links.txt
|
|
12
|
+
raglens_sdk.egg-info/requires.txt
|
|
13
|
+
raglens_sdk.egg-info/top_level.txt
|
|
14
|
+
tests/test_sdk.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
raglens
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from raglens import RAGLens, log_retrieval
|
|
9
|
+
from raglens.serializers import serialize_trace
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def client(handler):
|
|
13
|
+
return RAGLens(api_key="test-key", project_id="project", transport=httpx.MockTransport(handler))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_complete_payload_and_nesting():
|
|
17
|
+
captured = []
|
|
18
|
+
def handle(request):
|
|
19
|
+
assert request.url.path == "/v1/traces"
|
|
20
|
+
assert request.headers["authorization"] == "Bearer test-key"
|
|
21
|
+
captured.append(json.loads(request.content))
|
|
22
|
+
return httpx.Response(201, json={"trace_id": "trace_test"})
|
|
23
|
+
with client(handle).trace("test", input={"date": datetime.now(timezone.utc)}) as trace:
|
|
24
|
+
with trace.span("query", "root") as root:
|
|
25
|
+
with trace.span("retrieval", "search") as span:
|
|
26
|
+
log_retrieval(span, [{"chunk_id": "c", "document_id": "d", "document_name": "Doc",
|
|
27
|
+
"content": "text", "score": .9, "selected": True}])
|
|
28
|
+
trace.log_context(context="text", tokens=1, max_tokens=10)
|
|
29
|
+
trace.log_prompt(prompt="question", tokens=1)
|
|
30
|
+
trace.log_generation(response="answer", tokens={"input_tokens": 2, "output_tokens": 1}, model="mock")
|
|
31
|
+
trace.set_output({"answer": "answer"})
|
|
32
|
+
payload = captured[0]
|
|
33
|
+
assert len(payload["spans"]) == 5
|
|
34
|
+
assert all(s["parent_span_id"] == root.id for s in payload["spans"][1:])
|
|
35
|
+
assert payload["spans"][1]["retrieval_results"][0]["rank"] == 1
|
|
36
|
+
assert payload["metrics"]["total_tokens"] == 3
|
|
37
|
+
assert payload["ended_at"] >= payload["started_at"]
|
|
38
|
+
assert trace.url == "http://localhost:3000/projects/project/traces/trace_test"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.parametrize("failure", ["network", "http", "json", "serialize"])
|
|
42
|
+
def test_delivery_failures_do_not_replace_application_exception(failure):
|
|
43
|
+
def handle(request):
|
|
44
|
+
if failure == "network":
|
|
45
|
+
raise httpx.ConnectError("offline")
|
|
46
|
+
return httpx.Response(500 if failure == "http" else 201, text="invalid json")
|
|
47
|
+
error = ValueError("application failure")
|
|
48
|
+
with pytest.raises(ValueError) as caught:
|
|
49
|
+
with client(handle).trace("failure") as trace:
|
|
50
|
+
if failure == "serialize":
|
|
51
|
+
trace.set_output({"unsupported": object()})
|
|
52
|
+
with trace.span("custom", "fails"):
|
|
53
|
+
raise error
|
|
54
|
+
assert caught.value is error
|
|
55
|
+
assert trace.status == trace.spans[0].status == "error"
|
|
56
|
+
assert trace.id is None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_disabled_env_and_serialization(monkeypatch):
|
|
60
|
+
monkeypatch.setenv("RAGLENS_ENABLED", "false")
|
|
61
|
+
monkeypatch.setenv("RAGLENS_BASE_URL", "http://example.test")
|
|
62
|
+
c = RAGLens(transport=httpx.MockTransport(lambda r: pytest.fail("HTTP called")))
|
|
63
|
+
with c.trace("disabled", metadata={"error": ValueError("example")}) as trace:
|
|
64
|
+
pass
|
|
65
|
+
assert trace.id is None
|
|
66
|
+
assert c.base_url == "http://example.test"
|
|
67
|
+
assert serialize_trace(trace)["metadata"]["error"]["type"] == "ValueError"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_concurrent_span_parents_are_isolated():
|
|
71
|
+
async def run():
|
|
72
|
+
with RAGLens(enabled=False).trace("parallel") as trace:
|
|
73
|
+
async def child(name):
|
|
74
|
+
with trace.span("custom", name) as parent:
|
|
75
|
+
await asyncio.sleep(0)
|
|
76
|
+
with trace.span("custom", name + "-child") as nested:
|
|
77
|
+
assert nested.parent_span_id == parent.id
|
|
78
|
+
await asyncio.gather(child("one"), child("two"))
|
|
79
|
+
assert sum(s.parent_span_id is None for s in trace.spans) == 2
|
|
80
|
+
asyncio.run(run())
|