dsh-context-guardian 0.1.0-alpha.2
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.
- package/CHANGELOG.md +321 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/cg_recall.js +552 -0
- package/configure.py +186 -0
- package/context_guardian.py +1636 -0
- package/cordis.patch.yml +41 -0
- package/docs/dsh-integration.md +110 -0
- package/engine.js +608 -0
- package/index.js +253 -0
- package/modules/cg_bridge.py +251 -0
- package/package.json +74 -0
- package/requirements.txt +4 -0
- package/vendor/LICENSE.dsh-compaction-instant +28 -0
- package/vendor/compiler.js +851 -0
- package/vendor/region.js +455 -0
package/configure.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
configure.py
|
|
4
|
+
============
|
|
5
|
+
Interactive first-run setup for Context Guardian. Asks a few questions about
|
|
6
|
+
YOUR hardware/backend and writes them to .env, so you never hand-edit a config
|
|
7
|
+
file and guess.
|
|
8
|
+
|
|
9
|
+
python configure.py
|
|
10
|
+
|
|
11
|
+
Every question has a sensible default (press Enter to accept). Numeric answers
|
|
12
|
+
are validated, so a typo tells you rather than crashing Guardian at startup.
|
|
13
|
+
Re-run any time to change your answers, or edit .env directly.
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
REPO_ROOT = Path(__file__).resolve().parent
|
|
20
|
+
ENV_PATH = REPO_ROOT / ".env"
|
|
21
|
+
|
|
22
|
+
# Context-window presets, smallest to largest, with rough guidance.
|
|
23
|
+
#
|
|
24
|
+
# The VRAM notes are DELIBERATELY approximate and framed for the audience this
|
|
25
|
+
# tool serves: someone running a quantized ~7-14B model on a consumer GPU. The
|
|
26
|
+
# context window drives the KV-cache size, which is only PART of your VRAM use
|
|
27
|
+
# (the model weights are the other, larger part), and it scales with the
|
|
28
|
+
# model's size too -- a 30B model at 32K needs far more than an 8B at 32K. So
|
|
29
|
+
# treat these as "you'll want at least this much headroom on a typical mid-size
|
|
30
|
+
# model", not a guarantee. The real number for YOUR setup comes from
|
|
31
|
+
# `ollama ps` while your model is loaded. Bigger model => bump a tier.
|
|
32
|
+
CONTEXT_OPTIONS = [
|
|
33
|
+
(4096, "Light use / short chats. Roughly 8 GB+ VRAM on a small model."),
|
|
34
|
+
(8192, "Everyday agent use. Roughly 10 GB+ VRAM."),
|
|
35
|
+
(16384, "Comfortable coding/agent work. Roughly 12 GB+ VRAM."),
|
|
36
|
+
(32768, "Full agent sessions (default). Roughly 12-16 GB VRAM on a 7-14B model."),
|
|
37
|
+
(65536, "Large context. Roughly 20-24 GB VRAM, or heavy quant."),
|
|
38
|
+
(131072, "Very large context. 24 GB+ VRAM / aggressive quantization."),
|
|
39
|
+
]
|
|
40
|
+
DEFAULT_CTX = 32768
|
|
41
|
+
STANDARD_CTX = {tok for tok, _ in CONTEXT_OPTIONS}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def ask_str(prompt: str, default: str) -> str:
|
|
45
|
+
print("\n" + prompt)
|
|
46
|
+
return input(f" [{default}]: ").strip() or default
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def ask_int(prompt: str, default: int, lo: int = 1, hi: int = 10_000_000) -> int:
|
|
50
|
+
"""A validated positive integer. Re-asks on garbage instead of writing a
|
|
51
|
+
value that makes Guardian throw ValueError at import."""
|
|
52
|
+
while True:
|
|
53
|
+
raw = input(f"\n{prompt}\n [{default}]: ").strip()
|
|
54
|
+
if not raw:
|
|
55
|
+
return default
|
|
56
|
+
try:
|
|
57
|
+
val = int(raw.replace(",", "").replace("_", ""))
|
|
58
|
+
except ValueError:
|
|
59
|
+
print(f" '{raw}' is not a whole number. Digits only "
|
|
60
|
+
f"(e.g. 8192), no units or letters.")
|
|
61
|
+
continue
|
|
62
|
+
if not (lo <= val <= hi):
|
|
63
|
+
print(f" {val} is out of range. Expected {lo}..{hi}.")
|
|
64
|
+
continue
|
|
65
|
+
return val
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def ask_float(prompt: str, default: float, lo: float, hi: float) -> float:
|
|
69
|
+
while True:
|
|
70
|
+
raw = input(f"\n{prompt}\n [{default}]: ").strip()
|
|
71
|
+
if not raw:
|
|
72
|
+
return default
|
|
73
|
+
try:
|
|
74
|
+
val = float(raw)
|
|
75
|
+
except ValueError:
|
|
76
|
+
print(f" '{raw}' is not a number.")
|
|
77
|
+
continue
|
|
78
|
+
if not (lo <= val <= hi):
|
|
79
|
+
print(f" {val} is out of range. Expected {lo}..{hi}.")
|
|
80
|
+
continue
|
|
81
|
+
return val
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def ask_context() -> int:
|
|
85
|
+
"""A menu of context-window presets with VRAM guidance, plus a custom
|
|
86
|
+
option that CONFIRMS a non-standard value back to the user."""
|
|
87
|
+
print("\nContext window size (in TOKENS) of the model you're actually running.")
|
|
88
|
+
print("This is the single most important setting -- it MUST match how your")
|
|
89
|
+
print("backend/model is really configured (check `ollama ps` while loaded),")
|
|
90
|
+
print("not the model's theoretical max.\n")
|
|
91
|
+
for i, (tok, blurb) in enumerate(CONTEXT_OPTIONS, 1):
|
|
92
|
+
star = " <- default" if tok == DEFAULT_CTX else ""
|
|
93
|
+
print(f" {i}) {tok:>7,} tokens {blurb}{star}")
|
|
94
|
+
print(f" {len(CONTEXT_OPTIONS) + 1}) other Enter a custom number")
|
|
95
|
+
|
|
96
|
+
default_idx = [i for i, (t, _) in enumerate(CONTEXT_OPTIONS, 1)
|
|
97
|
+
if t == DEFAULT_CTX][0]
|
|
98
|
+
while True:
|
|
99
|
+
raw = input(f"\n Choose 1-{len(CONTEXT_OPTIONS) + 1} "
|
|
100
|
+
f"[{default_idx}]: ").strip()
|
|
101
|
+
if not raw:
|
|
102
|
+
return DEFAULT_CTX
|
|
103
|
+
if not raw.isdigit():
|
|
104
|
+
print(" Enter the NUMBER of an option above.")
|
|
105
|
+
continue
|
|
106
|
+
choice = int(raw)
|
|
107
|
+
if 1 <= choice <= len(CONTEXT_OPTIONS):
|
|
108
|
+
return CONTEXT_OPTIONS[choice - 1][0]
|
|
109
|
+
if choice == len(CONTEXT_OPTIONS) + 1:
|
|
110
|
+
# Ceiling deliberately far above any real context window: an
|
|
111
|
+
# unusually large number should reach the CONFIRMATION below
|
|
112
|
+
# (showing the user their actual figure), not be silently rejected
|
|
113
|
+
# as out of range. Letters/negatives are still refused by ask_int.
|
|
114
|
+
custom = ask_int(" Custom context window, in tokens", DEFAULT_CTX,
|
|
115
|
+
lo=256, hi=100_000_000)
|
|
116
|
+
if custom in STANDARD_CTX:
|
|
117
|
+
return custom
|
|
118
|
+
# Non-standard value: confirm it back with the ACTUAL number, so a
|
|
119
|
+
# fat-fingered 132768 or 13212348 is caught before it is written.
|
|
120
|
+
print(f"\n {custom:,} is not one of the standard sizes.")
|
|
121
|
+
ok = input(f" Are you sure you want a context window of "
|
|
122
|
+
f"{custom:,} tokens? [y/N]: ").strip().lower()
|
|
123
|
+
if ok == "y":
|
|
124
|
+
return custom
|
|
125
|
+
print(" Okay, let's pick again.")
|
|
126
|
+
continue
|
|
127
|
+
print(f" {choice} is not one of the options.")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def main() -> int:
|
|
131
|
+
print("Context Guardian -- first-run setup")
|
|
132
|
+
print("=" * 40)
|
|
133
|
+
print("Press Enter on any question to accept the default in [brackets].")
|
|
134
|
+
|
|
135
|
+
if ENV_PATH.exists():
|
|
136
|
+
if input(f"\n.env already exists at {ENV_PATH}. Overwrite? [y/N]: "
|
|
137
|
+
).strip().lower() != "y":
|
|
138
|
+
print("Left the existing .env untouched. Edit it by hand to tweak "
|
|
139
|
+
"one value.")
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
answers = {}
|
|
143
|
+
answers["GUARDIAN_UPSTREAM_URL"] = ask_str(
|
|
144
|
+
"OpenAI-compatible backend Guardian should forward to\n"
|
|
145
|
+
" (your Ollama /v1 endpoint, or another proxy in front of it)",
|
|
146
|
+
"http://localhost:11434/v1")
|
|
147
|
+
answers["GUARDIAN_NUM_CTX"] = str(ask_context())
|
|
148
|
+
answers["GUARDIAN_PORT"] = str(ask_int(
|
|
149
|
+
"Port Context Guardian itself should listen on", 8786,
|
|
150
|
+
lo=1, hi=65535))
|
|
151
|
+
answers["GUARDIAN_COMPACT_THRESHOLD"] = str(ask_float(
|
|
152
|
+
"Fraction of the context window at which Guardian compacts (0.1-1.0)",
|
|
153
|
+
0.85, lo=0.1, hi=1.0))
|
|
154
|
+
answers["GUARDIAN_KEEP_RECENT_MESSAGES"] = str(ask_int(
|
|
155
|
+
"Most-recent messages to always keep verbatim (never summarized)", 8,
|
|
156
|
+
lo=1, hi=1000))
|
|
157
|
+
answers["GUARDIAN_UPSTREAM_TIMEOUT"] = str(ask_int(
|
|
158
|
+
"Seconds to wait for the upstream backend to respond\n"
|
|
159
|
+
" (raise this if a slow local \"thinking\" model gives 500s only on "
|
|
160
|
+
"real requests)", 600, lo=1, hi=86400))
|
|
161
|
+
answers["GUARDIAN_LOG_PATH"] = ask_str(
|
|
162
|
+
"Where to log compaction events (JSON lines)",
|
|
163
|
+
"logs/context_guardian_log.json")
|
|
164
|
+
|
|
165
|
+
lines = ["# Generated by configure.py -- edit freely, or re-run configure.py.\n"]
|
|
166
|
+
for k, v in answers.items():
|
|
167
|
+
lines.append(f"{k}={v}\n")
|
|
168
|
+
ENV_PATH.write_text("".join(lines), encoding="utf-8")
|
|
169
|
+
|
|
170
|
+
ctx = int(answers["GUARDIAN_NUM_CTX"])
|
|
171
|
+
print(f"\nWrote {ENV_PATH}")
|
|
172
|
+
if ctx not in STANDARD_CTX:
|
|
173
|
+
print(f" (custom context window: {ctx:,} tokens)")
|
|
174
|
+
print("\nNext steps:")
|
|
175
|
+
print(" 1. pip install -r requirements.txt")
|
|
176
|
+
print(" 2. Make sure your real backend is already running.")
|
|
177
|
+
print(" 3. python context_guardian.py")
|
|
178
|
+
print(f" 4. Point your CLI's OPENAI_BASE_URL at "
|
|
179
|
+
f"http://localhost:{answers['GUARDIAN_PORT']}/v1")
|
|
180
|
+
print("\nSee README.md's \"Testing before you trust it\" section before "
|
|
181
|
+
"pointing a real session at it.")
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
sys.exit(main())
|