@lzhzzzzwill/cofos 1.3.3 → 1.3.4
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 +1 -7
- package/bin/cofos.js +13 -267
- package/package.json +1 -4
- package/runtime/src/inference/student_adapter_inference.py +17 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ COFOS CLI is a standalone Node.js command line wrapper around the COFOS Python r
|
|
|
6
6
|
npx @lzhzzzzwill/cofos
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
COFOS uses a plain terminal interface, so normal terminal scrollback, mouse selection, copy, and `Ctrl+C` work as expected.
|
|
10
10
|
|
|
11
11
|
## Runtime Model
|
|
12
12
|
|
|
@@ -77,11 +77,6 @@ Ask once and exit, useful for scripts and smoke tests:
|
|
|
77
77
|
npx @lzhzzzzwill/cofos -p "What ROS does TT-T-COF generate under visible-light photocatalysis?"
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
Use the experimental fixed-input TUI:
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
npx @lzhzzzzwill/cofos --tui
|
|
84
|
-
```
|
|
85
80
|
|
|
86
81
|
## Startup Behavior
|
|
87
82
|
|
|
@@ -133,7 +128,6 @@ cofos \
|
|
|
133
128
|
| `--rebuild-pdf-index` | Force PDF parsing and BM25 rebuild |
|
|
134
129
|
| `--top-k` | Retrieval top-k |
|
|
135
130
|
| `-p`, `--prompt` | Ask one question and exit |
|
|
136
|
-
| `--tui` | Use the experimental fixed-input full-screen TUI |
|
|
137
131
|
| `--max-new-tokens` | Override generation length |
|
|
138
132
|
| `--no-rag` | Run without KG/BM25 retrieval |
|
|
139
133
|
|
package/bin/cofos.js
CHANGED
|
@@ -158,208 +158,7 @@ function ensureDeps(py, log = console.log) {
|
|
|
158
158
|
if (r.status !== 0) { log("[cofos] Failed to install Python dependencies."); process.exit(1); }
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
async function createTui() {
|
|
164
|
-
const { default: blessed } = await import("blessed");
|
|
165
|
-
const screen = blessed.screen({
|
|
166
|
-
smartCSR: true,
|
|
167
|
-
fullUnicode: true,
|
|
168
|
-
title: "COFOS",
|
|
169
|
-
mouse: false,
|
|
170
|
-
});
|
|
171
|
-
screen.program.disableMouse();
|
|
172
|
-
|
|
173
|
-
const history = blessed.box({
|
|
174
|
-
top: 0,
|
|
175
|
-
left: 0,
|
|
176
|
-
width: "100%",
|
|
177
|
-
height: "100%-3",
|
|
178
|
-
scrollable: true,
|
|
179
|
-
alwaysScroll: true,
|
|
180
|
-
keys: true,
|
|
181
|
-
vi: true,
|
|
182
|
-
mouse: false,
|
|
183
|
-
tags: true,
|
|
184
|
-
scrollbar: { ch: " ", track: { bg: "black" }, style: { bg: "cyan" } },
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const suggestions = blessed.box({
|
|
188
|
-
bottom: 3,
|
|
189
|
-
left: 2,
|
|
190
|
-
width: "70%",
|
|
191
|
-
height: 6,
|
|
192
|
-
hidden: true,
|
|
193
|
-
border: { type: "line" },
|
|
194
|
-
tags: true,
|
|
195
|
-
style: { border: { fg: "yellow" }, fg: "white", bg: "black" },
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
const input = blessed.textbox({
|
|
199
|
-
bottom: 0,
|
|
200
|
-
left: 0,
|
|
201
|
-
width: "100%",
|
|
202
|
-
height: 3,
|
|
203
|
-
inputOnFocus: true,
|
|
204
|
-
keys: true,
|
|
205
|
-
mouse: false,
|
|
206
|
-
border: { type: "line" },
|
|
207
|
-
label: " cofos ",
|
|
208
|
-
style: {
|
|
209
|
-
border: { fg: "cyan" },
|
|
210
|
-
focus: { border: { fg: "green" } },
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
screen.append(history);
|
|
215
|
-
screen.append(suggestions);
|
|
216
|
-
screen.append(input);
|
|
217
|
-
|
|
218
|
-
let buffer = "";
|
|
219
|
-
let onSubmit = () => {};
|
|
220
|
-
let onExit = () => {};
|
|
221
|
-
let submitting = false;
|
|
222
|
-
let suggestionMatches = [];
|
|
223
|
-
let selectedSuggestion = 0;
|
|
224
|
-
|
|
225
|
-
function renderBuffer(wasAtBottom = true) {
|
|
226
|
-
history.setContent(buffer);
|
|
227
|
-
if (wasAtBottom) history.setScrollPerc(100);
|
|
228
|
-
screen.render();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function append(text = "") {
|
|
232
|
-
const wasAtBottom = history.getScrollPerc() >= 98;
|
|
233
|
-
const data = String(text).replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
|
|
234
|
-
for (const part of data.split("\r")) {
|
|
235
|
-
if (part === "") continue;
|
|
236
|
-
if (data.includes("\r")) {
|
|
237
|
-
const lastNewline = buffer.lastIndexOf("\n");
|
|
238
|
-
buffer = (lastNewline === -1 ? "" : buffer.slice(0, lastNewline + 1)) + part;
|
|
239
|
-
} else {
|
|
240
|
-
buffer += part;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (buffer.length > 250000) buffer = buffer.slice(-220000);
|
|
244
|
-
renderBuffer(wasAtBottom);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function hideSuggestions() {
|
|
248
|
-
suggestionMatches = [];
|
|
249
|
-
selectedSuggestion = 0;
|
|
250
|
-
if (!suggestions.hidden) {
|
|
251
|
-
suggestions.hide();
|
|
252
|
-
screen.render();
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function renderSuggestions() {
|
|
257
|
-
if (!suggestionMatches.length) {
|
|
258
|
-
hideSuggestions();
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
selectedSuggestion = Math.max(0, Math.min(selectedSuggestion, suggestionMatches.length - 1));
|
|
262
|
-
suggestions.setContent(suggestionMatches.map(({ cmd, desc }, i) => {
|
|
263
|
-
const marker = i === selectedSuggestion ? "{black-fg}{yellow-bg}›" : " ";
|
|
264
|
-
const close = i === selectedSuggestion ? "{/yellow-bg}{/black-fg}" : "";
|
|
265
|
-
return `${marker} {cyan-fg}${cmd}{/cyan-fg} ${desc}${close}`;
|
|
266
|
-
}).join("\n"));
|
|
267
|
-
suggestions.height = Math.min(8, suggestionMatches.length + 2);
|
|
268
|
-
suggestions.show();
|
|
269
|
-
screen.render();
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function updateSuggestions() {
|
|
273
|
-
const value = input.getValue() || "";
|
|
274
|
-
if (!value.startsWith("/")) {
|
|
275
|
-
hideSuggestions();
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
suggestionMatches = slashMatches(value);
|
|
279
|
-
selectedSuggestion = 0;
|
|
280
|
-
renderSuggestions();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function selectedCommand() {
|
|
284
|
-
return suggestionMatches[selectedSuggestion]?.cmd || "";
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function submitInput() {
|
|
288
|
-
if (submitting) return;
|
|
289
|
-
submitting = true;
|
|
290
|
-
let value = input.getValue();
|
|
291
|
-
if (!suggestions.hidden && selectedCommand()) value = selectedCommand();
|
|
292
|
-
hideSuggestions();
|
|
293
|
-
input.clearValue();
|
|
294
|
-
input.focus();
|
|
295
|
-
screen.render();
|
|
296
|
-
onSubmit(String(value || ""));
|
|
297
|
-
setTimeout(() => { submitting = false; }, 0);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
input.key(["enter"], submitInput);
|
|
301
|
-
screen.key(["enter"], submitInput);
|
|
302
|
-
|
|
303
|
-
input.on("keypress", (_ch, key = {}) => {
|
|
304
|
-
if (["up", "down", "tab", "enter", "escape"].includes(key.name)) return;
|
|
305
|
-
setTimeout(updateSuggestions, 0);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
screen.key(["tab"], () => {
|
|
309
|
-
const value = input.getValue() || "";
|
|
310
|
-
if (!value.startsWith("/")) return;
|
|
311
|
-
if (!suggestionMatches.length) suggestionMatches = slashMatches(value);
|
|
312
|
-
if (!suggestionMatches.length) return;
|
|
313
|
-
input.setValue(selectedCommand());
|
|
314
|
-
hideSuggestions();
|
|
315
|
-
input.focus();
|
|
316
|
-
screen.render();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
screen.key(["up"], () => {
|
|
320
|
-
if (suggestions.hidden || !suggestionMatches.length) return;
|
|
321
|
-
selectedSuggestion = (selectedSuggestion - 1 + suggestionMatches.length) % suggestionMatches.length;
|
|
322
|
-
renderSuggestions();
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
screen.key(["down"], () => {
|
|
326
|
-
if (suggestions.hidden || !suggestionMatches.length) return;
|
|
327
|
-
selectedSuggestion = (selectedSuggestion + 1) % suggestionMatches.length;
|
|
328
|
-
renderSuggestions();
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
screen.key(["escape"], hideSuggestions);
|
|
332
|
-
screen.key(["pageup"], () => { history.scroll(-Math.max(3, Math.floor(screen.height * 0.8))); screen.render(); });
|
|
333
|
-
screen.key(["pagedown"], () => { history.scroll(Math.max(3, Math.floor(screen.height * 0.8))); screen.render(); });
|
|
334
|
-
screen.key(["home"], () => { history.setScrollPerc(0); screen.render(); });
|
|
335
|
-
screen.key(["end"], () => { history.setScrollPerc(100); screen.render(); });
|
|
336
|
-
screen.key(["C-c"], () => onExit());
|
|
337
|
-
|
|
338
|
-
input.focus();
|
|
339
|
-
screen.render();
|
|
340
|
-
|
|
341
|
-
return {
|
|
342
|
-
supportsReplace: true,
|
|
343
|
-
echoUser: true,
|
|
344
|
-
screen,
|
|
345
|
-
input,
|
|
346
|
-
append,
|
|
347
|
-
print: (text = "") => append(String(text) + "\n"),
|
|
348
|
-
clear: () => { buffer = ""; renderBuffer(true); },
|
|
349
|
-
length: () => buffer.length,
|
|
350
|
-
replaceFrom: (index, text = "") => { buffer = buffer.slice(0, Math.max(0, index)) + String(text); renderBuffer(true); },
|
|
351
|
-
setSubmitHandler: (fn) => { onSubmit = fn; },
|
|
352
|
-
setExitHandler: (fn) => { onExit = fn; },
|
|
353
|
-
focus: () => { input.focus(); screen.render(); },
|
|
354
|
-
setPrompt: (label) => { input.setLabel(` ${label} `); screen.render(); },
|
|
355
|
-
destroy: () => {
|
|
356
|
-
try { screen.program.disableMouse(); } catch (_) {}
|
|
357
|
-
screen.destroy();
|
|
358
|
-
},
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function blessedToAnsi(text) {
|
|
161
|
+
function markupToAnsi(text) {
|
|
363
162
|
return String(text)
|
|
364
163
|
.replace(/\{bold\}/g, B)
|
|
365
164
|
.replace(/\{\/bold\}/g, R)
|
|
@@ -399,11 +198,11 @@ function createPlainUi() {
|
|
|
399
198
|
return {
|
|
400
199
|
supportsReplace: false,
|
|
401
200
|
echoUser: false,
|
|
402
|
-
append: (text = "") => process.stdout.write(
|
|
403
|
-
print: (text = "") => console.log(
|
|
201
|
+
append: (text = "") => process.stdout.write(markupToAnsi(text)),
|
|
202
|
+
print: (text = "") => console.log(markupToAnsi(text)),
|
|
404
203
|
clear: () => console.clear(),
|
|
405
204
|
length: () => 0,
|
|
406
|
-
replaceFrom: (_index, text = "") => process.stdout.write(
|
|
205
|
+
replaceFrom: (_index, text = "") => process.stdout.write(markupToAnsi(text)),
|
|
407
206
|
setSubmitHandler: (fn) => { onSubmit = fn; },
|
|
408
207
|
setExitHandler: (fn) => { onExit = fn; },
|
|
409
208
|
focus: () => rl.prompt(),
|
|
@@ -417,7 +216,7 @@ function createPlainUi() {
|
|
|
417
216
|
async function main() {
|
|
418
217
|
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
419
218
|
console.log("\n COFOS CLI — 9B + RAG chat (Willlzh/COFOS)\n");
|
|
420
|
-
console.log(" Usage: cofos [
|
|
219
|
+
console.log(" Usage: cofos [-p QUESTION] [--model PATH] [--config PATH] [--pdf-dir DIR] [--cache-dir DIR] [--top-k N] [--no-rag]\n");
|
|
421
220
|
console.log(" Commands in chat:");
|
|
422
221
|
console.log(" /help Show this help");
|
|
423
222
|
console.log(" /exit Quit");
|
|
@@ -430,7 +229,6 @@ async function main() {
|
|
|
430
229
|
console.log(" --pdf-dir DIR Add PDFs from DIR as extra BM25 evidence");
|
|
431
230
|
console.log(" --cache-dir DIR Store model/data cache here (default: ./.cofos)");
|
|
432
231
|
console.log(" --max-new-tokens N Override generation length");
|
|
433
|
-
console.log(" --tui Use the experimental fixed-input TUI");
|
|
434
232
|
console.log(" -p, --prompt TEXT Ask once and exit");
|
|
435
233
|
console.log(" --rebuild-pdf-index Force reparsing PDFs and rebuilding BM25\n");
|
|
436
234
|
console.log(" Multi-line: end a line with \\ to continue on the next line.\n");
|
|
@@ -447,7 +245,6 @@ async function main() {
|
|
|
447
245
|
let maxNewTokens = null;
|
|
448
246
|
let cacheDir = process.env.COFOS_CACHE_DIR ? resolve(process.env.COFOS_CACHE_DIR) : resolve(process.cwd(), ".cofos");
|
|
449
247
|
let rebuildPdfIndex = false;
|
|
450
|
-
let useTui = false;
|
|
451
248
|
let singlePrompt = null;
|
|
452
249
|
const backendArgs = [chatScript];
|
|
453
250
|
|
|
@@ -479,8 +276,6 @@ async function main() {
|
|
|
479
276
|
cacheDir = resolve(value(++i, arg));
|
|
480
277
|
} else if (arg === "--rebuild-pdf-index") {
|
|
481
278
|
rebuildPdfIndex = true;
|
|
482
|
-
} else if (arg === "--tui") {
|
|
483
|
-
useTui = true;
|
|
484
279
|
} else if (arg === "-p" || arg === "--prompt") {
|
|
485
280
|
singlePrompt = value(++i, arg);
|
|
486
281
|
} else if (arg === "--no-rag") {
|
|
@@ -498,8 +293,8 @@ async function main() {
|
|
|
498
293
|
if (!py) die("Python not found. Install Python 3.9+ and re-run.");
|
|
499
294
|
if (!existsSync(chatScript)) die(`Missing script: ${chatScript}`);
|
|
500
295
|
|
|
501
|
-
const ui =
|
|
502
|
-
ui.print(
|
|
296
|
+
const ui = createPlainUi();
|
|
297
|
+
ui.print(render({ modelPath, cacheDir, pdfDir }));
|
|
503
298
|
ensureDeps(py, (msg) => ui.print(msg));
|
|
504
299
|
ui.print(" Loading COFOS 9B + RAG backend from Hugging Face … first run may take a while");
|
|
505
300
|
if (pdfDir) ui.print(" Local PDF RAG: enabled for this session");
|
|
@@ -569,7 +364,7 @@ async function main() {
|
|
|
569
364
|
if (!input) return false;
|
|
570
365
|
|
|
571
366
|
if (input === "/") {
|
|
572
|
-
ui.print("
|
|
367
|
+
ui.print("Commands: " + SLASH_COMMANDS.map(({ cmd }) => cmd).join(" ") + "\nType a prefix such as /r or /to, then press Tab for completion.");
|
|
573
368
|
return false;
|
|
574
369
|
}
|
|
575
370
|
if (/^\/(exit|quit)$/.test(input)) { shutdown(); return true; }
|
|
@@ -681,65 +476,16 @@ async function main() {
|
|
|
681
476
|
|
|
682
477
|
function streamPlainAssistant(text, final = false) {
|
|
683
478
|
if (ui.supportsReplace) return;
|
|
684
|
-
plainAssistantPending += text;
|
|
685
479
|
if (plainAssistantMode === "undecided") {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
plainAssistantMode = "reason";
|
|
690
|
-
plainReasonBuffer = start.replace(/^<think>\s*/i, "");
|
|
691
|
-
plainReasonPrinted = 0;
|
|
692
|
-
plainAssistantPending = "";
|
|
693
|
-
ui.append(`${G}-${R} ${G}`);
|
|
694
|
-
} else if (final || start.length >= 12 || /\s/.test(start)) {
|
|
695
|
-
plainAssistantMode = "answer";
|
|
696
|
-
plainAssistantPending = start;
|
|
697
|
-
ui.append(`${GR}${B}◆${R} `);
|
|
698
|
-
} else {
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
if (plainAssistantMode === "reason") {
|
|
704
|
-
plainReasonBuffer += plainAssistantPending;
|
|
705
|
-
plainAssistantPending = "";
|
|
706
|
-
const thinkEnd = plainReasonBuffer.indexOf("</think>");
|
|
707
|
-
const paraEnd = plainReasonBuffer.search(/\n\s*\n/);
|
|
708
|
-
const split = thinkEnd !== -1 ? thinkEnd : paraEnd;
|
|
709
|
-
if (split !== -1) {
|
|
710
|
-
const reason = plainReasonBuffer.slice(plainReasonPrinted, split).replace(/<\/think>/gi, "");
|
|
711
|
-
const restStart = thinkEnd !== -1 ? split + "</think>".length : split;
|
|
712
|
-
const rest = plainReasonBuffer.slice(restStart).trimStart();
|
|
713
|
-
if (reason) ui.append(reason);
|
|
714
|
-
ui.append(`${R}\n${GR}${B}◆${R} `);
|
|
715
|
-
plainAssistantMode = "answer";
|
|
716
|
-
plainAssistantPending = rest;
|
|
717
|
-
plainReasonBuffer = "";
|
|
718
|
-
plainReasonPrinted = 0;
|
|
719
|
-
} else {
|
|
720
|
-
const printableUntil = final ? plainReasonBuffer.length : Math.max(0, plainReasonBuffer.length - 2);
|
|
721
|
-
if (printableUntil > plainReasonPrinted) {
|
|
722
|
-
ui.append(plainReasonBuffer.slice(plainReasonPrinted, printableUntil));
|
|
723
|
-
plainReasonPrinted = printableUntil;
|
|
724
|
-
}
|
|
725
|
-
if (!final) return;
|
|
726
|
-
if (plainReasonPrinted < plainReasonBuffer.length) ui.append(plainReasonBuffer.slice(plainReasonPrinted));
|
|
727
|
-
plainReasonBuffer = "";
|
|
728
|
-
plainReasonPrinted = 0;
|
|
729
|
-
ui.append(R);
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
if (plainAssistantMode === "answer") {
|
|
735
|
-
if (plainAssistantPending) {
|
|
736
|
-
ui.append(plainAssistantPending);
|
|
737
|
-
plainAssistantPending = "";
|
|
738
|
-
}
|
|
480
|
+
if (!text && !final) return;
|
|
481
|
+
plainAssistantMode = "answer";
|
|
482
|
+
ui.append(`${GR}${B}◆${R} `);
|
|
739
483
|
}
|
|
484
|
+
if (text) ui.append(text);
|
|
740
485
|
}
|
|
741
486
|
|
|
742
487
|
|
|
488
|
+
|
|
743
489
|
proc.stdout.on("data", (chunk) => {
|
|
744
490
|
buf += chunk;
|
|
745
491
|
if (!ready) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lzhzzzzwill/cofos",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "COFOS 9B + RAG chat CLI with optional local PDF ingestion.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,8 +38,5 @@
|
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=18"
|
|
41
|
-
},
|
|
42
|
-
"dependencies": {
|
|
43
|
-
"blessed": "^0.1.81"
|
|
44
41
|
}
|
|
45
42
|
}
|
|
@@ -538,7 +538,23 @@ class StudentAdapterInference:
|
|
|
538
538
|
)
|
|
539
539
|
if not stripped.strip():
|
|
540
540
|
continue
|
|
541
|
-
|
|
541
|
+
stripped = stripped.lstrip()
|
|
542
|
+
if awaiting_initial_answer and re.match(
|
|
543
|
+
r"(?is)^(?:the user|okay[,,]?\s+the user|用户问|用户用|i need|i should|i'll|let me|since the user|we need)\b",
|
|
544
|
+
stripped,
|
|
545
|
+
):
|
|
546
|
+
paragraph_end = re.search(r"\n\s*\n", stripped)
|
|
547
|
+
if paragraph_end is None:
|
|
548
|
+
# Keep waiting for the end of the leading reasoning paragraph.
|
|
549
|
+
buffer = stripped[-4000:]
|
|
550
|
+
continue
|
|
551
|
+
buffer = stripped[paragraph_end.end():].lstrip()
|
|
552
|
+
awaiting_initial_answer = False
|
|
553
|
+
if not buffer.strip():
|
|
554
|
+
continue
|
|
555
|
+
else:
|
|
556
|
+
buffer = stripped
|
|
557
|
+
awaiting_initial_answer = False
|
|
542
558
|
started = True
|
|
543
559
|
|
|
544
560
|
if len(buffer) > len("<think>"):
|