ragradar 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.
- ragradar-0.1.0/.gitignore +38 -0
- ragradar-0.1.0/PKG-INFO +142 -0
- ragradar-0.1.0/README.md +116 -0
- ragradar-0.1.0/pyproject.toml +51 -0
- ragradar-0.1.0/src/ragradar/__init__.py +74 -0
- ragradar-0.1.0/src/ragradar/cli.py +231 -0
- ragradar-0.1.0/src/ragradar/explain/__init__.py +0 -0
- ragradar-0.1.0/src/ragradar/explain/analyzers/__init__.py +0 -0
- ragradar-0.1.0/src/ragradar/explain/analyzers/cache.py +19 -0
- ragradar-0.1.0/src/ragradar/explain/analyzers/duplicates.py +49 -0
- ragradar-0.1.0/src/ragradar/explain/analyzers/history.py +25 -0
- ragradar-0.1.0/src/ragradar/explain/analyzers/scores.py +38 -0
- ragradar-0.1.0/src/ragradar/explain/analyzers/tokens.py +36 -0
- ragradar-0.1.0/src/ragradar/explain/analyzers/truncation.py +32 -0
- ragradar-0.1.0/src/ragradar/explain/loader.py +8 -0
- ragradar-0.1.0/src/ragradar/explain/renderer/__init__.py +0 -0
- ragradar-0.1.0/src/ragradar/explain/renderer/html.py +152 -0
- ragradar-0.1.0/src/ragradar/explain/renderer/terminal.py +351 -0
- ragradar-0.1.0/src/ragradar/find/__init__.py +0 -0
- ragradar-0.1.0/src/ragradar/find/bm25.py +7 -0
- ragradar-0.1.0/src/ragradar/find/query_builder.py +65 -0
- ragradar-0.1.0/src/ragradar/store.py +128 -0
- ragradar-0.1.0/tests/conftest.py +117 -0
- ragradar-0.1.0/tests/test_analyzers.py +320 -0
- ragradar-0.1.0/tests/test_cli.py +221 -0
- ragradar-0.1.0/tests/test_store.py +139 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.venv/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# uv
|
|
11
|
+
.uv/
|
|
12
|
+
uv.lock
|
|
13
|
+
|
|
14
|
+
# ragradar runtime — never commit user run data
|
|
15
|
+
.ragradar/
|
|
16
|
+
|
|
17
|
+
# environment
|
|
18
|
+
.env
|
|
19
|
+
*.env
|
|
20
|
+
.env.*
|
|
21
|
+
|
|
22
|
+
# IDE
|
|
23
|
+
.vscode/
|
|
24
|
+
.idea/
|
|
25
|
+
*.swp
|
|
26
|
+
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
30
|
+
|
|
31
|
+
# test output
|
|
32
|
+
.pytest_cache/
|
|
33
|
+
htmlcov/
|
|
34
|
+
.coverage
|
|
35
|
+
|
|
36
|
+
# example output
|
|
37
|
+
examples/rag_pipeline/output/
|
|
38
|
+
.claude/
|
ragradar-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ragradar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: RAG observability: capture, evaluate, and explain pipeline runs (umbrella package + analyst CLI)
|
|
5
|
+
Project-URL: Homepage, https://github.com/pleokarthik/RAGRadar
|
|
6
|
+
Project-URL: Repository, https://github.com/pleokarthik/RAGRadar
|
|
7
|
+
Project-URL: Issues, https://github.com/pleokarthik/RAGRadar/issues
|
|
8
|
+
Author-email: Leo Karthik Paramasivan <pleokarthik@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Requires-Dist: click>=8.0
|
|
18
|
+
Requires-Dist: ragradar-capture<0.2.0,>=0.1.0
|
|
19
|
+
Requires-Dist: ragradar-core<0.2.0,>=0.1.0
|
|
20
|
+
Requires-Dist: ragradar-evaluate<0.2.0,>=0.1.0
|
|
21
|
+
Requires-Dist: rich>=13.0
|
|
22
|
+
Provides-Extra: semantic
|
|
23
|
+
Requires-Dist: sentence-transformers>=3.0; extra == 'semantic'
|
|
24
|
+
Requires-Dist: sqlite-vec>=0.1; extra == 'semantic'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# ragradar
|
|
28
|
+
|
|
29
|
+
Analyst CLI for the ragradar observability system. Reads the local store at
|
|
30
|
+
`~/.ragradar/runs.db` that `ragradar-capture` writes; browses sessions, searches
|
|
31
|
+
runs, and explains exactly what went into a run's context window.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
pip install ragradar
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Runs are addressed as `sNrN` (session 2, run 3 → `s2r3`) — the id every
|
|
38
|
+
capture call returns. Commands that take a `<target>` accept an exact
|
|
39
|
+
id, nothing (= latest run), or a quoted text hint (searched; multiple
|
|
40
|
+
matches show a pick list).
|
|
41
|
+
|
|
42
|
+
## ragradar list — sessions and runs
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
ragradar list # sessions, newest first
|
|
46
|
+
ragradar list s2 # runs inside session 2
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Sessions
|
|
51
|
+
| ID | Runs | Pipeline | Created | Title |
|
|
52
|
+
|----+------+-------------+------------+-------|
|
|
53
|
+
| s2 | 3 | rag_example | 2026-07-02 | |
|
|
54
|
+
| s1 | 1 | quickstart | 2026-07-02 | |
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## ragradar find — search runs by query text
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
ragradar find "reranking" # token match (FTS5)
|
|
61
|
+
ragradar find "score scale" --exact # phrase match
|
|
62
|
+
ragradar find "RRF" --session s2 # scope to a session
|
|
63
|
+
ragradar find "RRF" --pipeline rag_example
|
|
64
|
+
ragradar find "RRF" --from 2026-07-01 --to 2026-07-02
|
|
65
|
+
ragradar find "RRF" --today
|
|
66
|
+
ragradar find --recent 5 # latest N runs, no hint
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Search results (2)
|
|
71
|
+
| Run | Date | Session | Query |
|
|
72
|
+
|-------+------------+---------+----------------------------------|
|
|
73
|
+
| s2 r3 | 2026-07-02 | | what is RRF and how does it ... |
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## ragradar explain — the seven analysis factors
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
ragradar explain # latest run
|
|
80
|
+
ragradar explain s2r3 # specific run
|
|
81
|
+
ragradar explain s2r3 --full
|
|
82
|
+
ragradar explain s2r3 --html # snapshot to ~/.ragradar/reports/s2r3.html
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Renders every factor the captured data supports, silently skipping the
|
|
86
|
+
rest: token usage, chunk scores, duplicates, truncation, dropped
|
|
87
|
+
history, cache hits, and the final prompt. Runs scored by
|
|
88
|
+
`ragradar-evaluate` also show an Evaluation Scores panel (risk score, policy
|
|
89
|
+
violations, RAGAS metrics).
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
+------------- Token Usage --------------+
|
|
93
|
+
| Total: 1138/4096 (27.8%) |
|
|
94
|
+
| Chunks: 625 |
|
|
95
|
+
| Headroom: 196 |
|
|
96
|
+
+----------------------------------------+
|
|
97
|
+
+------------- Duplicates ---------------+
|
|
98
|
+
| 1 duplicate (25%): 1 window |
|
|
99
|
+
+----------------------------------------+
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## ragradar diff — compare two runs
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
ragradar diff s2r1 s2r3
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Side-by-side query delta, chunks added/removed, per-chunk score deltas,
|
|
109
|
+
token budget deltas, history and truncation changes. Ambiguous targets
|
|
110
|
+
are rejected — use exact ids here.
|
|
111
|
+
|
|
112
|
+
## ragradar budget — token waterfall only
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
ragradar budget s2r3
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
+------------- Token Usage --------------+
|
|
120
|
+
| Total: 1138/4096 (27.8%) |
|
|
121
|
+
| Chunks: 625 History: 13 |
|
|
122
|
+
| System: 500 Headroom: 196 |
|
|
123
|
+
+----------------------------------------+
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## ragradar session rename
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
ragradar session rename s2 "RRF investigation"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
Session 2 renamed to "RRF investigation".
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Notes
|
|
137
|
+
|
|
138
|
+
- Read-mostly by design: the only data this CLI writes is a session
|
|
139
|
+
title. (Opening the store may create/migrate `runs.db` via `ragradar-core`
|
|
140
|
+
— that's environment setup, not run data.)
|
|
141
|
+
- No LLM anywhere in the navigation path; search is SQLite FTS5.
|
|
142
|
+
- Optional semantic search: `pip install ragradar[semantic]`.
|
ragradar-0.1.0/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# ragradar
|
|
2
|
+
|
|
3
|
+
Analyst CLI for the ragradar observability system. Reads the local store at
|
|
4
|
+
`~/.ragradar/runs.db` that `ragradar-capture` writes; browses sessions, searches
|
|
5
|
+
runs, and explains exactly what went into a run's context window.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install ragradar
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Runs are addressed as `sNrN` (session 2, run 3 → `s2r3`) — the id every
|
|
12
|
+
capture call returns. Commands that take a `<target>` accept an exact
|
|
13
|
+
id, nothing (= latest run), or a quoted text hint (searched; multiple
|
|
14
|
+
matches show a pick list).
|
|
15
|
+
|
|
16
|
+
## ragradar list — sessions and runs
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
ragradar list # sessions, newest first
|
|
20
|
+
ragradar list s2 # runs inside session 2
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Sessions
|
|
25
|
+
| ID | Runs | Pipeline | Created | Title |
|
|
26
|
+
|----+------+-------------+------------+-------|
|
|
27
|
+
| s2 | 3 | rag_example | 2026-07-02 | |
|
|
28
|
+
| s1 | 1 | quickstart | 2026-07-02 | |
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## ragradar find — search runs by query text
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
ragradar find "reranking" # token match (FTS5)
|
|
35
|
+
ragradar find "score scale" --exact # phrase match
|
|
36
|
+
ragradar find "RRF" --session s2 # scope to a session
|
|
37
|
+
ragradar find "RRF" --pipeline rag_example
|
|
38
|
+
ragradar find "RRF" --from 2026-07-01 --to 2026-07-02
|
|
39
|
+
ragradar find "RRF" --today
|
|
40
|
+
ragradar find --recent 5 # latest N runs, no hint
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Search results (2)
|
|
45
|
+
| Run | Date | Session | Query |
|
|
46
|
+
|-------+------------+---------+----------------------------------|
|
|
47
|
+
| s2 r3 | 2026-07-02 | | what is RRF and how does it ... |
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## ragradar explain — the seven analysis factors
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
ragradar explain # latest run
|
|
54
|
+
ragradar explain s2r3 # specific run
|
|
55
|
+
ragradar explain s2r3 --full
|
|
56
|
+
ragradar explain s2r3 --html # snapshot to ~/.ragradar/reports/s2r3.html
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Renders every factor the captured data supports, silently skipping the
|
|
60
|
+
rest: token usage, chunk scores, duplicates, truncation, dropped
|
|
61
|
+
history, cache hits, and the final prompt. Runs scored by
|
|
62
|
+
`ragradar-evaluate` also show an Evaluation Scores panel (risk score, policy
|
|
63
|
+
violations, RAGAS metrics).
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
+------------- Token Usage --------------+
|
|
67
|
+
| Total: 1138/4096 (27.8%) |
|
|
68
|
+
| Chunks: 625 |
|
|
69
|
+
| Headroom: 196 |
|
|
70
|
+
+----------------------------------------+
|
|
71
|
+
+------------- Duplicates ---------------+
|
|
72
|
+
| 1 duplicate (25%): 1 window |
|
|
73
|
+
+----------------------------------------+
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## ragradar diff — compare two runs
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
ragradar diff s2r1 s2r3
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Side-by-side query delta, chunks added/removed, per-chunk score deltas,
|
|
83
|
+
token budget deltas, history and truncation changes. Ambiguous targets
|
|
84
|
+
are rejected — use exact ids here.
|
|
85
|
+
|
|
86
|
+
## ragradar budget — token waterfall only
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
ragradar budget s2r3
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
+------------- Token Usage --------------+
|
|
94
|
+
| Total: 1138/4096 (27.8%) |
|
|
95
|
+
| Chunks: 625 History: 13 |
|
|
96
|
+
| System: 500 Headroom: 196 |
|
|
97
|
+
+----------------------------------------+
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## ragradar session rename
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
ragradar session rename s2 "RRF investigation"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
Session 2 renamed to "RRF investigation".
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Notes
|
|
111
|
+
|
|
112
|
+
- Read-mostly by design: the only data this CLI writes is a session
|
|
113
|
+
title. (Opening the store may create/migrate `runs.db` via `ragradar-core`
|
|
114
|
+
— that's environment setup, not run data.)
|
|
115
|
+
- No LLM anywhere in the navigation path; search is SQLite FTS5.
|
|
116
|
+
- Optional semantic search: `pip install ragradar[semantic]`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ragradar"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "RAG observability: capture, evaluate, and explain pipeline runs (umbrella package + analyst CLI)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Leo Karthik Paramasivan", email = "pleokarthik@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"ragradar-core>=0.1.0,<0.2.0",
|
|
21
|
+
"ragradar-capture>=0.1.0,<0.2.0",
|
|
22
|
+
"ragradar-evaluate>=0.1.0,<0.2.0",
|
|
23
|
+
"rich>=13.0",
|
|
24
|
+
"click>=8.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/pleokarthik/RAGRadar"
|
|
29
|
+
Repository = "https://github.com/pleokarthik/RAGRadar"
|
|
30
|
+
Issues = "https://github.com/pleokarthik/RAGRadar/issues"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
semantic = [
|
|
34
|
+
"sentence-transformers>=3.0",
|
|
35
|
+
"sqlite-vec>=0.1",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
ragradar = "ragradar.cli:main"
|
|
40
|
+
|
|
41
|
+
[tool.uv.sources]
|
|
42
|
+
ragradar-core = { workspace = true }
|
|
43
|
+
ragradar-capture = { workspace = true }
|
|
44
|
+
ragradar-evaluate = { workspace = true }
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["hatchling"]
|
|
48
|
+
build-backend = "hatchling.build"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/ragradar"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""ragradar — the single public import surface.
|
|
2
|
+
|
|
3
|
+
Users only ever write ``import ragradar``: capture entry points
|
|
4
|
+
(capture/start and the staged proxies), evaluation entry points
|
|
5
|
+
(check/evaluate/available_metrics), and the schema dataclasses are all
|
|
6
|
+
re-exported here. The underlying distributions (ragradar-core,
|
|
7
|
+
ragradar-capture, ragradar-evaluate) stay separately installable so a
|
|
8
|
+
production pipeline can depend on ragradar-capture alone without
|
|
9
|
+
pulling the evaluation stack (scipy/ragas) — but importing their
|
|
10
|
+
modules directly is an internal concern, not the public API.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from ragradar_capture import (
|
|
14
|
+
Capture,
|
|
15
|
+
cache,
|
|
16
|
+
capture,
|
|
17
|
+
chunks,
|
|
18
|
+
commit,
|
|
19
|
+
context,
|
|
20
|
+
history,
|
|
21
|
+
response,
|
|
22
|
+
set_strict,
|
|
23
|
+
start,
|
|
24
|
+
tool_call,
|
|
25
|
+
)
|
|
26
|
+
from ragradar_core.schema import (
|
|
27
|
+
CacheEvent,
|
|
28
|
+
ChunkRecord,
|
|
29
|
+
RunRecord,
|
|
30
|
+
TokenBudget,
|
|
31
|
+
TokenUsage,
|
|
32
|
+
ToolCallRecord,
|
|
33
|
+
Turn,
|
|
34
|
+
)
|
|
35
|
+
from ragradar_evaluate import (
|
|
36
|
+
CheckResult,
|
|
37
|
+
EvalResult,
|
|
38
|
+
InputQualityPolicy,
|
|
39
|
+
MetricInfo,
|
|
40
|
+
available_metrics,
|
|
41
|
+
check,
|
|
42
|
+
evaluate,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
# Capture
|
|
47
|
+
"Capture",
|
|
48
|
+
"start",
|
|
49
|
+
"capture",
|
|
50
|
+
"set_strict",
|
|
51
|
+
"chunks",
|
|
52
|
+
"context",
|
|
53
|
+
"history",
|
|
54
|
+
"response",
|
|
55
|
+
"cache",
|
|
56
|
+
"tool_call",
|
|
57
|
+
"commit",
|
|
58
|
+
# Evaluation
|
|
59
|
+
"check",
|
|
60
|
+
"evaluate",
|
|
61
|
+
"available_metrics",
|
|
62
|
+
"CheckResult",
|
|
63
|
+
"EvalResult",
|
|
64
|
+
"MetricInfo",
|
|
65
|
+
"InputQualityPolicy",
|
|
66
|
+
# Schema dataclasses (advanced path; primitives coerce everywhere)
|
|
67
|
+
"ChunkRecord",
|
|
68
|
+
"TokenBudget",
|
|
69
|
+
"TokenUsage",
|
|
70
|
+
"Turn",
|
|
71
|
+
"CacheEvent",
|
|
72
|
+
"ToolCallRecord",
|
|
73
|
+
"RunRecord",
|
|
74
|
+
]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import date
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
from ragradar import store
|
|
9
|
+
from ragradar.explain import loader
|
|
10
|
+
from ragradar.explain.renderer import html as html_renderer
|
|
11
|
+
from ragradar.explain.renderer import terminal as terminal_renderer
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
_SESSION_RE = re.compile(r"^s(\d+)$", re.IGNORECASE)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _parse_session_id(value: str) -> int:
|
|
19
|
+
m = _SESSION_RE.match(value)
|
|
20
|
+
if m:
|
|
21
|
+
return int(m.group(1))
|
|
22
|
+
return int(value)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _disambiguate(results: list[dict]) -> dict | None:
|
|
26
|
+
console.print("\n [bold]Multiple matches:[/bold]\n")
|
|
27
|
+
for i, r in enumerate(results, 1):
|
|
28
|
+
title = r.get("session_title") or ""
|
|
29
|
+
query_preview = r["query"][:60]
|
|
30
|
+
console.print(
|
|
31
|
+
f" {i} s{r['session_id']} r{r['run_seq']} "
|
|
32
|
+
f"{r['created_at'][:10]} {title} "
|
|
33
|
+
f'— "{query_preview}"'
|
|
34
|
+
)
|
|
35
|
+
console.print()
|
|
36
|
+
try:
|
|
37
|
+
choice = click.prompt(
|
|
38
|
+
" Pick (number) or press Enter to cancel",
|
|
39
|
+
default="",
|
|
40
|
+
show_default=False,
|
|
41
|
+
)
|
|
42
|
+
if not choice:
|
|
43
|
+
return None
|
|
44
|
+
idx = int(choice) - 1
|
|
45
|
+
if 0 <= idx < len(results):
|
|
46
|
+
r = results[idx]
|
|
47
|
+
return store.get_run(r["session_id"], r["run_seq"])
|
|
48
|
+
except (ValueError, KeyboardInterrupt, EOFError):
|
|
49
|
+
pass
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _resolve_and_load(target: str | None = None):
|
|
54
|
+
result = store.resolve_target(target)
|
|
55
|
+
if result is None:
|
|
56
|
+
console.print("No runs found.")
|
|
57
|
+
return None, None
|
|
58
|
+
if isinstance(result, list):
|
|
59
|
+
run_row = _disambiguate(result)
|
|
60
|
+
if run_row is None:
|
|
61
|
+
return None, None
|
|
62
|
+
else:
|
|
63
|
+
run_row = result
|
|
64
|
+
record = loader.load_run_record(run_row)
|
|
65
|
+
return run_row, record
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@click.group()
|
|
69
|
+
def main():
|
|
70
|
+
"""ragradar — analyst CLI for the ragradar observability system."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@main.command("list")
|
|
74
|
+
@click.argument("session_id", required=False)
|
|
75
|
+
def list_cmd(session_id):
|
|
76
|
+
"""List sessions, or runs within a session."""
|
|
77
|
+
if session_id is not None:
|
|
78
|
+
sid = _parse_session_id(session_id)
|
|
79
|
+
runs = store.list_runs(sid)
|
|
80
|
+
if not runs:
|
|
81
|
+
console.print(f"No runs found in session {sid}.")
|
|
82
|
+
return
|
|
83
|
+
tbl = Table(title=f"Session {sid} — Runs")
|
|
84
|
+
tbl.add_column("Run", style="cyan")
|
|
85
|
+
tbl.add_column("Date")
|
|
86
|
+
tbl.add_column("Query")
|
|
87
|
+
for r in runs:
|
|
88
|
+
tbl.add_row(
|
|
89
|
+
f"s{r['session_id']} r{r['run_seq']}",
|
|
90
|
+
r["created_at"][:10],
|
|
91
|
+
r["query"][:80],
|
|
92
|
+
)
|
|
93
|
+
console.print(tbl)
|
|
94
|
+
else:
|
|
95
|
+
sessions = store.list_sessions()
|
|
96
|
+
if not sessions:
|
|
97
|
+
console.print("No sessions found.")
|
|
98
|
+
return
|
|
99
|
+
tbl = Table(title="Sessions")
|
|
100
|
+
tbl.add_column("ID", style="cyan")
|
|
101
|
+
tbl.add_column("Runs", justify="right")
|
|
102
|
+
tbl.add_column("Pipeline")
|
|
103
|
+
tbl.add_column("Created")
|
|
104
|
+
tbl.add_column("Title")
|
|
105
|
+
for s in sessions:
|
|
106
|
+
tbl.add_row(
|
|
107
|
+
f"s{s['session_id']}",
|
|
108
|
+
str(s["run_count"]),
|
|
109
|
+
s["pipeline"] or "",
|
|
110
|
+
s["created_at"][:10],
|
|
111
|
+
s["title"] or "",
|
|
112
|
+
)
|
|
113
|
+
console.print(tbl)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@main.command()
|
|
117
|
+
@click.argument("hint", required=False)
|
|
118
|
+
@click.option("--exact", is_flag=True)
|
|
119
|
+
@click.option("--from", "from_dt", default=None)
|
|
120
|
+
@click.option("--to", "to_dt", default=None)
|
|
121
|
+
@click.option("--today", is_flag=True)
|
|
122
|
+
@click.option("--session", "session_filter", default=None)
|
|
123
|
+
@click.option("--pipeline", default=None)
|
|
124
|
+
@click.option("--recent", default=None, type=int)
|
|
125
|
+
def find(hint, exact, from_dt, to_dt, today, session_filter, pipeline, recent):
|
|
126
|
+
"""Search runs by query text."""
|
|
127
|
+
if today:
|
|
128
|
+
today_str = date.today().isoformat()
|
|
129
|
+
if from_dt is None:
|
|
130
|
+
from_dt = today_str
|
|
131
|
+
if to_dt is None:
|
|
132
|
+
to_dt = today_str + "T23:59:59.999999Z"
|
|
133
|
+
|
|
134
|
+
sid = None
|
|
135
|
+
if session_filter is not None:
|
|
136
|
+
sid = _parse_session_id(session_filter)
|
|
137
|
+
|
|
138
|
+
results = store.search_runs(
|
|
139
|
+
hint=hint,
|
|
140
|
+
exact=exact,
|
|
141
|
+
session_id=sid,
|
|
142
|
+
pipeline=pipeline,
|
|
143
|
+
from_dt=from_dt,
|
|
144
|
+
to_dt=to_dt,
|
|
145
|
+
recent_n=recent,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if not results:
|
|
149
|
+
console.print("No matching runs found.")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
tbl = Table(title=f"Search results ({len(results)})")
|
|
153
|
+
tbl.add_column("Run", style="cyan")
|
|
154
|
+
tbl.add_column("Date")
|
|
155
|
+
tbl.add_column("Session")
|
|
156
|
+
tbl.add_column("Query")
|
|
157
|
+
for r in results:
|
|
158
|
+
tbl.add_row(
|
|
159
|
+
f"s{r['session_id']} r{r['run_seq']}",
|
|
160
|
+
r["created_at"][:10],
|
|
161
|
+
r.get("session_title") or "",
|
|
162
|
+
r["query"][:80],
|
|
163
|
+
)
|
|
164
|
+
console.print(tbl)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@main.command()
|
|
168
|
+
@click.argument("target", required=False)
|
|
169
|
+
@click.option("--full", is_flag=True)
|
|
170
|
+
@click.option("--html", "to_html", is_flag=True)
|
|
171
|
+
def explain(target, full, to_html):
|
|
172
|
+
"""Explain a run — all analysis factors."""
|
|
173
|
+
run_row, record = _resolve_and_load(target)
|
|
174
|
+
if record is None:
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
if to_html:
|
|
178
|
+
run_id = f"s{run_row['session_id']}r{run_row['run_seq']}"
|
|
179
|
+
path = html_renderer.render(record, run_id)
|
|
180
|
+
console.print(f"Report written to {path}")
|
|
181
|
+
else:
|
|
182
|
+
terminal_renderer.render(record, full=full, run_row=run_row)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@main.command()
|
|
186
|
+
@click.argument("target_a")
|
|
187
|
+
@click.argument("target_b")
|
|
188
|
+
def diff(target_a, target_b):
|
|
189
|
+
"""Compare two runs side by side."""
|
|
190
|
+
row_a = store.resolve_target(target_a)
|
|
191
|
+
row_b = store.resolve_target(target_b)
|
|
192
|
+
|
|
193
|
+
if row_a is None or row_b is None:
|
|
194
|
+
console.print("Could not resolve both targets.")
|
|
195
|
+
return
|
|
196
|
+
if isinstance(row_a, list) or isinstance(row_b, list):
|
|
197
|
+
console.print("Ambiguous target — use exact run ID (e.g. s2r3).")
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
rec_a = loader.load_run_record(row_a)
|
|
201
|
+
rec_b = loader.load_run_record(row_b)
|
|
202
|
+
|
|
203
|
+
id_a = f"s{row_a['session_id']}r{row_a['run_seq']}"
|
|
204
|
+
id_b = f"s{row_b['session_id']}r{row_b['run_seq']}"
|
|
205
|
+
|
|
206
|
+
terminal_renderer.render_diff(rec_a, rec_b, id_a, id_b)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@main.command()
|
|
210
|
+
@click.argument("target")
|
|
211
|
+
def budget(target):
|
|
212
|
+
"""Token waterfall only."""
|
|
213
|
+
run_row, record = _resolve_and_load(target)
|
|
214
|
+
if record is None:
|
|
215
|
+
return
|
|
216
|
+
terminal_renderer.render_budget(record)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@main.group()
|
|
220
|
+
def session():
|
|
221
|
+
"""Session management commands."""
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@session.command()
|
|
225
|
+
@click.argument("session_id")
|
|
226
|
+
@click.argument("title")
|
|
227
|
+
def rename(session_id, title):
|
|
228
|
+
"""Rename a session."""
|
|
229
|
+
sid = _parse_session_id(session_id)
|
|
230
|
+
store.rename_session(sid, title)
|
|
231
|
+
console.print(f'Session {sid} renamed to "{title}".')
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from ragradar_core.schema import RunRecord
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def analyze(record: RunRecord) -> dict | None:
|
|
5
|
+
if not record.cache_events:
|
|
6
|
+
return None
|
|
7
|
+
|
|
8
|
+
hits = [e for e in record.cache_events if e.hit]
|
|
9
|
+
misses = [e for e in record.cache_events if not e.hit]
|
|
10
|
+
total = len(record.cache_events)
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
"total_events": total,
|
|
14
|
+
"hits": len(hits),
|
|
15
|
+
"misses": len(misses),
|
|
16
|
+
"hit_ratio": len(hits) / total if total else 0.0,
|
|
17
|
+
"hit_chunks": [e.chunk_id for e in hits],
|
|
18
|
+
"miss_chunks": [e.chunk_id for e in misses],
|
|
19
|
+
}
|