@lzhzzzzwill/cofos 1.1.15 → 1.2.0
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/README.md +5 -2
- package/RUNTIME.md +14 -0
- package/bin/cofos.js +244 -156
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -14,9 +14,11 @@ The standalone npm package includes a minimal Python runtime under `runtime/`:
|
|
|
14
14
|
fwdemo/
|
|
15
15
|
├── bin/cofos.js
|
|
16
16
|
├── scripts/chat.py
|
|
17
|
+
├── scripts/sync_runtime.py
|
|
17
18
|
├── runtime/
|
|
18
19
|
│ ├── config/
|
|
19
20
|
│ └── src/
|
|
21
|
+
├── RUNTIME.md
|
|
20
22
|
└── requirements.txt
|
|
21
23
|
```
|
|
22
24
|
|
|
@@ -139,8 +141,9 @@ under `runtime/` exists only so the published package is self-contained.
|
|
|
139
141
|
- Fix bugs in `src/` (or `config/`), never in `runtime/`.
|
|
140
142
|
- `npm pack` / `npm publish` rebuild `runtime/` automatically via the package-local
|
|
141
143
|
`prepack` wrapper when the full source checkout is present.
|
|
142
|
-
- Rebuild by hand with `npm run build
|
|
143
|
-
|
|
144
|
+
- Rebuild by hand from `fwdemo/` with `npm run build`.
|
|
145
|
+
- From the repository root, rebuild with `python scripts/sync_fwdemo_runtime.py`.
|
|
146
|
+
- The package-local wrapper `fwdemo/scripts/sync_runtime.py` delegates to the root sync script in a source checkout, and only validates bundled files in an installed package.
|
|
144
147
|
- Check for drift with `python scripts/sync_fwdemo_runtime.py --check`.
|
|
145
148
|
|
|
146
149
|
The published package requires its bundled `runtime/`; the backend does not fall
|
package/RUNTIME.md
CHANGED
|
@@ -21,6 +21,20 @@ cofos --model /path/to/local/merged_model
|
|
|
21
21
|
|
|
22
22
|
PDFs passed with `--pdf-dir` are parsed into local chunks and merged into BM25 retrieval data under the COFOS cache root, defaulting to `./.cofos/`. They are not uploaded anywhere.
|
|
23
23
|
|
|
24
|
+
## Packaged file scope
|
|
25
|
+
|
|
26
|
+
The npm package intentionally ships only the runtime subset needed for QA:
|
|
27
|
+
|
|
28
|
+
- `runtime/config/config.yaml`
|
|
29
|
+
- `runtime/config/prompt_templates.yaml`
|
|
30
|
+
- PDF parsing helpers
|
|
31
|
+
- student inference, query routing, KG/BM25 retrieval, BM25 building, and text search
|
|
32
|
+
|
|
33
|
+
Training, extraction, SFT generation, FAISS building, and full KG construction
|
|
34
|
+
remain repository-only tools and are not included in the package. The package
|
|
35
|
+
uses `fwdemo/scripts/sync_runtime.py` during `prepack`; in a source checkout that
|
|
36
|
+
wrapper delegates to `scripts/sync_fwdemo_runtime.py`.
|
|
37
|
+
|
|
24
38
|
## Runtime config scope
|
|
25
39
|
|
|
26
40
|
The packaged `runtime/config/config.yaml` is generated from the full project config with a strict whitelist. It keeps only the fields needed by the standalone QA path: paths, retrieval, generation, prompt location, ROS normalization, and the student model pointer.
|
package/bin/cofos.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
-
import
|
|
3
|
+
import blessed from "blessed";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -16,6 +16,7 @@ const DEFAULT_CONFIG = process.env.COFOS_CONFIG || "config/config.yaml";
|
|
|
16
16
|
/* ─── ANSI ─── */
|
|
17
17
|
const R = "\x1b[0m", B = "\x1b[1m", D = "\x1b[2m";
|
|
18
18
|
const C = "\x1b[96m", W = "\x1b[97m", G = "\x1b[90m", Y = "\x1b[93m";
|
|
19
|
+
const M = "\x1b[95m", BL = "\x1b[94m", GR = "\x1b[92m", A = "\x1b[38;5;208m";
|
|
19
20
|
|
|
20
21
|
/* ─── Protocol markers ─── */
|
|
21
22
|
const READY = "===READY===", RESPONSE = "===RESPONSE===", DONE = "===DONE===";
|
|
@@ -30,9 +31,16 @@ const ART_W = ART[0].length;
|
|
|
30
31
|
|
|
31
32
|
function vis(t) { return t.replace(/\x1b\[[0-9;]*m/g, "").length; }
|
|
32
33
|
function fw() { return Math.min(Math.max((process.stdout.columns || 110) - 2, 82), 150); }
|
|
33
|
-
function
|
|
34
|
-
function
|
|
35
|
-
|
|
34
|
+
function plain(t) { return t.replace(/\x1b\[[0-9;]*m/g, ""); }
|
|
35
|
+
function topTitle(w, left, right = "") {
|
|
36
|
+
const l = ` ${C}${B}${left}${R} `;
|
|
37
|
+
const r = right ? ` ${G}${right}${R} ` : "";
|
|
38
|
+
const room = Math.max(0, w - vis(l) - vis(r));
|
|
39
|
+
return C + "╭" + R + l + C + "─".repeat(room) + R + r + C + "╮" + R;
|
|
40
|
+
}
|
|
41
|
+
function top(w) { return C + "╭" + "─".repeat(w) + "╮" + R; }
|
|
42
|
+
function bot(w) { return C + "╰" + "─".repeat(w) + "╯" + R; }
|
|
43
|
+
function sep(w) { return C + "├" + "─".repeat(w) + "┤" + R; }
|
|
36
44
|
function vpad(t, w) { return t + " ".repeat(Math.max(0, w - vis(t))); }
|
|
37
45
|
function clip(t, w) {
|
|
38
46
|
const plain = t.replace(/\x1b\[[0-9;]*m/g, "");
|
|
@@ -41,17 +49,16 @@ function clip(t, w) {
|
|
|
41
49
|
return plain.slice(0, w - 1) + "…";
|
|
42
50
|
}
|
|
43
51
|
function cell(t, w) { return vpad(clip(t, w), w); }
|
|
44
|
-
function row(left, right, lw, rw) { return "│ " + cell(left, lw) + " │ " + cell(right, rw) + " │"; }
|
|
45
|
-
function full(t, w) { return "│ " + cell(t, w - 2) + " │"; }
|
|
52
|
+
function row(left, right, lw, rw) { return C + "│ " + R + cell(left, lw) + C + " │ " + R + cell(right, rw) + C + " │" + R; }
|
|
53
|
+
function full(t, w) { return C + "│ " + R + cell(t, w - 2) + C + " │" + R; }
|
|
46
54
|
function empty(w) { return full("", w); }
|
|
47
55
|
|
|
48
56
|
function logoLines(width) {
|
|
49
|
-
if (width >= ART_W) return ART.map((line) => C + B + line + R);
|
|
57
|
+
if (width >= ART_W) return ART.map((line, i) => (i < 2 ? C : i < 4 ? BL : M) + B + line + R);
|
|
50
58
|
return [C + B + "COFOS" + R];
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
function render({ modelPath, cacheDir, pdfDir }) {
|
|
54
|
-
console.clear();
|
|
55
62
|
const w = fw();
|
|
56
63
|
const minSplit = 96;
|
|
57
64
|
const cwd = process.cwd();
|
|
@@ -60,25 +67,26 @@ function render({ modelPath, cacheDir, pdfDir }) {
|
|
|
60
67
|
...logoLines(Math.floor((w - 5) * 0.48)),
|
|
61
68
|
"",
|
|
62
69
|
G + "Scientific RAG CLI" + R,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
Y + "Model" + R + " " + W + modelPath + R,
|
|
71
|
+
Y + "Params" + R + " " + W + "9B" + R,
|
|
72
|
+
Y + "Path" + R + " " + W + cwd + R,
|
|
73
|
+
Y + "Cache" + R + " " + W + cacheDir + R,
|
|
67
74
|
];
|
|
68
75
|
const right = [
|
|
69
|
-
|
|
76
|
+
GR + B + "What COFOS Does" + R,
|
|
70
77
|
"COFOS answers scientific questions about covalent organic frameworks,",
|
|
71
78
|
"photocatalysis, reactive oxygen species, and oxygen-reduction products.",
|
|
72
79
|
rag,
|
|
73
80
|
"",
|
|
74
|
-
|
|
75
|
-
"/help show commands and PDF-RAG usage",
|
|
76
|
-
"/topk 5 change retrieval depth",
|
|
77
|
-
"/rag off answer without retrieval",
|
|
78
|
-
"cofos --pdf-dir ./new_pdfs add local papers",
|
|
81
|
+
A + B + "Suggested Commands" + R,
|
|
82
|
+
C + "/help" + R + " show commands and PDF-RAG usage",
|
|
83
|
+
C + "/topk 5" + R + " change retrieval depth",
|
|
84
|
+
C + "/rag off" + R + " answer without retrieval",
|
|
85
|
+
M + "cofos --pdf-dir ./new_pdfs" + R + " add local papers",
|
|
79
86
|
];
|
|
80
87
|
|
|
81
|
-
const
|
|
88
|
+
const titleLeft = `COFOS CLI v${pkg.version}`;
|
|
89
|
+
const lines = [topTitle(w, titleLeft)];
|
|
82
90
|
if (w >= minSplit) {
|
|
83
91
|
const inner = w - 5;
|
|
84
92
|
const lw = Math.max(46, Math.floor(inner * 0.48));
|
|
@@ -91,7 +99,7 @@ function render({ modelPath, cacheDir, pdfDir }) {
|
|
|
91
99
|
for (const item of right) lines.push(full(item, w));
|
|
92
100
|
}
|
|
93
101
|
lines.push(bot(w));
|
|
94
|
-
|
|
102
|
+
return lines.join("\n");
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
/* ─── Slash commands ─── */
|
|
@@ -133,12 +141,149 @@ function findPy() { return cmdExist("python3") ? "python3" : cmdExist("python")
|
|
|
133
141
|
// Only the modules the chat backend actually imports for the standalone merged-model path.
|
|
134
142
|
const REQUIRED_MODULES = "import transformers, torch, yaml, huggingface_hub, pdfplumber, rank_bm25";
|
|
135
143
|
|
|
136
|
-
function ensureDeps(py) {
|
|
137
|
-
|
|
144
|
+
function ensureDeps(py, log = console.log) {
|
|
145
|
+
log("[cofos] Checking Python dependencies …");
|
|
138
146
|
if (spawnSync(py, ["-c", `${REQUIRED_MODULES}; print('ok')`], { encoding: "utf-8", stdio: "pipe" }).status === 0) return;
|
|
139
|
-
|
|
140
|
-
const r = spawnSync(py, ["-m", "pip", "install", "-r", reqFile], { encoding: "utf-8", stdio: "
|
|
141
|
-
if (r.
|
|
147
|
+
log("[cofos] Installing Python dependencies (first run, may take a while) …");
|
|
148
|
+
const r = spawnSync(py, ["-m", "pip", "install", "-r", reqFile], { encoding: "utf-8", stdio: "pipe" });
|
|
149
|
+
if (r.stdout) log(r.stdout.trimEnd());
|
|
150
|
+
if (r.stderr) log(r.stderr.trimEnd());
|
|
151
|
+
if (r.status !== 0) { log("[cofos] Failed to install Python dependencies."); process.exit(1); }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ─── TUI ─── */
|
|
155
|
+
|
|
156
|
+
function createTui() {
|
|
157
|
+
const screen = blessed.screen({
|
|
158
|
+
smartCSR: true,
|
|
159
|
+
fullUnicode: true,
|
|
160
|
+
title: "COFOS",
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const history = blessed.box({
|
|
164
|
+
top: 0,
|
|
165
|
+
left: 0,
|
|
166
|
+
width: "100%",
|
|
167
|
+
height: "100%-3",
|
|
168
|
+
scrollable: true,
|
|
169
|
+
alwaysScroll: true,
|
|
170
|
+
keys: true,
|
|
171
|
+
vi: true,
|
|
172
|
+
mouse: true,
|
|
173
|
+
tags: false,
|
|
174
|
+
border: { type: "line" },
|
|
175
|
+
style: {
|
|
176
|
+
border: { fg: "cyan" },
|
|
177
|
+
scrollbar: { bg: "cyan" },
|
|
178
|
+
},
|
|
179
|
+
scrollbar: { ch: " ", track: { bg: "black" }, style: { bg: "cyan" } },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const suggestions = blessed.box({
|
|
183
|
+
bottom: 3,
|
|
184
|
+
left: 2,
|
|
185
|
+
width: "70%",
|
|
186
|
+
height: 6,
|
|
187
|
+
hidden: true,
|
|
188
|
+
border: { type: "line" },
|
|
189
|
+
tags: true,
|
|
190
|
+
style: { border: { fg: "yellow" }, fg: "white", bg: "black" },
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const input = blessed.textbox({
|
|
194
|
+
bottom: 0,
|
|
195
|
+
left: 0,
|
|
196
|
+
width: "100%",
|
|
197
|
+
height: 3,
|
|
198
|
+
inputOnFocus: true,
|
|
199
|
+
keys: true,
|
|
200
|
+
mouse: true,
|
|
201
|
+
border: { type: "line" },
|
|
202
|
+
label: " cofos ",
|
|
203
|
+
style: {
|
|
204
|
+
border: { fg: "cyan" },
|
|
205
|
+
focus: { border: { fg: "green" } },
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
screen.append(history);
|
|
210
|
+
screen.append(suggestions);
|
|
211
|
+
screen.append(input);
|
|
212
|
+
|
|
213
|
+
let buffer = "";
|
|
214
|
+
let onSubmit = () => {};
|
|
215
|
+
|
|
216
|
+
function append(text = "") {
|
|
217
|
+
buffer += String(text).replace(/\r/g, "");
|
|
218
|
+
if (buffer.length > 250000) buffer = buffer.slice(-220000);
|
|
219
|
+
history.setContent(buffer);
|
|
220
|
+
history.setScrollPerc(100);
|
|
221
|
+
screen.render();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function hideSuggestions() {
|
|
225
|
+
if (!suggestions.hidden) {
|
|
226
|
+
suggestions.hide();
|
|
227
|
+
screen.render();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function updateSuggestions() {
|
|
232
|
+
const value = input.getValue() || "";
|
|
233
|
+
if (!value.startsWith("/")) {
|
|
234
|
+
hideSuggestions();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const matches = slashMatches(value);
|
|
238
|
+
if (!matches.length) {
|
|
239
|
+
hideSuggestions();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
suggestions.setContent(matches.map(({ cmd, desc }) => `{cyan-fg}${cmd}{/cyan-fg} ${desc}`).join("\n"));
|
|
243
|
+
suggestions.height = Math.min(8, matches.length + 2);
|
|
244
|
+
suggestions.show();
|
|
245
|
+
screen.render();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
input.on("submit", (value) => {
|
|
249
|
+
hideSuggestions();
|
|
250
|
+
input.clearValue();
|
|
251
|
+
screen.render();
|
|
252
|
+
onSubmit(String(value || ""));
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
input.on("keypress", () => setTimeout(updateSuggestions, 0));
|
|
256
|
+
|
|
257
|
+
screen.key(["tab"], () => {
|
|
258
|
+
const value = input.getValue() || "";
|
|
259
|
+
if (!value.startsWith("/")) return;
|
|
260
|
+
const matches = slashMatches(value);
|
|
261
|
+
if (!matches.length) return;
|
|
262
|
+
if (matches.length === 1) {
|
|
263
|
+
input.setValue(matches[0].cmd);
|
|
264
|
+
hideSuggestions();
|
|
265
|
+
input.focus();
|
|
266
|
+
screen.render();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
updateSuggestions();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
screen.key(["escape"], hideSuggestions);
|
|
273
|
+
screen.key(["C-c"], () => process.emit("SIGINT"));
|
|
274
|
+
|
|
275
|
+
input.focus();
|
|
276
|
+
screen.render();
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
screen,
|
|
280
|
+
input,
|
|
281
|
+
append,
|
|
282
|
+
print: (text = "") => append(String(text) + "\n"),
|
|
283
|
+
setSubmitHandler: (fn) => { onSubmit = fn; },
|
|
284
|
+
setPrompt: (label) => { input.setLabel(` ${label} `); screen.render(); },
|
|
285
|
+
destroy: () => screen.destroy(),
|
|
286
|
+
};
|
|
142
287
|
}
|
|
143
288
|
|
|
144
289
|
/* ─── main ─── */
|
|
@@ -209,17 +354,17 @@ function main() {
|
|
|
209
354
|
if (pdfDir) backendArgs.push("--pdf-dir", pdfDir);
|
|
210
355
|
if (rebuildPdfIndex) backendArgs.push("--rebuild-pdf-index");
|
|
211
356
|
|
|
212
|
-
render({ modelPath, cacheDir, pdfDir });
|
|
213
|
-
|
|
214
357
|
const py = findPy();
|
|
215
358
|
if (!py) die("Python not found. Install Python 3.9+ and re-run.");
|
|
216
359
|
if (!existsSync(chatScript)) die(`Missing script: ${chatScript}`);
|
|
217
|
-
ensureDeps(py);
|
|
218
360
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
361
|
+
const ui = createTui();
|
|
362
|
+
ui.print(plain(render({ modelPath, cacheDir, pdfDir })));
|
|
363
|
+
ensureDeps(py, (msg) => ui.print(msg));
|
|
364
|
+
ui.print(" Loading COFOS 9B + RAG backend from Hugging Face … first run may take a while");
|
|
365
|
+
if (pdfDir) ui.print(" Local PDF RAG: enabled for this session");
|
|
366
|
+
else ui.print(" Tip: add local papers with `cofos --pdf-dir ./new_pdfs`");
|
|
367
|
+
ui.print("");
|
|
223
368
|
|
|
224
369
|
const backendEnv = {
|
|
225
370
|
...process.env,
|
|
@@ -237,50 +382,78 @@ function main() {
|
|
|
237
382
|
env: backendEnv,
|
|
238
383
|
});
|
|
239
384
|
|
|
240
|
-
// `encoding` is not a spawn() option — without setEncoding each Buffer chunk
|
|
241
|
-
// is decoded on its own, so a multi-byte character (·OH, ¹O₂, any Chinese)
|
|
242
|
-
// straddling a chunk boundary comes out as mojibake.
|
|
243
385
|
proc.stdout.setEncoding("utf-8");
|
|
244
386
|
proc.stderr.setEncoding("utf-8");
|
|
245
387
|
|
|
246
388
|
let buf = "", ready = false, generating = false, responseOpen = false, closed = false;
|
|
247
389
|
const inputQ = [];
|
|
248
|
-
|
|
249
|
-
const contPrompt = G + " ... " + R;
|
|
250
|
-
const rl = createInterface({
|
|
251
|
-
input: process.stdin,
|
|
252
|
-
output: process.stdout,
|
|
253
|
-
terminal: true,
|
|
254
|
-
prompt: mainPrompt,
|
|
255
|
-
completer: slashCompletions,
|
|
256
|
-
});
|
|
257
|
-
rl.on("line", (l) => {
|
|
258
|
-
if (!ready || generating) inputQ.push(l);
|
|
259
|
-
else if (!handle(l)) ask();
|
|
260
|
-
});
|
|
390
|
+
let mlBuf = [];
|
|
261
391
|
|
|
262
|
-
function
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
392
|
+
function shutdown() {
|
|
393
|
+
if (closed) return;
|
|
394
|
+
closed = true;
|
|
395
|
+
try { proc.stdin.end(); } catch (_) {}
|
|
396
|
+
try { proc.kill("SIGTERM"); } catch (_) {}
|
|
397
|
+
ui.destroy();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
process.on("SIGTERM", shutdown);
|
|
401
|
+
process.on("SIGINT", shutdown);
|
|
402
|
+
|
|
403
|
+
function drain() {
|
|
404
|
+
while (inputQ.length) {
|
|
405
|
+
const line = inputQ.shift();
|
|
406
|
+
if (handle(line)) return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function handle(rawInput) {
|
|
411
|
+
let input = String(rawInput || "");
|
|
412
|
+
if (input.endsWith("\\")) {
|
|
413
|
+
mlBuf.push(input.slice(0, -1));
|
|
414
|
+
ui.setPrompt("...");
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
if (mlBuf.length > 0) {
|
|
418
|
+
mlBuf.push(input);
|
|
419
|
+
input = mlBuf.join("\n");
|
|
420
|
+
mlBuf = [];
|
|
421
|
+
ui.setPrompt("cofos");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
input = input.trim();
|
|
425
|
+
if (!input) return false;
|
|
426
|
+
|
|
427
|
+
if (/^\/(exit|quit)$/.test(input)) { proc.stdin.write("/exit\n"); proc.stdin.end(); return true; }
|
|
428
|
+
if (/^\/(help)$/.test(input)) {
|
|
429
|
+
ui.print(slashHint("/") + "\n Add local papers to RAG:\n cofos --pdf-dir ./new_pdfs\n cofos --pdf-dir ./new_pdfs --rebuild-pdf-index\n\n Multi-line: end a line with \\ to continue.");
|
|
430
|
+
return false;
|
|
267
431
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
432
|
+
if (/^\/(clear)$/.test(input)) {
|
|
433
|
+
mlBuf = [];
|
|
434
|
+
responseOpen = false;
|
|
435
|
+
proc.stdin.write(input + "\n");
|
|
436
|
+
generating = true;
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
if (/^\/(info)$/.test(input)) {
|
|
440
|
+
ui.print(`\n COFOS CLI — v${pkg.version}\n Model: ${modelPath} (Hugging Face)\n RAG: Willlzh/COFOS_data/runtime (KG + BM25) + optional --pdf-dir BM25 evidence\n Cache: ${cacheDir}\n HF: ${backendEnv.HF_HOME}\n Engine: StudentAdapterInference + transformers + PyTorch\n Hist: ~/.cofos_history.jsonl\n`);
|
|
441
|
+
return false;
|
|
272
442
|
}
|
|
273
|
-
|
|
274
|
-
|
|
443
|
+
|
|
444
|
+
ui.print(G + " you " + R + input);
|
|
445
|
+
responseOpen = false;
|
|
446
|
+
generating = true;
|
|
447
|
+
proc.stdin.write(input + "\n");
|
|
448
|
+
return true;
|
|
275
449
|
}
|
|
276
450
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
451
|
+
ui.setSubmitHandler((line) => {
|
|
452
|
+
if (!ready || generating) inputQ.push(line);
|
|
453
|
+
else handle(line);
|
|
454
|
+
});
|
|
281
455
|
|
|
282
|
-
|
|
283
|
-
proc.stderr.on("data", (chunk) => { preserveInputWrite(process.stderr, chunk); });
|
|
456
|
+
proc.stderr.on("data", (chunk) => ui.append(chunk));
|
|
284
457
|
|
|
285
458
|
proc.stdout.on("data", (chunk) => {
|
|
286
459
|
buf += chunk;
|
|
@@ -294,14 +467,11 @@ function main() {
|
|
|
294
467
|
}
|
|
295
468
|
return;
|
|
296
469
|
}
|
|
297
|
-
// Anything arriving while no answer is in flight is not part of a response;
|
|
298
|
-
// dropping it keeps it from being prepended to the next one.
|
|
299
470
|
if (!generating) { buf = ""; return; }
|
|
300
471
|
|
|
301
472
|
if (!responseOpen) {
|
|
302
473
|
const ri = buf.indexOf(RESPONSE);
|
|
303
474
|
if (ri === -1) {
|
|
304
|
-
// Keep enough tail to catch a RESPONSE marker split across chunks.
|
|
305
475
|
const keep = RESPONSE.length - 1;
|
|
306
476
|
if (buf.length > keep) buf = buf.slice(-keep);
|
|
307
477
|
return;
|
|
@@ -311,116 +481,34 @@ function main() {
|
|
|
311
481
|
responseOpen = true;
|
|
312
482
|
}
|
|
313
483
|
|
|
314
|
-
// Scan for DONE marker — stream everything before it.
|
|
315
484
|
const di = buf.indexOf(DONE);
|
|
316
485
|
if (di !== -1) {
|
|
317
486
|
const pre = buf.slice(0, di);
|
|
318
|
-
if (pre)
|
|
487
|
+
if (pre) ui.append(pre);
|
|
319
488
|
buf = buf.slice(di + DONE.length);
|
|
320
489
|
if (buf[0] === "\n") buf = buf.slice(1);
|
|
321
490
|
generating = false;
|
|
322
491
|
responseOpen = false;
|
|
323
|
-
|
|
492
|
+
ui.append("\n");
|
|
324
493
|
drain();
|
|
325
494
|
} else {
|
|
326
|
-
// Stream buffered content, keeping enough tail to catch a partial DONE marker.
|
|
327
495
|
const keep = DONE.length - 1;
|
|
328
496
|
if (buf.length > keep) {
|
|
329
497
|
const out = buf.slice(0, -keep);
|
|
330
|
-
if (out)
|
|
498
|
+
if (out) ui.append(out);
|
|
331
499
|
buf = buf.slice(-keep);
|
|
332
500
|
}
|
|
333
501
|
}
|
|
334
502
|
});
|
|
335
503
|
|
|
336
|
-
proc.on("error", (e) => {
|
|
504
|
+
proc.on("error", (e) => { ui.print("[cofos] Python error: " + e.message); shutdown(); });
|
|
337
505
|
proc.on("exit", (c) => {
|
|
338
506
|
closed = true;
|
|
339
|
-
if (c && c !== 0)
|
|
340
|
-
|
|
341
|
-
// Let queued stdout writes flush before tearing the process down.
|
|
507
|
+
if (c && c !== 0) ui.print("[cofos] Python exited with code " + c);
|
|
508
|
+
ui.destroy();
|
|
342
509
|
process.exitCode = c ?? 0;
|
|
343
510
|
});
|
|
344
511
|
|
|
345
|
-
function shutdown() {
|
|
346
|
-
if (closed) return;
|
|
347
|
-
closed = true;
|
|
348
|
-
try { proc.stdin.end(); } catch (_) {}
|
|
349
|
-
try { rl.close(); } catch (_) {}
|
|
350
|
-
proc.kill("SIGTERM");
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Ctrl+C and Ctrl+D should stop the backend instead of orphaning it.
|
|
354
|
-
rl.on("SIGINT", () => { console.log("\n[cofos] Interrupted."); shutdown(); });
|
|
355
|
-
rl.on("close", () => { if (!closed) shutdown(); });
|
|
356
|
-
process.on("SIGTERM", shutdown);
|
|
357
|
-
|
|
358
|
-
function drain() {
|
|
359
|
-
while (inputQ.length) {
|
|
360
|
-
const line = inputQ.shift();
|
|
361
|
-
if (handle(line)) return;
|
|
362
|
-
}
|
|
363
|
-
ask();
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function ask(prompt = mainPrompt) {
|
|
367
|
-
if (closed) return;
|
|
368
|
-
try {
|
|
369
|
-
rl.setPrompt(prompt);
|
|
370
|
-
rl.prompt();
|
|
371
|
-
} catch (_) {}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/* ─── multi-line accumulator ─── */
|
|
375
|
-
let mlBuf = [];
|
|
376
|
-
|
|
377
|
-
function handle(input) {
|
|
378
|
-
// Multi-line continuation: lines ending with "\"
|
|
379
|
-
if (input.endsWith("\\")) {
|
|
380
|
-
mlBuf.push(input.slice(0, -1));
|
|
381
|
-
ask(contPrompt);
|
|
382
|
-
return true;
|
|
383
|
-
}
|
|
384
|
-
if (mlBuf.length > 0) {
|
|
385
|
-
mlBuf.push(input);
|
|
386
|
-
input = mlBuf.join("\n");
|
|
387
|
-
mlBuf = [];
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
input = input.trim();
|
|
391
|
-
if (!input) return false;
|
|
392
|
-
|
|
393
|
-
// Local-only commands (don't hit the backend).
|
|
394
|
-
if (/^\/(exit|quit)$/.test(input)) { proc.stdin.write("/exit\n"); proc.stdin.end(); return true; }
|
|
395
|
-
if (/^\/(help)$/.test(input)) {
|
|
396
|
-
console.log(slashHint("/") + "\n Add local papers to RAG:\n cofos --pdf-dir ./new_pdfs\n cofos --pdf-dir ./new_pdfs --rebuild-pdf-index\n\n Multi-line: end a line with \\ to continue.\n");
|
|
397
|
-
return false;
|
|
398
|
-
}
|
|
399
|
-
if (/^\/(clear)$/.test(input)) {
|
|
400
|
-
mlBuf = [];
|
|
401
|
-
responseOpen = false;
|
|
402
|
-
proc.stdin.write(input + "\n");
|
|
403
|
-
generating = true;
|
|
404
|
-
return true;
|
|
405
|
-
}
|
|
406
|
-
if (/^\/(info)$/.test(input)) {
|
|
407
|
-
console.log(`\n COFOS CLI — v${pkg.version}`);
|
|
408
|
-
console.log(" Model: Willlzh/COFOS (Hugging Face)");
|
|
409
|
-
console.log(" RAG: Willlzh/COFOS_data/runtime (KG + BM25) + optional --pdf-dir BM25 evidence");
|
|
410
|
-
console.log(` Cache: ${cacheDir}`);
|
|
411
|
-
console.log(` HF: ${backendEnv.HF_HOME}`);
|
|
412
|
-
console.log(" Engine: StudentAdapterInference + transformers + PyTorch");
|
|
413
|
-
console.log(" Hist: ~/.cofos_history.jsonl\n");
|
|
414
|
-
return false;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Forward to backend. Echo a stable user turn so pasted/submitted questions remain visible in the transcript.
|
|
418
|
-
console.log(G + " you " + R + input);
|
|
419
|
-
responseOpen = false;
|
|
420
|
-
generating = true;
|
|
421
|
-
proc.stdin.write(input + "\n");
|
|
422
|
-
return true;
|
|
423
|
-
}
|
|
424
512
|
}
|
|
425
513
|
|
|
426
514
|
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lzhzzzzwill/cofos",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "COFOS 9B + RAG chat CLI with optional local PDF ingestion.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,5 +38,8 @@
|
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"blessed": "^0.1.81"
|
|
41
44
|
}
|
|
42
45
|
}
|