coderag-ai 0.3.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.
Files changed (73) hide show
  1. coderag/__init__.py +12 -0
  2. coderag/analyzers/__init__.py +0 -0
  3. coderag/analyzers/base.py +36 -0
  4. coderag/analyzers/flake8_adapter.py +43 -0
  5. coderag/analyzers/pylint_adapter.py +50 -0
  6. coderag/analyzers/workflow.py +107 -0
  7. coderag/api/__init__.py +0 -0
  8. coderag/api/app.py +351 -0
  9. coderag/api/dashboard.py +237 -0
  10. coderag/api/schemas.py +181 -0
  11. coderag/cli/__init__.py +0 -0
  12. coderag/cli/main.py +359 -0
  13. coderag/context/__init__.py +0 -0
  14. coderag/context/builder.py +206 -0
  15. coderag/context/package.py +100 -0
  16. coderag/core/__init__.py +0 -0
  17. coderag/core/config.py +100 -0
  18. coderag/core/logging.py +58 -0
  19. coderag/core/text.py +80 -0
  20. coderag/core/tokens.py +66 -0
  21. coderag/db/__init__.py +0 -0
  22. coderag/db/base.py +58 -0
  23. coderag/db/migrations/env.py +77 -0
  24. coderag/db/migrations/script.py.mako +26 -0
  25. coderag/db/migrations/versions/.gitkeep +0 -0
  26. coderag/db/migrations/versions/9a973932b94d_measure_baseline_tokens.py +41 -0
  27. coderag/db/migrations/versions/c8a40feeda41_initial_schema.py +227 -0
  28. coderag/db/models.py +275 -0
  29. coderag/embeddings/__init__.py +0 -0
  30. coderag/embeddings/base.py +53 -0
  31. coderag/embeddings/hashing.py +43 -0
  32. coderag/embeddings/pipeline.py +87 -0
  33. coderag/embeddings/registry.py +38 -0
  34. coderag/embeddings/sentence_transformer.py +47 -0
  35. coderag/evaluation/__init__.py +0 -0
  36. coderag/evaluation/datasets.py +36 -0
  37. coderag/evaluation/harness.py +246 -0
  38. coderag/git/__init__.py +0 -0
  39. coderag/git/repo.py +139 -0
  40. coderag/indexing/__init__.py +0 -0
  41. coderag/indexing/ignore.py +101 -0
  42. coderag/indexing/indexer.py +445 -0
  43. coderag/llm/__init__.py +0 -0
  44. coderag/llm/anthropic.py +117 -0
  45. coderag/llm/base.py +60 -0
  46. coderag/llm/null.py +16 -0
  47. coderag/llm/registry.py +17 -0
  48. coderag/localdb.py +100 -0
  49. coderag/mcp_server.py +209 -0
  50. coderag/parsing/__init__.py +0 -0
  51. coderag/parsing/base.py +76 -0
  52. coderag/parsing/python.py +294 -0
  53. coderag/parsing/registry.py +40 -0
  54. coderag/retrieval/__init__.py +0 -0
  55. coderag/retrieval/base.py +60 -0
  56. coderag/retrieval/engine.py +176 -0
  57. coderag/retrieval/fusion.py +41 -0
  58. coderag/retrieval/graph.py +139 -0
  59. coderag/retrieval/lexical.py +57 -0
  60. coderag/retrieval/reranking.py +99 -0
  61. coderag/retrieval/semantic.py +53 -0
  62. coderag/retrieval/symbol.py +91 -0
  63. coderag/security/__init__.py +0 -0
  64. coderag/security/authz.py +35 -0
  65. coderag/security/secrets.py +110 -0
  66. coderag/service.py +160 -0
  67. coderag/telemetry.py +72 -0
  68. coderag_ai-0.3.0.dist-info/METADATA +364 -0
  69. coderag_ai-0.3.0.dist-info/RECORD +73 -0
  70. coderag_ai-0.3.0.dist-info/WHEEL +4 -0
  71. coderag_ai-0.3.0.dist-info/entry_points.txt +3 -0
  72. coderag_ai-0.3.0.dist-info/licenses/LICENSE +202 -0
  73. coderag_ai-0.3.0.dist-info/licenses/NOTICE +5 -0
@@ -0,0 +1,237 @@
1
+ """Self-contained observability dashboard (served at GET /dashboard).
2
+
3
+ Read-only view over persisted telemetry: what was queried and how many tokens the
4
+ budgeted context saved versus the retrieved candidate set. No external assets
5
+ (CSP-safe), no template engine — the page fetches /metrics and /queries as JSON.
6
+ Colours use the validated data-viz palette (blue = context sent to the LLM,
7
+ green = tokens saved; neutral track), theme-aware for light/dark.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ DASHBOARD_HTML = r"""<!doctype html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="utf-8">
16
+ <meta name="viewport" content="width=device-width, initial-scale=1">
17
+ <title>CodeRAG · Token Dashboard</title>
18
+ <style>
19
+ :root{
20
+ color-scheme: light;
21
+ --plane:#f9f9f7; --surface:#fcfcfb; --ink:#0b0b0b; --ink2:#52514e; --muted:#898781;
22
+ --grid:#e1e0d9; --ring:rgba(11,11,11,.10);
23
+ --context:#2a78d6; --saved:#0ca30c; --saved-ink:#006300; --track:#e1e0d9;
24
+ --shadow:0 1px 2px rgba(11,11,11,.05),0 4px 16px rgba(11,11,11,.06);
25
+ }
26
+ @media (prefers-color-scheme:dark){ :root:where(:not([data-theme="light"])){
27
+ color-scheme:dark;
28
+ --plane:#0d0d0d; --surface:#1a1a19; --ink:#fff; --ink2:#c3c2b7; --muted:#898781;
29
+ --grid:#2c2c2a; --ring:rgba(255,255,255,.10);
30
+ --context:#3987e5; --saved:#0ca30c; --saved-ink:#0ca30c; --track:#2c2c2a;
31
+ --shadow:0 1px 2px rgba(0,0,0,.4),0 4px 16px rgba(0,0,0,.5);
32
+ }}
33
+ :root[data-theme="dark"]{
34
+ color-scheme:dark;
35
+ --plane:#0d0d0d; --surface:#1a1a19; --ink:#fff; --ink2:#c3c2b7; --muted:#898781;
36
+ --grid:#2c2c2a; --ring:rgba(255,255,255,.10);
37
+ --context:#3987e5; --saved:#0ca30c; --saved-ink:#0ca30c; --track:#2c2c2a;
38
+ --shadow:0 1px 2px rgba(0,0,0,.4),0 4px 16px rgba(0,0,0,.5);
39
+ }
40
+ *{box-sizing:border-box}
41
+ body{margin:0;background:var(--plane);color:var(--ink);
42
+ font-family:system-ui,-apple-system,"Segoe UI",sans-serif;line-height:1.45}
43
+ .wrap{max-width:1180px;margin:0 auto;padding:28px 20px 64px}
44
+ header{display:flex;align-items:baseline;justify-content:space-between;gap:16px;
45
+ flex-wrap:wrap;margin-bottom:22px}
46
+ h1{font-size:20px;margin:0;letter-spacing:-.01em}
47
+ h1 .dot{color:var(--context)}
48
+ .sub{color:var(--ink2);font-size:13px;margin-top:3px}
49
+ .controls{display:flex;gap:8px;align-items:center}
50
+ button{font:inherit;font-size:13px;color:var(--ink2);background:var(--surface);
51
+ border:1px solid var(--ring);border-radius:8px;padding:6px 12px;cursor:pointer}
52
+ button:hover{color:var(--ink);border-color:var(--muted)}
53
+ .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(158px,1fr));gap:12px;
54
+ margin-bottom:26px}
55
+ .tile{background:var(--surface);border:1px solid var(--ring);border-radius:12px;
56
+ padding:14px 16px;box-shadow:var(--shadow)}
57
+ .tile .label{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
58
+ .tile .val{font-size:26px;margin-top:6px;font-variant-numeric:tabular-nums;
59
+ letter-spacing:-.02em}
60
+ .tile .unit{font-size:13px;color:var(--ink2);margin-left:3px}
61
+ .tile.hero .val{color:var(--saved-ink)}
62
+ .card{background:var(--surface);border:1px solid var(--ring);border-radius:12px;
63
+ padding:18px 18px 8px;box-shadow:var(--shadow);margin-bottom:24px}
64
+ .card h2{font-size:14px;margin:0 0 2px}
65
+ .card .hint{font-size:12px;color:var(--muted);margin:0 0 14px}
66
+ .legend{display:flex;gap:16px;font-size:12px;color:var(--ink2);margin:0 0 14px}
67
+ .legend i{display:inline-block;width:11px;height:11px;border-radius:3px;
68
+ margin-right:6px;vertical-align:middle}
69
+ .bars{display:flex;flex-direction:column;gap:9px}
70
+ .bar-row{display:grid;grid-template-columns:190px 1fr 74px;gap:12px;align-items:center}
71
+ .bar-lbl{font-size:12px;color:var(--ink2);white-space:nowrap;overflow:hidden;
72
+ text-overflow:ellipsis}
73
+ .bar-lbl b{color:var(--ink);font-weight:600;font-size:10px;text-transform:uppercase;
74
+ letter-spacing:.04em;margin-right:6px}
75
+ .track{height:18px;background:var(--track);border-radius:5px;display:flex;overflow:hidden}
76
+ .seg{height:100%}
77
+ .seg.context{background:var(--context)}
78
+ .seg.saved{background:var(--saved);margin-left:2px}
79
+ .seg:first-child{border-radius:5px 0 0 5px}
80
+ .seg:last-child{border-radius:0 5px 5px 0}
81
+ .bar-red{font-size:12px;font-variant-numeric:tabular-nums;color:var(--saved-ink);
82
+ text-align:right;font-weight:600}
83
+ .tablewrap{overflow-x:auto}
84
+ table{width:100%;border-collapse:collapse;font-size:12.5px}
85
+ th,td{padding:9px 10px;text-align:right;white-space:nowrap;
86
+ border-bottom:1px solid var(--grid);font-variant-numeric:tabular-nums}
87
+ th{color:var(--muted);font-weight:600;text-transform:uppercase;font-size:10.5px;
88
+ letter-spacing:.05em;position:sticky;top:0;background:var(--surface)}
89
+ td.l,th.l{text-align:left}
90
+ td.q{max-width:340px;overflow:hidden;text-overflow:ellipsis;color:var(--ink2)}
91
+ .badge{font-size:10px;text-transform:uppercase;letter-spacing:.04em;padding:2px 7px;
92
+ border-radius:999px;border:1px solid var(--ring);color:var(--ink2)}
93
+ .badge.ask{color:var(--context);border-color:var(--context)}
94
+ .empty{text-align:center;color:var(--muted);padding:48px 0;font-size:14px}
95
+ .foot{color:var(--muted);font-size:12px;margin-top:8px}
96
+ </style>
97
+ </head>
98
+ <body>
99
+ <div class="wrap">
100
+ <header>
101
+ <div>
102
+ <h1>CodeRAG<span class="dot">.</span> Token Dashboard</h1>
103
+ <div class="sub">What was queried, and how much context was saved before hitting the LLM.</div>
104
+ </div>
105
+ <div class="controls">
106
+ <button id="refresh">Refresh</button>
107
+ <button id="theme">◐ Theme</button>
108
+ </div>
109
+ </header>
110
+
111
+ <section class="kpis" id="kpis"></section>
112
+
113
+ <section class="card">
114
+ <h2>Context vs. saved — recent queries</h2>
115
+ <p class="hint">Bar length = retrieved candidate tokens. Blue = context actually sent; green = tokens saved by dedup + budgeting.</p>
116
+ <div class="legend">
117
+ <span><i style="background:var(--context)"></i>Context sent to LLM</span>
118
+ <span><i style="background:var(--saved)"></i>Tokens saved</span>
119
+ </div>
120
+ <div class="bars" id="bars"></div>
121
+ </section>
122
+
123
+ <section class="card">
124
+ <h2>Query log</h2>
125
+ <p class="hint">Most recent first. Saved = candidate tokens − context tokens.</p>
126
+ <div class="tablewrap">
127
+ <table>
128
+ <thead><tr>
129
+ <th class="l">#</th><th class="l">when</th><th class="l">mode</th><!-- 17 cols -->
130
+ <th class="l">repo</th><th class="l">query</th>
131
+ <th>found</th><th>selected</th><th>candidate</th><th>context</th>
132
+ <th>saved</th><th>reduction</th>
133
+ <th>whole files</th><th>saved vs files</th><th>vs files</th>
134
+ <th>retr ms</th><th>llm in</th><th>llm out</th>
135
+ </tr></thead>
136
+ <tbody id="rows"></tbody>
137
+ </table>
138
+ </div>
139
+ <div class="foot" id="foot"></div>
140
+ </section>
141
+ </div>
142
+
143
+ <script>
144
+ const n = x => (x==null? "—" : Number(x).toLocaleString());
145
+ const pct = x => (x==null? "—" : Number(x).toFixed(1) + "%");
146
+ const esc = s => (s||"").replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
147
+
148
+ function tile(label, val, unit, hero){
149
+ return `<div class="tile ${hero?'hero':''}"><div class="label">${label}</div>`+
150
+ `<div class="val">${val}${unit?`<span class="unit">${unit}</span>`:''}</div></div>`;
151
+ }
152
+
153
+ function renderKpis(m){
154
+ document.getElementById('kpis').innerHTML =
155
+ tile('Saved vs reading files', n(m.total_saved_vs_files), 'tok', true) +
156
+ tile('Reduction vs files', pct(m.reduction_vs_files_percent), '', true) +
157
+ tile('Whole-file baseline', n(m.total_baseline_tokens), 'tok') +
158
+ tile('Tokens saved (budgeting)', n(m.total_tokens_saved)) +
159
+ tile('Overall reduction', pct(m.avg_token_reduction_percent)) +
160
+ tile('Queries', n(m.queries)) +
161
+ tile('Context sent', n(m.total_context_tokens), 'tok') +
162
+ tile('Candidate tokens', n(m.total_candidate_tokens), 'tok') +
163
+ tile('Avg retrieval', n(m.avg_retrieval_latency_ms), 'ms') +
164
+ tile('LLM input', n(m.total_llm_input_tokens), 'tok') +
165
+ tile('LLM output', n(m.total_llm_output_tokens), 'tok');
166
+ }
167
+
168
+ function renderBars(rows){
169
+ const el = document.getElementById('bars');
170
+ const shown = rows.slice(0, 16);
171
+ if(!shown.length){ el.innerHTML = '<div class="empty">No queries yet — run a search, context, or ask.</div>'; return; }
172
+ const max = Math.max(...shown.map(r => r.candidate_tokens || 0), 1);
173
+ el.innerHTML = shown.map(r => {
174
+ const total = (r.candidate_tokens/max)*100;
175
+ const ctx = r.candidate_tokens ? (r.context_tokens/r.candidate_tokens) : 0;
176
+ const cw = total*ctx, sw = total*(1-ctx);
177
+ const title = `${esc(r.query)}\ncandidate ${n(r.candidate_tokens)} · context ${n(r.context_tokens)} · saved ${n(r.tokens_saved)} (${pct(r.reduction_percent)})`;
178
+ return `<div class="bar-row" title="${title}">`+
179
+ `<div class="bar-lbl"><b>${r.mode}</b>${esc(r.query)}</div>`+
180
+ `<div class="track">`+
181
+ `<div class="seg context" style="width:${cw}%"></div>`+
182
+ `<div class="seg saved" style="width:${sw}%"></div>`+
183
+ `</div>`+
184
+ `<div class="bar-red">${pct(r.reduction_percent)}</div>`+
185
+ `</div>`;
186
+ }).join('');
187
+ }
188
+
189
+ function renderTable(rows){
190
+ const tb = document.getElementById('rows');
191
+ if(!rows.length){ tb.innerHTML = '<tr><td class="l empty" colspan="17">No queries recorded yet.</td></tr>'; return; }
192
+ tb.innerHTML = rows.map(r => {
193
+ const when = r.created_at ? new Date(r.created_at).toLocaleString() : '—';
194
+ return `<tr>`+
195
+ `<td class="l">${r.id}</td>`+
196
+ `<td class="l">${esc(when)}</td>`+
197
+ `<td class="l"><span class="badge ${r.mode==='ask'?'ask':''}">${esc(r.mode)}</span></td>`+
198
+ `<td class="l">${esc(r.repository)}</td>`+
199
+ `<td class="l q" title="${esc(r.query)}">${esc(r.query)}</td>`+
200
+ `<td>${n(r.candidates_found)}</td><td>${n(r.candidates_selected)}</td>`+
201
+ `<td>${n(r.candidate_tokens)}</td><td>${n(r.context_tokens)}</td>`+
202
+ `<td style="color:var(--saved-ink);font-weight:600">${n(r.tokens_saved)}</td>`+
203
+ `<td>${pct(r.reduction_percent)}</td>`+
204
+ `<td>${r.baseline_tokens? n(r.baseline_tokens)+' ('+r.baseline_files+'f)' : '—'}</td>`+
205
+ `<td style="color:var(--saved-ink);font-weight:600">${r.baseline_tokens? n(r.saved_vs_files):'—'}</td>`+
206
+ `<td>${r.baseline_tokens? pct(r.reduction_vs_files):'—'}</td>`+
207
+ `<td>${n(r.retrieval_latency_ms)}</td>`+
208
+ `<td>${n(r.llm_input_tokens)}</td><td>${n(r.llm_output_tokens)}</td>`+
209
+ `</tr>`;
210
+ }).join('');
211
+ document.getElementById('foot').textContent = `Showing ${rows.length} most recent queries.`;
212
+ }
213
+
214
+ async function load(){
215
+ try{
216
+ const [m, q] = await Promise.all([
217
+ fetch('metrics').then(r=>r.json()),
218
+ fetch('queries?limit=200').then(r=>r.json()),
219
+ ]);
220
+ renderKpis(m); renderBars(q); renderTable(q);
221
+ }catch(e){
222
+ document.getElementById('kpis').innerHTML =
223
+ '<div class="empty">Could not reach the API. Is the CodeRAG server running?</div>';
224
+ }
225
+ }
226
+
227
+ document.getElementById('refresh').onclick = load;
228
+ document.getElementById('theme').onclick = () => {
229
+ const cur = document.documentElement.dataset.theme;
230
+ document.documentElement.dataset.theme = cur === 'dark' ? 'light' : (cur === 'light' ? 'dark' : 'light');
231
+ };
232
+ load();
233
+ setInterval(load, 15000);
234
+ </script>
235
+ </body>
236
+ </html>
237
+ """
coderag/api/schemas.py ADDED
@@ -0,0 +1,181 @@
1
+ """Pydantic request/response models for the API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class RegisterRepoRequest(BaseModel):
9
+ name: str
10
+ path: str
11
+ url: str | None = None
12
+
13
+
14
+ class RepositoryOut(BaseModel):
15
+ id: int
16
+ name: str
17
+ local_path: str
18
+ default_branch: str
19
+ indexed_commit_sha: str | None = None
20
+
21
+
22
+ class IndexStatusOut(BaseModel):
23
+ repository_id: int
24
+ status: str | None = None
25
+ mode: str | None = None
26
+ files_indexed: int = 0
27
+ symbols_indexed: int = 0
28
+ embeddings_created: int = 0
29
+ relationships: int = 0
30
+ duration_seconds: float = 0.0
31
+ to_commit: str | None = None
32
+ error: str | None = None
33
+
34
+
35
+ class SearchRequest(BaseModel):
36
+ query: str
37
+ repository: str | None = None
38
+ limit: int = 10
39
+ semantic: bool = True
40
+ graph: bool = True
41
+
42
+
43
+ class CandidateOut(BaseModel):
44
+ symbol_id: int
45
+ qualified_name: str
46
+ symbol_type: str
47
+ file_path: str
48
+ start_line: int
49
+ end_line: int
50
+ score: float
51
+ reasons: list[str]
52
+
53
+
54
+ class SearchResponse(BaseModel):
55
+ repository: str
56
+ latency_ms: float
57
+ candidates: list[CandidateOut]
58
+
59
+
60
+ class ContextRequest(BaseModel):
61
+ query: str
62
+ repository: str | None = None
63
+ max_tokens: int | None = None
64
+ finding: str | None = None
65
+ include_prompt: bool = True
66
+
67
+
68
+ class ContextEntryOut(BaseModel):
69
+ category: str
70
+ qualified_name: str
71
+ file_path: str
72
+ start_line: int
73
+ end_line: int
74
+ tokens: int
75
+ reasons: list[str]
76
+
77
+
78
+ class AccountingOut(BaseModel):
79
+ query_tokens: int
80
+ candidates_found: int
81
+ candidates_selected: int
82
+ candidate_tokens: int
83
+ context_tokens: int
84
+ dropped_tokens: int
85
+ final_prompt_tokens: int
86
+ token_reduction_from_candidates: float
87
+
88
+
89
+ class ContextResponse(BaseModel):
90
+ repository: str
91
+ entries: list[ContextEntryOut]
92
+ accounting: AccountingOut
93
+ prompt: str | None = None
94
+
95
+
96
+ class AskRequest(BaseModel):
97
+ query: str
98
+ repository: str | None = None
99
+ max_tokens: int | None = None
100
+ max_output_tokens: int | None = None
101
+
102
+
103
+ class UsageOut(BaseModel):
104
+ input_tokens: int
105
+ output_tokens: int
106
+ cached_input_tokens: int | None = None
107
+ model: str
108
+ latency_ms: float
109
+
110
+
111
+ class AskResponse(BaseModel):
112
+ repository: str
113
+ answer: str
114
+ usage: UsageOut
115
+ accounting: AccountingOut
116
+
117
+
118
+ class SymbolOut(BaseModel):
119
+ id: int
120
+ qualified_name: str
121
+ symbol_name: str
122
+ symbol_type: str
123
+ file_path: str
124
+ start_line: int
125
+ end_line: int
126
+ signature: str | None = None
127
+ docstring: str | None = None
128
+ token_count: int
129
+ source_code: str
130
+
131
+
132
+ class RelationshipOut(BaseModel):
133
+ relationship_type: str
134
+ confidence: float
135
+ target_symbol_id: int | None = None
136
+ target_qualified_name: str | None = None
137
+ target_name: str | None = None
138
+ direction: str = Field(description="outgoing|incoming")
139
+
140
+
141
+ class MetricsOut(BaseModel):
142
+ repositories: int
143
+ symbols: int
144
+ embeddings: int
145
+ relationships: int
146
+ queries: int
147
+ llm_requests: int
148
+ avg_context_tokens: float
149
+ avg_retrieval_latency_ms: float
150
+ total_llm_input_tokens: int
151
+ total_llm_output_tokens: int
152
+ # token-savings aggregates (candidate set -> budgeted context)
153
+ total_candidate_tokens: int
154
+ total_context_tokens: int
155
+ total_tokens_saved: int
156
+ avg_token_reduction_percent: float
157
+ # "what reading whole files would have cost" baseline
158
+ total_baseline_tokens: int
159
+ total_saved_vs_files: int
160
+ reduction_vs_files_percent: float
161
+
162
+
163
+ class QueryRow(BaseModel):
164
+ id: int
165
+ repository: str
166
+ mode: str
167
+ query: str
168
+ candidates_found: int
169
+ candidates_selected: int
170
+ candidate_tokens: int
171
+ context_tokens: int
172
+ tokens_saved: int
173
+ reduction_percent: float
174
+ baseline_tokens: int = 0
175
+ baseline_files: int = 0
176
+ saved_vs_files: int = 0
177
+ reduction_vs_files: float = 0.0
178
+ retrieval_latency_ms: float
179
+ llm_input_tokens: int | None = None
180
+ llm_output_tokens: int | None = None
181
+ created_at: str
File without changes