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.
@@ -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,6 @@
1
+ httpx<1,>=0.27
2
+
3
+ [dev]
4
+ pytest>=8
5
+ build
6
+ twine
@@ -0,0 +1 @@
1
+ raglens
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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())