contexttrace 0.1.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.
- contexttrace/__init__.py +36 -0
- contexttrace/_version.py +1 -0
- contexttrace/cli.py +474 -0
- contexttrace/client.py +1074 -0
- contexttrace/config.py +246 -0
- contexttrace/demo.py +311 -0
- contexttrace/demo_data.py +257 -0
- contexttrace/endpoint_eval.py +314 -0
- contexttrace/errors.py +14 -0
- contexttrace/evaluator.py +448 -0
- contexttrace/integrations/__init__.py +14 -0
- contexttrace/integrations/fastapi.py +311 -0
- contexttrace/integrations/langchain.py +440 -0
- contexttrace/integrations/langgraph.py +197 -0
- contexttrace/integrations/llamaindex.py +422 -0
- contexttrace/integrations/opentelemetry.py +111 -0
- contexttrace/local.py +325 -0
- contexttrace/py.typed +1 -0
- contexttrace/regression.py +123 -0
- contexttrace/reliability.py +284 -0
- contexttrace/report.py +550 -0
- contexttrace/storage/__init__.py +3 -0
- contexttrace/storage/sqlite_store.py +604 -0
- contexttrace/thresholds.py +50 -0
- contexttrace/transport.py +183 -0
- contexttrace/viewer.py +148 -0
- contexttrace-0.1.0.dist-info/METADATA +154 -0
- contexttrace-0.1.0.dist-info/RECORD +31 -0
- contexttrace-0.1.0.dist-info/WHEEL +5 -0
- contexttrace-0.1.0.dist-info/entry_points.txt +2 -0
- contexttrace-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Optional, Protocol
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from contexttrace.errors import ContextTraceHTTPError
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("contexttrace")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Transport(Protocol):
|
|
16
|
+
def post(self, path: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
def get(self, path: str) -> dict[str, Any]:
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HttpTransport:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
base_url: str,
|
|
28
|
+
api_key: str,
|
|
29
|
+
timeout: float = 30.0,
|
|
30
|
+
retries: int = 2,
|
|
31
|
+
debug: bool = False,
|
|
32
|
+
client: Optional[httpx.Client] = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.retries = max(0, retries)
|
|
35
|
+
self.debug = debug
|
|
36
|
+
self._client = client or httpx.Client(
|
|
37
|
+
base_url=base_url.rstrip("/"),
|
|
38
|
+
timeout=timeout,
|
|
39
|
+
headers={
|
|
40
|
+
"Authorization": f"Bearer {api_key}",
|
|
41
|
+
"User-Agent": "contexttrace-python/0.1.0",
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def post(self, path: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
46
|
+
return self._request("POST", path, json=payload or {})
|
|
47
|
+
|
|
48
|
+
def get(self, path: str) -> dict[str, Any]:
|
|
49
|
+
return self._request("GET", path)
|
|
50
|
+
|
|
51
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
52
|
+
last_error: Optional[BaseException] = None
|
|
53
|
+
attempts = self.retries + 1
|
|
54
|
+
|
|
55
|
+
for attempt in range(attempts):
|
|
56
|
+
try:
|
|
57
|
+
if self.debug:
|
|
58
|
+
logger.debug("%s %s", method, path)
|
|
59
|
+
response = self._client.request(method, path, **kwargs)
|
|
60
|
+
if response.status_code >= 500 and attempt < attempts - 1:
|
|
61
|
+
last_error = _http_error(response)
|
|
62
|
+
_sleep_before_retry(attempt)
|
|
63
|
+
continue
|
|
64
|
+
return _json_or_raise(response)
|
|
65
|
+
except (httpx.TimeoutException, httpx.NetworkError, httpx.TransportError) as exc:
|
|
66
|
+
last_error = exc
|
|
67
|
+
if attempt >= attempts - 1:
|
|
68
|
+
break
|
|
69
|
+
_sleep_before_retry(attempt)
|
|
70
|
+
|
|
71
|
+
raise ContextTraceHTTPError(
|
|
72
|
+
"ContextTrace API request failed after %s attempt(s): %s %s: %s"
|
|
73
|
+
% (attempts, method, path, last_error)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def close(self) -> None:
|
|
77
|
+
self._client.close()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AsyncTransport(Protocol):
|
|
81
|
+
async def post(self, path: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
async def get(self, path: str) -> dict[str, Any]:
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AsyncHttpTransport:
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
*,
|
|
92
|
+
base_url: str,
|
|
93
|
+
api_key: str,
|
|
94
|
+
timeout: float = 30.0,
|
|
95
|
+
retries: int = 2,
|
|
96
|
+
debug: bool = False,
|
|
97
|
+
client: Optional[httpx.AsyncClient] = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
self.retries = max(0, retries)
|
|
100
|
+
self.debug = debug
|
|
101
|
+
self._client = client or httpx.AsyncClient(
|
|
102
|
+
base_url=base_url.rstrip("/"),
|
|
103
|
+
timeout=timeout,
|
|
104
|
+
headers={
|
|
105
|
+
"Authorization": f"Bearer {api_key}",
|
|
106
|
+
"User-Agent": "contexttrace-python/0.1.0",
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
async def post(self, path: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
111
|
+
return await self._request("POST", path, json=payload or {})
|
|
112
|
+
|
|
113
|
+
async def get(self, path: str) -> dict[str, Any]:
|
|
114
|
+
return await self._request("GET", path)
|
|
115
|
+
|
|
116
|
+
async def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
117
|
+
last_error: Optional[BaseException] = None
|
|
118
|
+
attempts = self.retries + 1
|
|
119
|
+
|
|
120
|
+
for attempt in range(attempts):
|
|
121
|
+
try:
|
|
122
|
+
if self.debug:
|
|
123
|
+
logger.debug("%s %s", method, path)
|
|
124
|
+
response = await self._client.request(method, path, **kwargs)
|
|
125
|
+
if response.status_code >= 500 and attempt < attempts - 1:
|
|
126
|
+
last_error = _http_error(response)
|
|
127
|
+
await _async_sleep_before_retry(attempt)
|
|
128
|
+
continue
|
|
129
|
+
return _json_or_raise(response)
|
|
130
|
+
except (httpx.TimeoutException, httpx.NetworkError, httpx.TransportError) as exc:
|
|
131
|
+
last_error = exc
|
|
132
|
+
if attempt >= attempts - 1:
|
|
133
|
+
break
|
|
134
|
+
await _async_sleep_before_retry(attempt)
|
|
135
|
+
|
|
136
|
+
raise ContextTraceHTTPError(
|
|
137
|
+
"ContextTrace API request failed after %s attempt(s): %s %s: %s"
|
|
138
|
+
% (attempts, method, path, last_error)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
async def close(self) -> None:
|
|
142
|
+
await self._client.aclose()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _json_or_raise(response: httpx.Response) -> dict[str, Any]:
|
|
146
|
+
try:
|
|
147
|
+
response.raise_for_status()
|
|
148
|
+
except httpx.HTTPStatusError as exc:
|
|
149
|
+
raise _http_error(response) from exc
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
data = response.json()
|
|
153
|
+
except ValueError as exc:
|
|
154
|
+
raise ContextTraceHTTPError(
|
|
155
|
+
"ContextTrace API returned invalid JSON for %s %s."
|
|
156
|
+
% (response.request.method, response.request.url)
|
|
157
|
+
) from exc
|
|
158
|
+
|
|
159
|
+
if not isinstance(data, dict):
|
|
160
|
+
raise ContextTraceHTTPError("ContextTrace API returned a non-object JSON response.")
|
|
161
|
+
return data
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _http_error(response: httpx.Response) -> ContextTraceHTTPError:
|
|
165
|
+
detail = response.text
|
|
166
|
+
try:
|
|
167
|
+
body = response.json()
|
|
168
|
+
if isinstance(body, dict):
|
|
169
|
+
detail = str(body.get("detail") or body.get("error") or body)
|
|
170
|
+
except ValueError:
|
|
171
|
+
pass
|
|
172
|
+
return ContextTraceHTTPError(
|
|
173
|
+
"ContextTrace API request failed with HTTP %s for %s %s: %s"
|
|
174
|
+
% (response.status_code, response.request.method, response.request.url, detail)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _sleep_before_retry(attempt: int) -> None:
|
|
179
|
+
time.sleep(min(0.5, 0.1 * (2 ** attempt)))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async def _async_sleep_before_retry(attempt: int) -> None:
|
|
183
|
+
await asyncio.sleep(min(0.5, 0.1 * (2 ** attempt)))
|
contexttrace/viewer.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from html import escape
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
from wsgiref.simple_server import make_server
|
|
7
|
+
|
|
8
|
+
from contexttrace.report import ReportGenerator
|
|
9
|
+
from contexttrace.storage import SQLiteTraceStore
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_viewer_app(storage_path: str = ".contexttrace/contexttrace.db") -> Callable:
|
|
13
|
+
store = SQLiteTraceStore(storage_path)
|
|
14
|
+
|
|
15
|
+
def app(environ: dict[str, Any], start_response: Callable) -> list[bytes]:
|
|
16
|
+
path = environ.get("PATH_INFO") or "/"
|
|
17
|
+
try:
|
|
18
|
+
status = "200 OK"
|
|
19
|
+
body = _route(path, store)
|
|
20
|
+
except Exception as exc:
|
|
21
|
+
status = "500 Internal Server Error"
|
|
22
|
+
body = _page("ContextTrace Viewer Error", "<p>%s</p>" % escape(str(exc)))
|
|
23
|
+
start_response(status, [("Content-Type", "text/html; charset=utf-8")])
|
|
24
|
+
return [body.encode("utf-8")]
|
|
25
|
+
|
|
26
|
+
return app
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def serve_viewer(
|
|
30
|
+
*,
|
|
31
|
+
storage_path: str = ".contexttrace/contexttrace.db",
|
|
32
|
+
host: str = "127.0.0.1",
|
|
33
|
+
port: int = 8765,
|
|
34
|
+
) -> None:
|
|
35
|
+
app = create_viewer_app(storage_path)
|
|
36
|
+
with make_server(host, port, app) as server:
|
|
37
|
+
print("ContextTrace local viewer: http://%s:%s" % (host, port))
|
|
38
|
+
server.serve_forever()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _route(path: str, store: SQLiteTraceStore) -> str:
|
|
42
|
+
if path == "/":
|
|
43
|
+
status = store.trace_count()
|
|
44
|
+
last_eval = store.last_eval_run()
|
|
45
|
+
return _page(
|
|
46
|
+
"ContextTrace Local Viewer",
|
|
47
|
+
"""
|
|
48
|
+
<section class="hero">
|
|
49
|
+
<h1>ContextTrace Local Viewer</h1>
|
|
50
|
+
<p>Inspect local RAG and agent traces stored in SQLite.</p>
|
|
51
|
+
<div class="metrics">
|
|
52
|
+
<div><dt>Traces</dt><dd>{trace_count}</dd></div>
|
|
53
|
+
<div><dt>Last Eval Run</dt><dd>{last_eval}</dd></div>
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
""".format(
|
|
57
|
+
trace_count=status,
|
|
58
|
+
last_eval=escape(str((last_eval or {}).get("id") or "None")),
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
if path == "/traces":
|
|
62
|
+
rows = []
|
|
63
|
+
for trace in store.list_traces(limit=100):
|
|
64
|
+
failure = ((trace.get("evaluation") or {}).get("failure") or {}).get("failure_type") or "not_evaluated"
|
|
65
|
+
score = ((trace.get("evaluation") or {}).get("scores") or {}).get("citation_support", "")
|
|
66
|
+
rows.append(
|
|
67
|
+
"<tr><td><a href=\"/traces/{id}\">{id}</a></td><td>{query}</td><td>{failure}</td><td>{score}</td><td>{created}</td></tr>".format(
|
|
68
|
+
id=escape(str(trace.get("id"))),
|
|
69
|
+
query=escape(str(trace.get("query") or "")[:140]),
|
|
70
|
+
failure=escape(str(failure)),
|
|
71
|
+
score=escape(str(score)),
|
|
72
|
+
created=escape(str(trace.get("created_at") or "")),
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
return _page(
|
|
76
|
+
"Traces",
|
|
77
|
+
"<h1>Traces</h1><table><thead><tr><th>Trace ID</th><th>Query</th><th>Failure</th><th>Citation Support</th><th>Created</th></tr></thead><tbody>%s</tbody></table>"
|
|
78
|
+
% ("\n".join(rows) or "<tr><td colspan=\"5\">No traces found.</td></tr>"),
|
|
79
|
+
)
|
|
80
|
+
if path.startswith("/traces/"):
|
|
81
|
+
trace_id = path.rsplit("/", 1)[-1]
|
|
82
|
+
trace = store.get_trace(trace_id)
|
|
83
|
+
return ReportGenerator().render(trace)
|
|
84
|
+
if path == "/eval-runs":
|
|
85
|
+
rows = []
|
|
86
|
+
for run in store.list_eval_runs(limit=100):
|
|
87
|
+
summary = run.get("summary") or {}
|
|
88
|
+
rows.append(
|
|
89
|
+
"<tr><td>{id}</td><td>{dataset}</td><td>{endpoint}</td><td>{score}</td><td>{created}</td></tr>".format(
|
|
90
|
+
id=escape(str(run.get("id"))),
|
|
91
|
+
dataset=escape(str(run.get("dataset") or "")),
|
|
92
|
+
endpoint=escape(str(run.get("endpoint") or "")),
|
|
93
|
+
score=escape(str(summary.get("reliability_score", ""))),
|
|
94
|
+
created=escape(str(run.get("created_at") or "")),
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
return _page(
|
|
98
|
+
"Eval Runs",
|
|
99
|
+
"<h1>Eval Runs</h1><table><thead><tr><th>ID</th><th>Dataset</th><th>Endpoint</th><th>Reliability</th><th>Created</th></tr></thead><tbody>%s</tbody></table>"
|
|
100
|
+
% ("\n".join(rows) or "<tr><td colspan=\"5\">No eval runs found.</td></tr>"),
|
|
101
|
+
)
|
|
102
|
+
if path == "/reports":
|
|
103
|
+
report_dir = Path(".contexttrace") / "reports"
|
|
104
|
+
items = []
|
|
105
|
+
for report in sorted(report_dir.glob("*.html")) if report_dir.exists() else []:
|
|
106
|
+
items.append("<li>%s</li>" % escape(str(report)))
|
|
107
|
+
return _page("Reports", "<h1>Reports</h1><ul>%s</ul>" % ("\n".join(items) or "<li>No reports found.</li>"))
|
|
108
|
+
return _page("Not Found", "<h1>Not Found</h1><p>No local viewer page exists for this path.</p>")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _page(title: str, body: str) -> str:
|
|
112
|
+
return """<!doctype html>
|
|
113
|
+
<html lang="en">
|
|
114
|
+
<head>
|
|
115
|
+
<meta charset="utf-8">
|
|
116
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
117
|
+
<title>{title}</title>
|
|
118
|
+
<style>
|
|
119
|
+
:root {{ --bg: #f7f8fa; --panel: #fff; --text: #1f2933; --muted: #697386; --line: #d8dee8; --accent: #2458d3; }}
|
|
120
|
+
* {{ box-sizing: border-box; }}
|
|
121
|
+
body {{ margin: 0; background: var(--bg); color: var(--text); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }}
|
|
122
|
+
main {{ max-width: 1120px; margin: 0 auto; padding: 32px 20px 56px; }}
|
|
123
|
+
nav {{ display: flex; gap: 16px; margin-bottom: 22px; }}
|
|
124
|
+
nav a {{ color: var(--accent); text-decoration: none; font-weight: 700; }}
|
|
125
|
+
h1 {{ margin-top: 0; }}
|
|
126
|
+
section, table {{ background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }}
|
|
127
|
+
section {{ padding: 20px; }}
|
|
128
|
+
table {{ width: 100%; border-collapse: collapse; overflow: hidden; }}
|
|
129
|
+
th, td {{ border-bottom: 1px solid var(--line); padding: 10px; text-align: left; vertical-align: top; }}
|
|
130
|
+
th {{ color: var(--muted); font-size: 12px; text-transform: uppercase; }}
|
|
131
|
+
.metrics {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-top: 16px; }}
|
|
132
|
+
.metrics div {{ border: 1px solid var(--line); border-radius: 8px; padding: 12px; background: #fbfcfe; }}
|
|
133
|
+
dt {{ color: var(--muted); font-size: 12px; font-weight: 700; text-transform: uppercase; }}
|
|
134
|
+
dd {{ margin: 4px 0 0; font-size: 20px; }}
|
|
135
|
+
</style>
|
|
136
|
+
</head>
|
|
137
|
+
<body>
|
|
138
|
+
<main>
|
|
139
|
+
<nav>
|
|
140
|
+
<a href="/">Overview</a>
|
|
141
|
+
<a href="/traces">Traces</a>
|
|
142
|
+
<a href="/eval-runs">Eval Runs</a>
|
|
143
|
+
<a href="/reports">Reports</a>
|
|
144
|
+
</nav>
|
|
145
|
+
{body}
|
|
146
|
+
</main>
|
|
147
|
+
</body>
|
|
148
|
+
</html>""".format(title=escape(title), body=body)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: contexttrace
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local-first SDK and CLI for RAG and agent reliability tracing, citation checks, and failure diagnosis.
|
|
5
|
+
Author: ContextTrace contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/samarth1412/Context-Trace
|
|
8
|
+
Project-URL: Documentation, https://github.com/samarth1412/Context-Trace/tree/main/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/samarth1412/Context-Trace
|
|
10
|
+
Project-URL: Issues, https://github.com/samarth1412/Context-Trace/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/samarth1412/Context-Trace/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: rag,llm,retrieval-augmented-generation,citations,evaluation,observability,agents,cli,sqlite
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
Requires-Dist: click>=8.1
|
|
28
|
+
Requires-Dist: httpx>=0.27
|
|
29
|
+
Requires-Dist: typing-extensions>=4.9
|
|
30
|
+
Provides-Extra: all
|
|
31
|
+
Requires-Dist: fastapi>=0.110; extra == "all"
|
|
32
|
+
Requires-Dist: langchain-core>=0.2; extra == "all"
|
|
33
|
+
Requires-Dist: langgraph>=0.2; extra == "all"
|
|
34
|
+
Requires-Dist: llama-index-core>=0.10; extra == "all"
|
|
35
|
+
Requires-Dist: opentelemetry-api>=1.24; extra == "all"
|
|
36
|
+
Provides-Extra: fastapi
|
|
37
|
+
Requires-Dist: fastapi>=0.110; extra == "fastapi"
|
|
38
|
+
Provides-Extra: integrations
|
|
39
|
+
Requires-Dist: fastapi>=0.110; extra == "integrations"
|
|
40
|
+
Requires-Dist: langchain-core>=0.2; extra == "integrations"
|
|
41
|
+
Requires-Dist: langgraph>=0.2; extra == "integrations"
|
|
42
|
+
Requires-Dist: llama-index-core>=0.10; extra == "integrations"
|
|
43
|
+
Requires-Dist: opentelemetry-api>=1.24; extra == "integrations"
|
|
44
|
+
Provides-Extra: langchain
|
|
45
|
+
Requires-Dist: langchain-core>=0.2; extra == "langchain"
|
|
46
|
+
Provides-Extra: langgraph
|
|
47
|
+
Requires-Dist: langgraph>=0.2; extra == "langgraph"
|
|
48
|
+
Provides-Extra: llamaindex
|
|
49
|
+
Requires-Dist: llama-index-core>=0.10; extra == "llamaindex"
|
|
50
|
+
Provides-Extra: local
|
|
51
|
+
Provides-Extra: opentelemetry
|
|
52
|
+
Requires-Dist: opentelemetry-api>=1.24; extra == "opentelemetry"
|
|
53
|
+
Provides-Extra: otel
|
|
54
|
+
Requires-Dist: opentelemetry-api>=1.24; extra == "otel"
|
|
55
|
+
Provides-Extra: test
|
|
56
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
57
|
+
|
|
58
|
+
# ContextTrace
|
|
59
|
+
|
|
60
|
+
**Debug RAG failures before users find them.**
|
|
61
|
+
|
|
62
|
+
ContextTrace is a local-first Python SDK and CLI for evaluating existing RAG and AI agent systems. It records retrieved chunks, selected context, answer claims, citations, token usage, latency, and agent events, then writes local traces and HTML reports without requiring a hosted dashboard.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install contexttrace
|
|
68
|
+
contexttrace --version
|
|
69
|
+
contexttrace init
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Optional integrations:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install "contexttrace[langchain]"
|
|
76
|
+
pip install "contexttrace[llamaindex]"
|
|
77
|
+
pip install "contexttrace[fastapi]"
|
|
78
|
+
pip install "contexttrace[langgraph]"
|
|
79
|
+
pip install "contexttrace[otel]"
|
|
80
|
+
pip install "contexttrace[all]"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Quickstart
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
contexttrace init
|
|
87
|
+
contexttrace demo --dataset refund_policy
|
|
88
|
+
contexttrace report --last
|
|
89
|
+
contexttrace doctor
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
By default, traces are stored locally in:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
.contexttrace/contexttrace.db
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## SDK Example
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from contexttrace import ContextTrace
|
|
102
|
+
|
|
103
|
+
ct = ContextTrace(project="support-rag")
|
|
104
|
+
|
|
105
|
+
with ct.trace(query="What is the refund policy?") as trace:
|
|
106
|
+
chunks = retriever.search("What is the refund policy?")
|
|
107
|
+
trace.log_retrieval(chunks)
|
|
108
|
+
trace.log_context(chunks[:5])
|
|
109
|
+
|
|
110
|
+
answer = llm.generate("What is the refund policy?", chunks[:5])
|
|
111
|
+
trace.log_answer(answer, usage={"total_tokens": 1200})
|
|
112
|
+
trace.log_citations([
|
|
113
|
+
{"claim": "Refunds are available within 30 days.", "source_chunk_id": "chunk_12"}
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
result = trace.evaluate()
|
|
117
|
+
print(result["failure"]["failure_type"])
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## BYO RAG Endpoint
|
|
121
|
+
|
|
122
|
+
Evaluate a running local or hosted RAG API without adding SDK code:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
contexttrace eval \
|
|
126
|
+
--dataset evals/questions.json \
|
|
127
|
+
--endpoint http://localhost:8000/query \
|
|
128
|
+
--method POST \
|
|
129
|
+
--input-key question \
|
|
130
|
+
--answer-path $.answer \
|
|
131
|
+
--contexts-path $.contexts \
|
|
132
|
+
--citations-path $.citations \
|
|
133
|
+
--fail-on "failure_rate>0.25"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## What It Catches
|
|
137
|
+
|
|
138
|
+
- `retrieval_miss`
|
|
139
|
+
- `citation_mismatch`
|
|
140
|
+
- `unsupported_answer`
|
|
141
|
+
- `contradicted_answer`
|
|
142
|
+
- `conflicting_sources`
|
|
143
|
+
- `should_have_abstained`
|
|
144
|
+
- agent failures such as `stale_memory_used` and `tool_error`
|
|
145
|
+
|
|
146
|
+
## Privacy
|
|
147
|
+
|
|
148
|
+
Local mode is the default. ContextTrace makes no network calls unless you configure an LLM judge provider or evaluate a RAG endpoint you provide.
|
|
149
|
+
|
|
150
|
+
## Links
|
|
151
|
+
|
|
152
|
+
- Repository: https://github.com/samarth1412/Context-Trace
|
|
153
|
+
- Documentation: https://github.com/samarth1412/Context-Trace/tree/main/docs
|
|
154
|
+
- Issues: https://github.com/samarth1412/Context-Trace/issues
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
contexttrace/__init__.py,sha256=PYE5v4eQyRQj85tm4r1QU4vD50Ha_sMy2y_Vq0oP5V0,1347
|
|
2
|
+
contexttrace/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
3
|
+
contexttrace/cli.py,sha256=HwPlpP89ZlMjVdU-tjIFD_Z4cwgxOWtG-Xgn389P_sI,19751
|
|
4
|
+
contexttrace/client.py,sha256=i-fFRG3ESXgfIIy2_wmmQpgWK1F8VmVDx8K5KpDdQdk,34333
|
|
5
|
+
contexttrace/config.py,sha256=inyY1xhYB8Py9T5COmI6Rj_WcLLR6ItP1Hs7T2XtjTk,7401
|
|
6
|
+
contexttrace/demo.py,sha256=COxHRNnyYySgujBrBrwe-z1ti19VZzubkDNrhm79358,11445
|
|
7
|
+
contexttrace/demo_data.py,sha256=cJjprUipIqjorF_JAscOqONpqeSqdEUFx4WlLqcFmAc,14069
|
|
8
|
+
contexttrace/endpoint_eval.py,sha256=0WZLYnTs03GXiJoHOnXyj_ATeYpkLPtZTYaeU_Mb4Qs,12242
|
|
9
|
+
contexttrace/errors.py,sha256=WsqJGLoP02pjheLTZEDL1R9N52vA3A0_QhlJBOGDr-A,393
|
|
10
|
+
contexttrace/evaluator.py,sha256=bQhHvnysDZuwBGmU1GfPLB9ae7_Gaquh3aFP3Zc9HCU,14414
|
|
11
|
+
contexttrace/local.py,sha256=k6_6gvbzQEg_ZETkTC4gzN1vXitYSFus7d8GtNj3A58,12723
|
|
12
|
+
contexttrace/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
13
|
+
contexttrace/regression.py,sha256=mexwqdv8H-HUNqyoOUT6oBQWiApDKtNvRjNn9N0i77E,4494
|
|
14
|
+
contexttrace/reliability.py,sha256=MYxgwNOsHhErnjTDJkAYsAhNPjgFqcvLbE5pkannxYw,11130
|
|
15
|
+
contexttrace/report.py,sha256=AwUIweDMy1WINaxSYOM2bj-LkVwlHo23NlunuqLBF7A,17667
|
|
16
|
+
contexttrace/thresholds.py,sha256=J6yrMD3dPRtqx7JKnLFYqvchd4paiyXBPm5KhfLKs1Q,1572
|
|
17
|
+
contexttrace/transport.py,sha256=ra4GIgSjM5gPkS_VGHQXFmgeP-JswYHO9JSJqMNnhgc,5938
|
|
18
|
+
contexttrace/viewer.py,sha256=GERrf61SMvVNZ8wt-RDOMHiqJU5uqvX0OJshAEEMO2U,6599
|
|
19
|
+
contexttrace/integrations/__init__.py,sha256=EhuHHsPlRfxzMm3L_1BSV1VJj7BCf0KmVE8MG8xbbEM,645
|
|
20
|
+
contexttrace/integrations/fastapi.py,sha256=uDL2NfTGHbw8Dq58Ubx83mKqpDmsANOUeTYquX3cDeA,11417
|
|
21
|
+
contexttrace/integrations/langchain.py,sha256=w3FoqlrvBojpOfIcTAXSkX7ZaWPaJ-lqM1o6S0csh0M,15584
|
|
22
|
+
contexttrace/integrations/langgraph.py,sha256=9RkqTwmf0dIIbEIe_JsYTFCa7hKATj_kBLjBKWyezbw,6775
|
|
23
|
+
contexttrace/integrations/llamaindex.py,sha256=J3bEUDZmCBKJ6skWsHO-o9vVYu_2tKzRJNUohalPv_U,14446
|
|
24
|
+
contexttrace/integrations/opentelemetry.py,sha256=9hlMvnFiz3XykFWb3kw3oH5fLesV8t7yXw_UFAAbF1E,4179
|
|
25
|
+
contexttrace/storage/__init__.py,sha256=VcZgNDrQXc9SWt37nJ6F2LYOyMQryXYBWpedLQkK2vQ,95
|
|
26
|
+
contexttrace/storage/sqlite_store.py,sha256=hg117peJvWoCPe4pQ_8zMohq6otIF0Zowa80WvFjcnk,23807
|
|
27
|
+
contexttrace-0.1.0.dist-info/METADATA,sha256=P-fJL8KKplGOYuv6F3r77xihSlplWWAlTMU-KOAm80I,5291
|
|
28
|
+
contexttrace-0.1.0.dist-info/WHEEL,sha256=BNRMDyzLkkcmlv0J8ppDQkk2VED33SesJDynr9ED1gc,91
|
|
29
|
+
contexttrace-0.1.0.dist-info/entry_points.txt,sha256=RJDO0pz-10mTUe9HZwgtBSRWsyz4JtSNvzvt0T0_T5k,55
|
|
30
|
+
contexttrace-0.1.0.dist-info/top_level.txt,sha256=r1Az9kxVpsJuPfnrMOPOp2U_6ceBO4CQt6C5tTnGaLM,13
|
|
31
|
+
contexttrace-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
contexttrace
|