@lzhzzzzwill/cofos 1.1.9 → 1.1.13
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/bin/cofos.js +102 -32
- package/package.json +1 -1
- package/runtime/src/inference/student_adapter_inference.py +20 -5
- package/scripts/chat.py +29 -8
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 { createInterface } from "node:readline";
|
|
3
|
+
import { createInterface, emitKeypressEvents } from "node:readline";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -28,47 +28,97 @@ const S_ = [" ██████╗ ","██╔════╝ ","████
|
|
|
28
28
|
const ART = C_.map((_, i) => [C_[i], O_[i], F_[i], O_[i], S_[i]].join(" "));
|
|
29
29
|
const ART_W = ART[0].length;
|
|
30
30
|
|
|
31
|
-
function shadow() {
|
|
32
|
-
let s = " ";
|
|
33
|
-
for (const ch of ART[ART.length - 1]) s += ch !== " " ? "░" : " ";
|
|
34
|
-
return G + D + s.slice(0, -2) + " " + R;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
31
|
function vis(t) { return t.replace(/\x1b\[[0-9;]*m/g, "").length; }
|
|
38
|
-
function fw() { return Math.max((process.stdout.columns ||
|
|
39
|
-
|
|
32
|
+
function fw() { return Math.min(Math.max((process.stdout.columns || 110) - 2, 82), 150); }
|
|
40
33
|
function top(w) { return "╭" + "─".repeat(w) + "╮"; }
|
|
41
34
|
function bot(w) { return "╰" + "─".repeat(w) + "╯"; }
|
|
42
35
|
function sep(w) { return "├" + "─".repeat(w) + "┤"; }
|
|
43
|
-
function
|
|
44
|
-
function
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
return
|
|
36
|
+
function vpad(t, w) { return t + " ".repeat(Math.max(0, w - vis(t))); }
|
|
37
|
+
function clip(t, w) {
|
|
38
|
+
const plain = t.replace(/\x1b\[[0-9;]*m/g, "");
|
|
39
|
+
if (plain.length <= w) return t;
|
|
40
|
+
if (w <= 1) return "…";
|
|
41
|
+
return plain.slice(0, w - 1) + "…";
|
|
49
42
|
}
|
|
43
|
+
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) + " │"; }
|
|
46
|
+
function empty(w) { return full("", w); }
|
|
50
47
|
|
|
51
48
|
function render() {
|
|
52
49
|
console.clear();
|
|
53
50
|
const w = fw();
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
const minSplit = 96;
|
|
52
|
+
const cwd = process.cwd();
|
|
53
|
+
const rag = "RAG: KG + BM25 + optional PDFs";
|
|
54
|
+
const leftTitle = C + B + "COFOS" + R;
|
|
55
|
+
const left = [
|
|
56
|
+
leftTitle,
|
|
57
|
+
G + "9B scientific RAG CLI" + R,
|
|
58
|
+
"",
|
|
59
|
+
W + "Model" + R + " Willlzh/COFOS",
|
|
60
|
+
W + "Params" + R + " 9B",
|
|
61
|
+
W + "Path" + R + " " + cwd,
|
|
62
|
+
];
|
|
63
|
+
const right = [
|
|
64
|
+
W + B + "What COFOS Does" + R,
|
|
65
|
+
"Answers COF / photocatalysis / ROS questions with a local 9B model.",
|
|
66
|
+
rag,
|
|
67
|
+
"",
|
|
68
|
+
W + B + "Suggested Commands" + R,
|
|
69
|
+
"/help show commands and PDF-RAG usage",
|
|
70
|
+
"/topk 5 change retrieval depth",
|
|
71
|
+
"/rag off answer without retrieval",
|
|
72
|
+
"cofos --pdf-dir ./new_pdfs add local papers",
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const lines = [top(w)];
|
|
76
|
+
if (w >= minSplit) {
|
|
77
|
+
const inner = w - 5;
|
|
78
|
+
const lw = Math.max(28, Math.floor(inner * 0.34));
|
|
79
|
+
const rw = inner - lw;
|
|
80
|
+
const n = Math.max(left.length, right.length);
|
|
81
|
+
for (let i = 0; i < n; i++) lines.push(row(left[i] || "", right[i] || "", lw, rw));
|
|
59
82
|
} else {
|
|
60
|
-
|
|
61
|
-
|
|
83
|
+
for (const item of left) lines.push(full(item, w));
|
|
84
|
+
lines.push(sep(w));
|
|
85
|
+
for (const item of right) lines.push(full(item, w));
|
|
62
86
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
lines.push(empty(w));
|
|
88
|
+
lines.push(full(G + "Type / for commands · /exit · /topk 5 · /rag off · \\ for multiline" + R, w));
|
|
89
|
+
lines.push(bot(w));
|
|
90
|
+
console.log(lines.join("\n"));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* ─── Slash commands ─── */
|
|
94
|
+
const SLASH_COMMANDS = [
|
|
95
|
+
{ cmd: "/help", desc: "Show help" },
|
|
96
|
+
{ cmd: "/exit", desc: "Quit" },
|
|
97
|
+
{ cmd: "/clear", desc: "Clear conversation history" },
|
|
98
|
+
{ cmd: "/save", desc: "Save this session history" },
|
|
99
|
+
{ cmd: "/info", desc: "Show model and RAG info" },
|
|
100
|
+
{ cmd: "/topk 5", desc: "Set retrieval top-k" },
|
|
101
|
+
{ cmd: "/rag on", desc: "Enable RAG" },
|
|
102
|
+
{ cmd: "/rag off", desc: "Disable RAG" },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
function slashMatches(line) {
|
|
106
|
+
const needle = line.trim().toLowerCase();
|
|
107
|
+
return SLASH_COMMANDS.filter(({ cmd }) => cmd.toLowerCase().startsWith(needle));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function slashCompletions(line) {
|
|
111
|
+
if (!line.startsWith("/")) return [[], line];
|
|
112
|
+
const hits = slashMatches(line).map(({ cmd }) => cmd);
|
|
113
|
+
return [hits.length ? hits : SLASH_COMMANDS.map(({ cmd }) => cmd), line];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function slashHint(line = "/") {
|
|
117
|
+
const hits = slashMatches(line);
|
|
118
|
+
const rows = (hits.length ? hits : SLASH_COMMANDS)
|
|
119
|
+
.map(({ cmd, desc }) => ` ${cmd.padEnd(10)} ${desc}`)
|
|
120
|
+
.join("\n");
|
|
121
|
+
return `\n Commands\n${rows}\n Press Tab to complete.\n`;
|
|
72
122
|
}
|
|
73
123
|
|
|
74
124
|
/* ─── helpers ─── */
|
|
@@ -198,11 +248,31 @@ function main() {
|
|
|
198
248
|
output: process.stdout,
|
|
199
249
|
terminal: true,
|
|
200
250
|
prompt: mainPrompt,
|
|
251
|
+
completer: slashCompletions,
|
|
201
252
|
});
|
|
253
|
+
let lastSlashHint = "";
|
|
202
254
|
rl.on("line", (l) => {
|
|
255
|
+
lastSlashHint = "";
|
|
203
256
|
if (!ready || generating) inputQ.push(l);
|
|
204
257
|
else if (!handle(l)) ask();
|
|
205
258
|
});
|
|
259
|
+
if (process.stdin.isTTY) {
|
|
260
|
+
emitKeypressEvents(process.stdin, rl);
|
|
261
|
+
process.stdin.on("keypress", () => {
|
|
262
|
+
if (!ready || generating || closed) return;
|
|
263
|
+
const line = rl.line || "";
|
|
264
|
+
if (!line.startsWith("/")) {
|
|
265
|
+
lastSlashHint = "";
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const key = line.trim();
|
|
269
|
+
if (key && key !== lastSlashHint) {
|
|
270
|
+
lastSlashHint = key;
|
|
271
|
+
rl.output.write(slashHint(line));
|
|
272
|
+
rl.prompt(true);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
206
276
|
|
|
207
277
|
// Forward Python stderr (status / progress messages) to our stderr.
|
|
208
278
|
proc.stderr.on("data", (chunk) => { process.stderr.write(chunk); });
|
|
@@ -318,7 +388,7 @@ function main() {
|
|
|
318
388
|
// Local-only commands (don't hit the backend).
|
|
319
389
|
if (/^\/(exit|quit)$/.test(input)) { proc.stdin.write("/exit\n"); proc.stdin.end(); return true; }
|
|
320
390
|
if (/^\/(help)$/.test(input)) {
|
|
321
|
-
console.log("
|
|
391
|
+
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");
|
|
322
392
|
return false;
|
|
323
393
|
}
|
|
324
394
|
if (/^\/(clear)$/.test(input)) {
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"""Local student-model inference with optional confidence-gated KG/BM25 RAG."""
|
|
3
3
|
|
|
4
4
|
import logging
|
|
5
|
+
import os
|
|
5
6
|
import re
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from threading import Thread
|
|
@@ -225,17 +226,31 @@ class StudentAdapterInference:
|
|
|
225
226
|
|
|
226
227
|
@staticmethod
|
|
227
228
|
def _select_dtype() -> torch.dtype:
|
|
228
|
-
"""Pick
|
|
229
|
+
"""Pick a stable dtype for the current runtime.
|
|
229
230
|
|
|
230
|
-
|
|
231
|
-
|
|
231
|
+
CUDA can safely use bf16/fp16. On Mac/CPU, ``device_map=auto`` may place
|
|
232
|
+
layers on CPU or disk; fp16 there can produce NaN probabilities or
|
|
233
|
+
garbage tokens. Prefer float32 outside CUDA unless the user explicitly
|
|
234
|
+
opts into another dtype with COFOS_DTYPE.
|
|
232
235
|
"""
|
|
236
|
+
requested = os.environ.get("COFOS_DTYPE", "auto").strip().lower()
|
|
237
|
+
aliases = {
|
|
238
|
+
"fp32": torch.float32,
|
|
239
|
+
"float32": torch.float32,
|
|
240
|
+
"fp16": torch.float16,
|
|
241
|
+
"float16": torch.float16,
|
|
242
|
+
"bf16": torch.bfloat16,
|
|
243
|
+
"bfloat16": torch.bfloat16,
|
|
244
|
+
}
|
|
245
|
+
if requested in aliases:
|
|
246
|
+
return aliases[requested]
|
|
247
|
+
if requested not in {"", "auto"}:
|
|
248
|
+
logging.warning("Ignoring unsupported COFOS_DTYPE=%s; using auto", requested)
|
|
249
|
+
|
|
233
250
|
if torch.cuda.is_available():
|
|
234
251
|
if torch.cuda.is_bf16_supported():
|
|
235
252
|
return torch.bfloat16
|
|
236
253
|
return torch.float16
|
|
237
|
-
if torch.backends.mps.is_available():
|
|
238
|
-
return torch.float16
|
|
239
254
|
return torch.float32
|
|
240
255
|
|
|
241
256
|
def load_model(self) -> None:
|
package/scripts/chat.py
CHANGED
|
@@ -99,6 +99,11 @@ def parse_args() -> argparse.Namespace:
|
|
|
99
99
|
parser.add_argument("--pdf-dir", default=os.environ.get("COFOS_PDF_DIR"))
|
|
100
100
|
parser.add_argument("--rebuild-pdf-index", action="store_true")
|
|
101
101
|
parser.add_argument("--no-rag", action="store_true")
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"--restore-history",
|
|
104
|
+
action="store_true",
|
|
105
|
+
default=os.environ.get("COFOS_RESTORE_HISTORY", "").lower() in {"1", "true", "yes", "on"},
|
|
106
|
+
)
|
|
102
107
|
return parser.parse_args()
|
|
103
108
|
|
|
104
109
|
|
|
@@ -381,6 +386,20 @@ def _clean_history_content(role: str, content: str) -> str:
|
|
|
381
386
|
content = re.sub(r"(?is)^.*?</think>", "", content).strip()
|
|
382
387
|
return content
|
|
383
388
|
|
|
389
|
+
|
|
390
|
+
def _looks_like_corrupt_generation(answer: str) -> bool:
|
|
391
|
+
text = answer.strip()
|
|
392
|
+
if len(text) < 20:
|
|
393
|
+
return False
|
|
394
|
+
visible = [ch for ch in text if not ch.isspace()]
|
|
395
|
+
if not visible:
|
|
396
|
+
return True
|
|
397
|
+
alnum = sum(ch.isalnum() for ch in visible)
|
|
398
|
+
letters = sum(ch.isalpha() for ch in visible)
|
|
399
|
+
punct = sum((not ch.isalnum()) for ch in visible)
|
|
400
|
+
repeated_digits = bool(re.search(r"\d(?:\s*\d){8,}", text))
|
|
401
|
+
return (letters / len(visible) < 0.20 and punct / len(visible) > 0.35) or repeated_digits
|
|
402
|
+
|
|
384
403
|
def _load_history() -> list:
|
|
385
404
|
"""Load recent conversation turns from the persistent history file.
|
|
386
405
|
|
|
@@ -520,8 +539,8 @@ def main() -> None:
|
|
|
520
539
|
print("===READY===", flush=True)
|
|
521
540
|
|
|
522
541
|
top_k = args.top_k
|
|
523
|
-
history = _load_history()
|
|
524
|
-
if history:
|
|
542
|
+
history = _load_history() if args.restore_history else []
|
|
543
|
+
if history and os.environ.get("COFOS_DEBUG", "").lower() in {"1", "true", "yes", "on"}:
|
|
525
544
|
print(f"[chat] Restored {len(history) // 2} previous turns from history.",
|
|
526
545
|
file=sys.stderr, flush=True)
|
|
527
546
|
|
|
@@ -589,12 +608,14 @@ def main() -> None:
|
|
|
589
608
|
if os.environ.get("COFOS_DEBUG", "").lower() in {"1", "true", "yes", "on"}:
|
|
590
609
|
print(f"[chat] Generated in {gen_elapsed:.1f}s", file=sys.stderr, flush=True)
|
|
591
610
|
|
|
592
|
-
#
|
|
593
|
-
#
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
611
|
+
# Keep history in-memory for the current session. Persist only when
|
|
612
|
+
# the user explicitly runs /save, so a bad generation cannot poison
|
|
613
|
+
# future launches.
|
|
614
|
+
if _looks_like_corrupt_generation(answer):
|
|
615
|
+
print("[COFOS warning] The generated text looked corrupted and was not kept in history.", file=sys.stderr, flush=True)
|
|
616
|
+
else:
|
|
617
|
+
history.append({"role": "user", "content": question})
|
|
618
|
+
history.append({"role": "assistant", "content": answer})
|
|
598
619
|
except Exception as exc:
|
|
599
620
|
if os.environ.get("COFOS_DEBUG", "").lower() in {"1", "true", "yes", "on"}:
|
|
600
621
|
logging.exception("Generation failed")
|