arcus-cli 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.
Files changed (47) hide show
  1. arcus_cli-0.1.0/.github/workflows/ci.yml +23 -0
  2. arcus_cli-0.1.0/.github/workflows/release.yml +50 -0
  3. arcus_cli-0.1.0/.gitignore +12 -0
  4. arcus_cli-0.1.0/.python-version +1 -0
  5. arcus_cli-0.1.0/LICENSE +21 -0
  6. arcus_cli-0.1.0/PKG-INFO +308 -0
  7. arcus_cli-0.1.0/README.md +278 -0
  8. arcus_cli-0.1.0/pyproject.toml +50 -0
  9. arcus_cli-0.1.0/src/arcus/__init__.py +0 -0
  10. arcus_cli-0.1.0/src/arcus/adapters/__init__.py +0 -0
  11. arcus_cli-0.1.0/src/arcus/adapters/arc_adapter.py +68 -0
  12. arcus_cli-0.1.0/src/arcus/cache/__init__.py +0 -0
  13. arcus_cli-0.1.0/src/arcus/cache/benchmark.py +159 -0
  14. arcus_cli-0.1.0/src/arcus/cache/semantic_cache.py +168 -0
  15. arcus_cli-0.1.0/src/arcus/cli.py +335 -0
  16. arcus_cli-0.1.0/src/arcus/config.py +46 -0
  17. arcus_cli-0.1.0/src/arcus/embeddings.py +34 -0
  18. arcus_cli-0.1.0/src/arcus/eval/__init__.py +0 -0
  19. arcus_cli-0.1.0/src/arcus/eval/offline.py +219 -0
  20. arcus_cli-0.1.0/src/arcus/eval/regret.py +96 -0
  21. arcus_cli-0.1.0/src/arcus/quality/__init__.py +0 -0
  22. arcus_cli-0.1.0/src/arcus/quality/gate.py +225 -0
  23. arcus_cli-0.1.0/src/arcus/routing/__init__.py +0 -0
  24. arcus_cli-0.1.0/src/arcus/routing/bandit.py +226 -0
  25. arcus_cli-0.1.0/src/arcus/routing/context.py +218 -0
  26. arcus_cli-0.1.0/src/arcus/routing/reward.py +91 -0
  27. arcus_cli-0.1.0/src/arcus/routing/warm_start.py +35 -0
  28. arcus_cli-0.1.0/src/arcus/storage/__init__.py +0 -0
  29. arcus_cli-0.1.0/src/arcus/storage/db.py +117 -0
  30. arcus_cli-0.1.0/src/arcus/storage/stats.py +51 -0
  31. arcus_cli-0.1.0/tests/adapters/test_arc_adapter.py +70 -0
  32. arcus_cli-0.1.0/tests/adapters/test_arc_adapter_live.py +42 -0
  33. arcus_cli-0.1.0/tests/cache/test_benchmark.py +21 -0
  34. arcus_cli-0.1.0/tests/cache/test_semantic_cache.py +142 -0
  35. arcus_cli-0.1.0/tests/eval/test_offline.py +172 -0
  36. arcus_cli-0.1.0/tests/eval/test_regret.py +42 -0
  37. arcus_cli-0.1.0/tests/quality/test_gate.py +230 -0
  38. arcus_cli-0.1.0/tests/routing/test_bandit.py +241 -0
  39. arcus_cli-0.1.0/tests/routing/test_context.py +81 -0
  40. arcus_cli-0.1.0/tests/routing/test_reward.py +85 -0
  41. arcus_cli-0.1.0/tests/routing/test_warm_start.py +117 -0
  42. arcus_cli-0.1.0/tests/storage/test_db.py +140 -0
  43. arcus_cli-0.1.0/tests/storage/test_stats.py +83 -0
  44. arcus_cli-0.1.0/tests/test_cli.py +472 -0
  45. arcus_cli-0.1.0/tests/test_config.py +55 -0
  46. arcus_cli-0.1.0/tests/test_embeddings.py +14 -0
  47. arcus_cli-0.1.0/uv.lock +1401 -0
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Install uv
15
+ uses: astral-sh/setup-uv@v3
16
+ with:
17
+ enable-cache: true
18
+
19
+ - name: Install dependencies
20
+ run: uv sync
21
+
22
+ - name: Run tests
23
+ run: uv run pytest tests/ -q
@@ -0,0 +1,50 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Install uv
15
+ uses: astral-sh/setup-uv@v3
16
+ with:
17
+ enable-cache: true
18
+
19
+ - name: Install dependencies
20
+ run: uv sync
21
+
22
+ - name: Run tests
23
+ run: uv run pytest tests/ -q
24
+
25
+ publish:
26
+ needs: test
27
+ runs-on: ubuntu-latest
28
+ environment: pypi
29
+ permissions:
30
+ id-token: write # required for PyPI trusted publishing, no API token needed
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+
34
+ - name: Install uv
35
+ uses: astral-sh/setup-uv@v3
36
+
37
+ - name: Check tag matches pyproject.toml version
38
+ run: |
39
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
40
+ PYPROJECT_VERSION=$(grep -m1 '^version = ' pyproject.toml | sed -E 's/version = "(.*)"/\1/')
41
+ if [ "$TAG_VERSION" != "$PYPROJECT_VERSION" ]; then
42
+ echo "Tag $GITHUB_REF_NAME doesn't match pyproject.toml version $PYPROJECT_VERSION"
43
+ exit 1
44
+ fi
45
+
46
+ - name: Build
47
+ run: uv build
48
+
49
+ - name: Publish to PyPI
50
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ .env
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shakir Farhan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,308 @@
1
+ Metadata-Version: 2.5
2
+ Name: arcus-cli
3
+ Version: 0.1.0
4
+ Summary: Adaptive routing, a quality gate, and a correctness-aware cache on top of Virginia Tech ARC's LLM API
5
+ Project-URL: Homepage, https://github.com/ShakirFarhan/Arcus
6
+ Project-URL: Repository, https://github.com/ShakirFarhan/Arcus
7
+ Project-URL: Issues, https://github.com/ShakirFarhan/Arcus/issues
8
+ Author: Shakir Farhan
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: arc,bandit,cli,llm,routing,virginia-tech
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.13
21
+ Requires-Dist: openai
22
+ Requires-Dist: platformdirs>=4.11.3
23
+ Requires-Dist: pydantic-settings>=2.15.0
24
+ Requires-Dist: pydantic>=2.13.4
25
+ Requires-Dist: rich>=15.0.0
26
+ Requires-Dist: sentence-transformers>=6.0.0
27
+ Requires-Dist: sqlmodel>=0.0.39
28
+ Requires-Dist: typer>=0.27.1
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Arcus
32
+
33
+ A CLI that sits on top of Virginia Tech ARC's LLM API and makes it
34
+ smarter: it picks which of ARC's four open-weight models to route a
35
+ request to, checks the response before handing it back to you, and
36
+ caches answers to questions it's already seen. Runs entirely on your own
37
+ machine with your own ARC key. Nothing goes through a shared server.
38
+
39
+ ```bash
40
+ arcus "explain how binary search works"
41
+ ```
42
+
43
+ ## Why
44
+
45
+ ARC gives every VT user free access to four open-weight models
46
+ (gpt-oss-120b, GLM-5.3, Kimi-K3, DeepSeek-V4-Flash) through one
47
+ OpenAI-compatible endpoint. Picking a model by hand every time is
48
+ tedious, and a plain HTTP 200 doesn't tell you whether the response
49
+ inside it was actually any good, a truncated answer or a flat refusal
50
+ comes back looking the same as a correct one unless something reads the
51
+ content. Arcus adds three things on top of the raw API:
52
+
53
+ - **Adaptive routing** — a multi-armed bandit learns, per kind of
54
+ question, which model tends to give the best result for the least
55
+ latency and cost.
56
+ - **A quality gate** — validates every response (truncation, empty
57
+ output, repetition loops, refusal phrases, schema conformance) before
58
+ it reaches you, and silently retries with a different model if the
59
+ first one produced garbage.
60
+ - **A correctness-aware cache** — skips the API call entirely for
61
+ questions it's answered before, but only when it's actually confident
62
+ the new question means the same thing as the cached one.
63
+
64
+ ## How it works
65
+
66
+ ```
67
+ your question (CLI arg or piped stdin)
68
+ |
69
+ v
70
+ context classifier -- code / reasoning-math / writing / long-document / general
71
+ |
72
+ v
73
+ semantic cache check -- hit? return the cached answer, skip everything below
74
+ | miss
75
+ v
76
+ bandit router -- picks a model, one bandit instance per (task type, length) bucket
77
+ |
78
+ v
79
+ ARC API call (your own key, OpenAI-compatible endpoint)
80
+ |
81
+ v
82
+ quality gate -- validates the response, retries with a different model on failure
83
+ | pass
84
+ v
85
+ answer to you + write to cache + reward logged back to the bandit
86
+ ```
87
+
88
+ Everything after "your question" runs locally. The only network call
89
+ this tool ever makes is to ARC, with your own key.
90
+
91
+ ### Context classification
92
+
93
+ Regex/keyword rules catch the obvious cases fast (a traceback is
94
+ obviously a code question, "write me a poem" is obviously a writing
95
+ request). Anything that doesn't match falls back to comparing the
96
+ prompt's embedding against a small set of labeled anchor examples per
97
+ category, so phrasing the regex rules never thought of still lands in
98
+ the right bucket instead of defaulting to "general." See
99
+ `src/arcus/routing/context.py`.
100
+
101
+ ### Adaptive routing
102
+
103
+ Three interchangeable bandit algorithms, picked via config
104
+ (`bandit_algorithm` in `~/.config/arcus/config.toml`, default
105
+ `thompson`):
106
+
107
+ - **Epsilon-greedy** — simplest baseline, explores randomly a fixed
108
+ fraction of the time.
109
+ - **UCB1** — no tunable knob, explores under-tried arms automatically
110
+ via a confidence bound.
111
+ - **Thompson sampling** — Bayesian, samples from each arm's learned
112
+ `Beta` distribution, the default because it adapts fastest early on.
113
+
114
+ A random-selection baseline (`--random`) is also wired in as an A/B
115
+ comparison point, mostly useful for the offline evaluation report below.
116
+ The reward each arm is updated with is a weighted mix of quality (from
117
+ the gate below), normalized latency, and a simulated cost signal built
118
+ from real published hosting rates for these same open-weight models
119
+ (ARC itself is free, this exists to demonstrate cost-aware routing as a
120
+ practice). See `src/arcus/routing/bandit.py` and
121
+ `src/arcus/routing/reward.py`.
122
+
123
+ Since every `arcus` invocation is a fresh process, there's no daemon
124
+ holding the bandit's learned state in memory between runs. Instead,
125
+ `src/arcus/routing/warm_start.py` rebuilds it at the start of every call
126
+ by replaying the local request log, which works because a bandit's
127
+ `update()` is just an associative accumulation of pull counts and reward
128
+ sums.
129
+
130
+ ### Quality gate
131
+
132
+ Five independent checks run over every response: truncation
133
+ (`finish_reason == "length"`), empty output, repetition (trigram
134
+ duplication ratio), refusal-phrase matching, and optional Pydantic
135
+ schema validation for structured-output requests. Any failure logs a
136
+ negative reward for that model in that context and retries with a
137
+ different one, up to once per available arm, before giving up and
138
+ returning the last attempt. See `src/arcus/quality/gate.py`.
139
+
140
+ ### Semantic cache
141
+
142
+ Local `sentence-transformers` embeddings (`all-MiniLM-L6-v2`), cosine
143
+ similarity lookup against everything stored so far. Two things keep it
144
+ from just being a naive "similar enough, ship it" cache:
145
+
146
+ - **Volatility classification** — a query containing words like
147
+ "today," "current," or "latest" gets a TTL of zero (never actually
148
+ served stale), stable conceptual questions get a week.
149
+ - **Parameter-diff check** — before trusting a high-similarity match,
150
+ numbers and capitalized entities extracted from both queries are
151
+ compared. "when is project 2 due" and "when is project 3 due" read as
152
+ almost identical to a cosine similarity score, this check catches
153
+ that they're different questions.
154
+
155
+ Measured against a 62-pair labeled benchmark of true paraphrases and
156
+ near-duplicate-but-different prompts (`src/arcus/cache/benchmark.py`):
157
+
158
+ | approach | precision | recall |
159
+ | -------------------------- | --------- | ------ |
160
+ | naive cosine similarity | 0.306 | 0.688 |
161
+ | + parameter-diff check | 1.000 | 0.625 |
162
+
163
+ The param-diff check trades some recall (it rejects a few pairs it
164
+ shouldn't, "World War 1" vs "the First World War" gets flagged as a
165
+ conflicting parameter, a known and documented limitation) for a real
166
+ jump in precision, going from roughly 1-in-3 cache hits being wrong to
167
+ zero false hits in this benchmark.
168
+
169
+ ### Offline policy evaluation and regret benchmarking
170
+
171
+ Every request logs the propensity (the probability the routing policy
172
+ assigned to whichever model it picked), which makes it possible to
173
+ estimate how a *different* policy would have performed without ever
174
+ running it live, using only the log that already exists. `arcus` logs
175
+ propensity from the very first request, this can't be added
176
+ retroactively to old data.
177
+
178
+ `src/arcus/eval/offline.py` implements inverse propensity scoring (IPS)
179
+ and doubly robust (DR) estimators plus percentile bootstrap confidence
180
+ intervals, and `evaluate_policies()` produces a comparison table:
181
+ the logged policy's actual average reward next to estimated values for
182
+ any alternative policies you want to compare it against (e.g. "what if
183
+ we'd always used the cheapest model").
184
+
185
+ Regret benchmarking is a different technique: it needs a *known*
186
+ ground-truth reward per arm to measure regret against, which real
187
+ traffic can't provide (a real request only ever explores one model per
188
+ round, so there's no way to know what the other three would have
189
+ scored). `src/arcus/eval/regret.py` simulates each algorithm against a
190
+ labeled synthetic reward environment instead, this is the standard way
191
+ to study a bandit algorithm's exploration behavior on its own, separate
192
+ from real-world model quality. A sample run (2000 rounds, seed 42):
193
+
194
+ | algorithm | final cumulative regret |
195
+ | -------------- | ------------------------ |
196
+ | epsilon-greedy | 7.9 |
197
+ | thompson | 25.4 |
198
+ | ucb1 | 58.6 |
199
+ | random | 76.9 |
200
+
201
+ All three real algorithms land well below the random baseline, which is
202
+ the actual point: they're spending far less time on worse-than-best
203
+ arms than picking blindly would.
204
+
205
+ ## Install
206
+
207
+ ```bash
208
+ pip install arcus-cli
209
+ # or, with uv
210
+ uv tool install arcus-cli
211
+ ```
212
+
213
+ Or run from source:
214
+
215
+ ```bash
216
+ git clone https://github.com/ShakirFarhan/Arcus.git
217
+ cd Arcus
218
+ uv sync
219
+ uv run arcus "explain how binary search works"
220
+ ```
221
+
222
+ First run walks you through a one-time setup: it asks for your ARC key
223
+ (get one from `llm.arc.vt.edu` under User profile > Settings > Account
224
+ > API keys), makes one live call to check it works, and saves it to
225
+ `~/.config/arcus/config.toml` with `chmod 600`. No separate setup
226
+ command to remember.
227
+
228
+ ## Usage
229
+
230
+ ```bash
231
+ # ask something directly
232
+ arcus "explain how binary search works"
233
+
234
+ # pipe an error straight in
235
+ python broken.py 2>&1 | arcus
236
+
237
+ # or combine piped context with an explicit instruction
238
+ python broken.py 2>&1 | arcus "why is this failing"
239
+
240
+ # force the random-routing baseline instead of the learned bandit policy
241
+ arcus --random "explain how binary search works"
242
+
243
+ # see how it's doing
244
+ arcus stats
245
+
246
+ # hold a multi-turn conversation instead of a single question
247
+ arcus chat
248
+ ```
249
+
250
+ `arcus chat` opens a REPL that remembers everything said earlier in that
251
+ session (resending the growing transcript each turn, since ARC's API has
252
+ no session concept of its own) and routes each turn through the same
253
+ bandit/quality-gate/logging pipeline as a one-shot `arcus "..."` call.
254
+ Type `exit` or press ctrl-d to leave. The conversation only lives for
255
+ that one run, closing the terminal loses it.
256
+
257
+ `arcus stats` reads your local SQLite log and prints a `rich`-formatted
258
+ table: request count, average reward, average latency, and cost score
259
+ per model per mode, plus your cache hit rate and how many attempts the
260
+ quality gate has caught and retried. Entirely local, no network call.
261
+
262
+ ## Status
263
+
264
+ Everything described above is implemented and working: the ARC adapter,
265
+ context classification, all three bandit algorithms with propensity
266
+ tracking, the reward function, the quality gate, the semantic cache and
267
+ its benchmark, the CLI (ask command, chat mode, first-run wizard, error
268
+ piping, stats), and the offline evaluation + regret benchmarking layer.
269
+
270
+ Verified live against a real ARC key: all four models respond correctly
271
+ (`tests/adapters/test_arc_adapter_live.py`), and a full end-to-end
272
+ `arcus "..."` run exercises the whole pipeline (context classification,
273
+ cache miss, bandit routing, a real ARC call, the quality gate, logging,
274
+ and caching the result) against real traffic. Test suite: 178 passing
275
+ with a key set (174 plus 4 live-only tests), 4 skipped without one.
276
+
277
+ Worth knowing: ARC's models are reasoning models under the hood, they
278
+ write to a hidden `reasoning` field before `content`, so a small
279
+ `max_tokens` budget can get entirely spent on reasoning before any real
280
+ answer comes out. The CLI itself never sets `max_tokens`, so normal
281
+ usage isn't affected, ARC's server-side default leaves plenty of room,
282
+ this only matters if you're calling the adapter directly with a tight
283
+ budget of your own.
284
+
285
+ What's still open:
286
+
287
+ - Not published to PyPI yet.
288
+ - Real logged usage is still thin (a handful of manual runs). Once
289
+ there's a real query history, `arcus/eval/offline.py`'s
290
+ `evaluate_policies()` is what turns it into the comparison table
291
+ described above.
292
+
293
+ ## Security & privacy
294
+
295
+ - Each install uses its own user's ARC key. Keys are never shared,
296
+ bundled, or sent anywhere but ARC's own endpoint.
297
+ - No data leaves your machine by default. Request logs, cache entries,
298
+ and stats are all local SQLite, nothing is aggregated or reported
299
+ anywhere.
300
+ - This tool hasn't been through ARC's security review for regulated
301
+ data (FERPA records, health data, etc.) the way ARC's own web
302
+ interface has. Don't route sensitive regulated data through it.
303
+ - MIT licensed, source is fully readable, that's the actual trust
304
+ mechanism here rather than a policy document.
305
+
306
+ ## License
307
+
308
+ MIT, see `LICENSE`.