@lzhzzzzwill/cofos 1.0.0 → 1.0.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/README.md CHANGED
@@ -1,112 +1,133 @@
1
1
  # COFOS CLI
2
2
 
3
- 一个本地 LLM 聊天 CLI:3D 立体欢迎界面 + Hugging Face 模型对话。
3
+ COFOS CLI is a standalone Node.js command line wrapper around the COFOS Python runtime. It starts a persistent Python backend, loads the COFOS student model, prepares KG/BM25 runtime data when missing, and can ingest local PDFs as extra retrieval evidence.
4
4
 
5
5
  ```bash
6
- npx cofos
6
+ npx @lzhzzzzwill/cofos
7
7
  ```
8
8
 
9
- ```
10
- ╭─────────────────────────────────────────────────────────╮
11
- │ ██████╗ ██████╗ ███████╗ ██████╗ ███████╗
12
- │ ██╔════╝ ██╔═══██╗ ██╔════╝ ██╔═══██╗ ██╔════╝ │
13
- │ ██║ ██║ ██║ ███████╗ ██║ ██║ ███████╗ │
14
- │ ██║ ██║ ██║ ██╔════╝ ██║ ██║ ╚════██║ │
15
- │ ╚██████╗ ╚██████╔╝ ██║ ╚██████╔╝ ███████║ │
16
- │ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝
17
- │ ░░░░░░░ ░░░░░░░ ░░░ ░░░░░░░ ░░░░░░░ │ ← 3D shadow
18
- Welcome to COFOS CLI — local LLM chat │
19
- Type /help for commands · /exit to quit│
20
- ╰─────────────────────────────────────────────────────────╯
21
- cofos > What is a MOF material?
22
- [Model streams response...]
23
- cofos > Tell me more about its applications.
9
+ ## Runtime Model
10
+
11
+ The standalone npm package includes a minimal Python runtime under `runtime/`:
12
+
13
+ ```text
14
+ fwdemo/
15
+ ├── bin/cofos.js
16
+ ├── scripts/chat.py
17
+ ├── runtime/
18
+ ├── config/
19
+ └── src/
20
+ └── requirements.txt
24
21
  ```
25
22
 
26
- ## 快速开始
23
+ The package does not include model weights or KG/BM25 data. Those are resolved lazily:
27
24
 
28
- ```bash
29
- # 直接运行
30
- npx cofos
25
+ | Resource | Default | Local location |
26
+ |---|---|---|
27
+ | Student QA model | `Willlzh/COFOS` | Hugging Face cache |
28
+ | Runtime KG/BM25 | `Willlzh/COFOS_data/runtime` | `~/.cache/cofos/` |
29
+ | Local PDF chunks / manifest | user `--pdf-dir` | `~/.cache/cofos/data/processed/` |
30
+ | Conversation history | local file | `~/.cofos_history.jsonl` |
31
31
 
32
- # 或本地
33
- git clone <repo> && cd demo && npm link
34
- cofos
35
- ```
32
+ “Lazy” means the CLI checks local files first. It downloads or rebuilds only missing or changed resources.
33
+
34
+ ## Quick Start
36
35
 
37
- 首次运行会自动安装 Python 依赖并下载模型(约 30-60 秒)。
36
+ ```bash
37
+ npx @lzhzzzzwill/cofos
38
+ ```
38
39
 
39
- ## 命令
40
+ Use local PDFs as additional BM25 evidence:
40
41
 
41
- | 命令 | 说明 |
42
- |------|------|
43
- | `/help` | 显示帮助 |
44
- | `/exit` | 退出 |
45
- | `/clear` | 清屏重新显示欢迎界面 |
46
- | `/info` | 显示模型和版本信息 |
47
- | `/model <id>` | 切换 Hugging Face 模型 |
42
+ ```bash
43
+ npx @lzhzzzzwill/cofos --pdf-dir ./new_pdfs
44
+ ```
48
45
 
49
- ## 项目结构
46
+ Force PDF reparsing and BM25 rebuilding:
50
47
 
48
+ ```bash
49
+ npx @lzhzzzzwill/cofos --pdf-dir ./new_pdfs --rebuild-pdf-index
51
50
  ```
52
- ├── package.json # npm 包配置,bin → cofos
53
- ├── requirements.txt # Python 依赖:transformers + torch
54
- ├── .gitignore
55
- ├── bin/
56
- │ └── cofos.js # Node.js CLI:3D 前端 + 子进程管理
57
- └── scripts/
58
- └── chat.py # Python 推理:加载模型、生成回复
51
+
52
+ Use a local merged model instead of Hugging Face:
53
+
54
+ ```bash
55
+ npx @lzhzzzzwill/cofos --model /path/to/merged_model
59
56
  ```
60
57
 
61
- ## 架构概览
58
+ Disable RAG for a session:
62
59
 
60
+ ```bash
61
+ npx @lzhzzzzwill/cofos --no-rag
63
62
  ```
64
- 终端 Node.js (cofos.js) Python (chat.py)
65
- │ │ │
66
- │ cofos │ │
67
- │────────────────────────────────>│ │
68
- │ │ spawn python3 scripts/chat.py │
69
- │ │──────────────────────────────────────>│
70
- │ │ │ 加载模型
71
- │ │ stdout ===READY=== │
72
- │ │<──────────────────────────────────────│
73
- │ 显示 3D 欢迎界面 + 提示符 │ │
74
- │<───────────────────────────────│ │
75
- │ │ │
76
- │ 用户输入 "What is MOF?" │ │
77
- │────────────────────────────────>│ │
78
- │ │ stdin "What is MOF?" │
79
- │ │──────────────────────────────────────>│
80
- │ │ │ 生成 token
81
- │ │ stdout ← 流式 token │
82
- │ 流式显示回复 │<──────────────────────────────────────│
83
- │<───────────────────────────────│ │
84
- │ │ stdout \n===DONE===\n │
85
- │ │<──────────────────────────────────────│
86
- │ 显示 "cofos >" 等待下一条 │ │
87
- │<───────────────────────────────│ │
63
+
64
+ ## Startup Behavior
65
+
66
+ On startup, the CLI does the following:
67
+
68
+ 1. Finds Python and checks required Python packages.
69
+ 2. Uses bundled `runtime/src` and `runtime/config`, so a full COFOS git checkout is not required.
70
+ 3. Checks `~/.cache/cofos/` for KG/BM25 runtime data.
71
+ 4. Downloads missing runtime files from `Willlzh/COFOS_data` only if needed.
72
+ 5. If `--pdf-dir` is provided, scans PDF names, sizes, and mtimes.
73
+ 6. Re-parses PDFs only when the folder contents changed or `--rebuild-pdf-index` is passed.
74
+ 7. Merges ROS-related PDF chunks into BM25 retrieval.
75
+ 8. Loads the model once and keeps the Python backend alive for the whole chat session.
76
+
77
+ PDF files are read locally and are not uploaded anywhere.
78
+
79
+ ## Chat Commands
80
+
81
+ | Command | Description |
82
+ |---|---|
83
+ | `/help` | Show help |
84
+ | `/exit` | Quit |
85
+ | `/clear` | Clear conversation history for the session |
86
+ | `/save` | Report persisted history path |
87
+ | `/info` | Show model and RAG info |
88
+ | `/topk <n>` | Change retrieval top-k |
89
+ | `/rag off` | Disable RAG for this session |
90
+ | `/rag on` | Re-enable RAG for this session |
91
+
92
+ Multi-line questions are supported by ending a line with `\`.
93
+
94
+ ## CLI Options
95
+
96
+ ```bash
97
+ cofos \
98
+ --model Willlzh/COFOS \
99
+ --config config/config.yaml \
100
+ --pdf-dir ./new_pdfs \
101
+ --top-k 5
88
102
  ```
89
103
 
90
- ## 技术栈
104
+ | Option | Description |
105
+ |---|---|
106
+ | `--model`, `--merged-model-path` | Hugging Face repo id or local merged model path |
107
+ | `--config` | Config path; defaults to bundled runtime config |
108
+ | `--pdf-dir` | Folder of PDFs to add as extra BM25 evidence |
109
+ | `--rebuild-pdf-index` | Force PDF parsing and BM25 rebuild |
110
+ | `--top-k` | Retrieval top-k |
111
+ | `--max-new-tokens` | Override generation length |
112
+ | `--no-rag` | Run without KG/BM25 retrieval |
91
113
 
92
- | 层 | 技术 | 职责 |
93
- |----|------|------|
94
- | CLI 前端 | Node.js (ESM) | 终端 UI、子进程管理、管道通信 |
95
- | 模型后端 | Python + transformers | 模型加载、token 生成、流式输出 |
96
- | 模型 | SmolLM2-360M-Instruct (HF) | 对话推理 |
97
- | 通信 | stdin/stdout 管道 | 进程间消息传递 |
98
- | 分發 | npm | 包管理、全局命令、npx |
114
+ ## Development
99
115
 
100
- ## 依赖
116
+ From this repository:
101
117
 
102
- - Node.js ≥ 18
103
- - Python 3.8+
104
- - 首次运行需联网(下载模型,~360MB)
118
+ ```bash
119
+ cd fwdemo
120
+ npm link
121
+ cofos --help
122
+ cofos --pdf-dir ../data/raw_pdfs
123
+ ```
105
124
 
106
- ## 开发
125
+ Before publishing:
107
126
 
108
127
  ```bash
109
- npm link # 注册全局命令
110
- cofos # 启动
111
- npm run start # 或直接 node ./bin/cofos.js
128
+ node --check bin/cofos.js
129
+ python -m py_compile scripts/chat.py
130
+ npm pack --dry-run
112
131
  ```
132
+
133
+ `npm pack --dry-run` should include `runtime/config`, `runtime/src`, `bin`, `scripts`, and `requirements.txt`, but should not include `__pycache__` or `*.pyc` files.
package/bin/cofos.js CHANGED
@@ -9,10 +9,15 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
9
9
  const root = resolve(__dirname, "..");
10
10
  const chatScript = join(root, "scripts", "chat.py");
11
11
  const reqFile = join(root, "requirements.txt");
12
+ const DEFAULT_MODEL = process.env.COFOS_MODEL || "Willlzh/COFOS";
13
+ const DEFAULT_CONFIG = process.env.COFOS_CONFIG || "config/config.yaml";
12
14
 
13
15
  /* ─── ANSI ─── */
14
16
  const R = "\x1b[0m", B = "\x1b[1m", D = "\x1b[2m";
15
- const C = "\x1b[96m", W = "\x1b[97m", G = "\x1b[90m";
17
+ const C = "\x1b[96m", W = "\x1b[97m", G = "\x1b[90m", Y = "\x1b[93m";
18
+
19
+ /* ─── Protocol markers ─── */
20
+ const READY = "===READY===", RESPONSE = "===RESPONSE===", DONE = "===DONE===";
16
21
 
17
22
  /* ─── 3D COFOS art ─── */
18
23
  const C_ = [" ██████╗ ","██╔════╝ ","██║ ","██║ ","╚██████╗ "," ╚═════╝ "];
@@ -57,9 +62,9 @@ function render() {
57
62
  l.push(emp(w));
58
63
  l.push(sep(w));
59
64
  l.push(emp(w));
60
- l.push(cnt(W + B + "Welcome to COFOS CLI — local LLM chat" + R, w));
65
+ l.push(cnt(W + B + "Welcome to COFOS CLI — 9B + RAG chat (Willlzh/COFOS)" + R, w));
61
66
  l.push(emp(w));
62
- l.push(cnt(G + "Type /help for commands · /exit to quit" + R, w));
67
+ l.push(cnt(G + "Type /help · /exit · /topk 5 · /rag off · \\ for multiline" + R, w));
63
68
  l.push(emp(w));
64
69
  l.push(bot(w));
65
70
  console.log(l.join("\n"));
@@ -72,7 +77,7 @@ function findPy() { return cmdExist("python3") ? "python3" : cmdExist("python")
72
77
 
73
78
  function ensureDeps(py) {
74
79
  console.log("[cofos] Checking Python dependencies …");
75
- if (spawnSync(py, ["-c", "import transformers, torch; print('ok')"], { encoding: "utf-8", stdio: "pipe" }).status === 0) return;
80
+ if (spawnSync(py, ["-c", "import transformers, torch, yaml, huggingface_hub, requests, numpy, pdfplumber; import peft; import rank_bm25; print('ok')"], { encoding: "utf-8", stdio: "pipe" }).status === 0) return;
76
81
  console.log("[cofos] Installing Python dependencies (first run, may take a while) …");
77
82
  const r = spawnSync(py, ["-m", "pip", "install", "-r", reqFile], { encoding: "utf-8", stdio: "inherit" });
78
83
  if (r.status !== 0) { console.error("[cofos] Failed to install Python dependencies."); process.exit(1); }
@@ -82,7 +87,21 @@ function ensureDeps(py) {
82
87
 
83
88
  function main() {
84
89
  if (process.argv.includes("-h") || process.argv.includes("--help")) {
85
- console.log("\n COFOS CLI — local LLM chat\n Usage: cofos\n"); process.exit(0);
90
+ console.log("\n COFOS CLI — 9B + RAG chat (Willlzh/COFOS)\n");
91
+ console.log(" Usage: cofos [--model PATH] [--config PATH] [--pdf-dir DIR] [--top-k N] [--no-rag]\n");
92
+ console.log(" Commands in chat:");
93
+ console.log(" /help Show this help");
94
+ console.log(" /exit Quit");
95
+ console.log(" /clear Clear screen + conversation history");
96
+ console.log(" /save Persist conversation history");
97
+ console.log(" /info Show model and RAG info");
98
+ console.log(" /topk N Set retrieval top-k");
99
+ console.log(" /rag on|off Enable / disable RAG\n");
100
+ console.log(" Startup options:");
101
+ console.log(" --pdf-dir DIR Add PDFs from DIR as extra BM25 evidence");
102
+ console.log(" --rebuild-pdf-index Force reparsing PDFs and rebuilding BM25\n");
103
+ console.log(" Multi-line: end a line with \\ to continue on the next line.\n");
104
+ process.exit(0);
86
105
  }
87
106
 
88
107
  render();
@@ -92,42 +111,84 @@ function main() {
92
111
  if (!existsSync(chatScript)) { console.error("[cofos] Missing script:", chatScript); process.exit(1); }
93
112
  ensureDeps(py);
94
113
 
95
- console.log(" Loading model (30–60 s on first run)\n");
114
+ console.log(" Loading COFOS 9B + RAG backend from Hugging Face … first run may take a while\n");
115
+
116
+ const cliArgs = process.argv.slice(2);
117
+ let modelPath = DEFAULT_MODEL;
118
+ let configPath = DEFAULT_CONFIG;
119
+ let topK = "5";
120
+ let pdfDir = null;
121
+ let rebuildPdfIndex = false;
122
+ const backendArgs = [chatScript];
123
+ for (let i = 0; i < cliArgs.length; i++) {
124
+ const arg = cliArgs[i];
125
+ if (arg === "--model" || arg === "--merged-model-path") {
126
+ modelPath = cliArgs[++i];
127
+ } else if (arg === "--config") {
128
+ configPath = cliArgs[++i];
129
+ } else if (arg === "--top-k") {
130
+ topK = cliArgs[++i];
131
+ } else if (arg === "--pdf-dir") {
132
+ pdfDir = cliArgs[++i];
133
+ } else if (arg === "--rebuild-pdf-index") {
134
+ rebuildPdfIndex = true;
135
+ } else if (arg === "--no-rag") {
136
+ backendArgs.push("--no-rag");
137
+ }
138
+ }
139
+ backendArgs.push("--merged-model-path", modelPath, "--config", configPath, "--top-k", topK);
140
+ if (pdfDir) backendArgs.push("--pdf-dir", pdfDir);
141
+ if (rebuildPdfIndex) backendArgs.push("--rebuild-pdf-index");
96
142
 
97
- const proc = spawn(py, [chatScript], { stdio: ["pipe", "pipe", "inherit"], encoding: "utf-8" });
143
+ const proc = spawn(py, backendArgs, { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" });
98
144
 
99
145
  let buf = "", ready = false, generating = false, closed = false;
100
146
  const inputQ = [];
101
- const rl = createInterface({ input: process.stdin });
147
+ const rl = createInterface({ input: process.stdin, terminal: true });
102
148
  rl.on("line", (l) => { inputQ.push(l); });
103
149
 
150
+ // Forward Python stderr (status / progress messages) to our stderr.
151
+ proc.stderr.on("data", (chunk) => { process.stderr.write(chunk); });
152
+
104
153
  proc.stdout.on("data", (chunk) => {
105
- try {
106
- buf += chunk;
107
- if (!ready) {
108
- const i = buf.indexOf("===READY===");
109
- if (i !== -1) {
110
- ready = true;
111
- buf = buf.slice(i + 10);
112
- if (buf[0] === "\n") buf = buf.slice(1);
113
- drain();
114
- }
115
- return;
116
- }
117
- if (!generating) return;
118
- const i = buf.indexOf("\n===DONE===");
154
+ buf += chunk;
155
+ if (!ready) {
156
+ const i = buf.indexOf(READY);
119
157
  if (i !== -1) {
120
- process.stdout.write(buf.slice(0, i));
121
- buf = buf.slice(i + 11);
158
+ ready = true;
159
+ buf = buf.slice(i + READY.length);
122
160
  if (buf[0] === "\n") buf = buf.slice(1);
123
- generating = false;
124
- console.log("");
125
161
  drain();
126
- } else {
127
- const k = 10;
128
- if (buf.length > k) { process.stdout.write(buf.slice(0, -k)); buf = buf.slice(-k); }
129
162
  }
130
- } catch (_) {}
163
+ return;
164
+ }
165
+ if (!generating) return;
166
+
167
+ // Scan for DONE marker — stream everything before it.
168
+ const di = buf.indexOf(DONE);
169
+ if (di !== -1) {
170
+ // Flush any pre-DONE content (cleaned of RESPONSE marker).
171
+ let pre = buf.slice(0, di);
172
+ const ri = pre.indexOf(RESPONSE);
173
+ if (ri !== -1) pre = pre.slice(ri + RESPONSE.length);
174
+ if (pre) process.stdout.write(pre);
175
+ buf = buf.slice(di + DONE.length);
176
+ if (buf[0] === "\n") buf = buf.slice(1);
177
+ generating = false;
178
+ console.log("");
179
+ drain();
180
+ } else {
181
+ // Stream buffered content, keeping only enough tail to catch a
182
+ // partial DONE marker.
183
+ const keep = DONE.length;
184
+ if (buf.length > keep) {
185
+ let out = buf.slice(0, -keep);
186
+ const ri = out.indexOf(RESPONSE);
187
+ if (ri !== -1) out = out.slice(ri + RESPONSE.length);
188
+ if (out) process.stdout.write(out);
189
+ buf = buf.slice(-keep);
190
+ }
191
+ }
131
192
  });
132
193
 
133
194
  proc.on("error", (e) => { console.error("[cofos] Python error:", e.message); process.exit(1); });
@@ -151,30 +212,47 @@ function main() {
151
212
  try { rl.question(C + " cofos " + R, handle); } catch (_) {}
152
213
  }
153
214
 
215
+ /* ─── multi-line accumulator ─── */
216
+ let mlBuf = [];
217
+
154
218
  function handle(input) {
219
+ // Multi-line continuation: lines ending with "\"
220
+ if (input.endsWith("\\")) {
221
+ mlBuf.push(input.slice(0, -1));
222
+ try { rl.question(G + " ... " + R, handle); } catch (_) {}
223
+ return;
224
+ }
225
+ if (mlBuf.length > 0) {
226
+ mlBuf.push(input);
227
+ input = mlBuf.join("\n");
228
+ mlBuf = [];
229
+ }
230
+
155
231
  input = input.trim();
156
232
  if (!input) { ask(); return; }
157
233
 
234
+ // Local-only commands (don't hit the backend).
158
235
  if (/^\/(exit|quit)$/.test(input)) { proc.stdin.write("/exit\n"); proc.stdin.end(); return; }
159
236
  if (/^\/(help)$/.test(input)) {
160
- console.log("\n Commands:\n /help Show help\n /exit Exit\n /clear Clear screen\n /info Model info\n /model Switch model\n");
161
- ask(); return;
162
- }
163
- if (/^\/(clear)$/.test(input)) { render(); console.log(""); ask(); return; }
164
- if (/^\/(info)$/.test(input)) {
165
- console.log("\n COFOS CLI — v1.0.0");
166
- console.log(" Model: HuggingFaceTB/SmolLM2-360M-Instruct");
167
- console.log(" Backend: transformers + PyTorch\n");
237
+ console.log("\n Commands:\n /help Show help\n /exit Quit\n /clear Clear conversation history\n /save Persist conversation history\n /info Model and RAG info\n /topk N Set retrieval top-k\n /rag on|off Enable/disable RAG\n\n Multi-line: end a line with \\ to continue.\n");
168
238
  ask(); return;
169
239
  }
170
- if (/^\/model\s/.test(input)) {
171
- const m = input.slice(7).trim();
172
- console.log("[cofos] Switching model to", m);
240
+ if (/^\/(clear)$/.test(input)) {
241
+ mlBuf = [];
173
242
  proc.stdin.write(input + "\n");
174
243
  generating = true;
175
244
  return;
176
245
  }
246
+ if (/^\/(info)$/.test(input)) {
247
+ console.log("\n COFOS CLI — v1.0.0");
248
+ console.log(" Model: Willlzh/COFOS (Hugging Face)");
249
+ console.log(" RAG: Willlzh/COFOS_data/runtime (KG + BM25) + optional --pdf-dir BM25 evidence");
250
+ console.log(" Engine: StudentAdapterInference + transformers + PyTorch");
251
+ console.log(" Hist: ~/.cofos_history.jsonl\n");
252
+ ask(); return;
253
+ }
177
254
 
255
+ // Forward to backend.
178
256
  generating = true;
179
257
  proc.stdin.write(input + "\n");
180
258
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lzhzzzzwill/cofos",
3
- "version": "1.0.0",
4
- "description": "A local LLM chat CLI with a 3D stereoscopic welcome screen.",
3
+ "version": "1.0.2",
4
+ "description": "COFOS 9B + RAG chat CLI with optional local PDF ingestion.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "cofos": "bin/cofos.js"
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "bin",
11
11
  "scripts",
12
+ "runtime",
12
13
  "requirements.txt"
13
14
  ],
14
15
  "scripts": {
package/requirements.txt CHANGED
@@ -1,2 +1,10 @@
1
1
  transformers>=4.40.0
2
2
  torch
3
+ accelerate
4
+ peft
5
+ PyYAML
6
+ rank-bm25
7
+ huggingface_hub>=0.20.0
8
+ requests>=2.31.0
9
+ numpy>=1.24.0
10
+ pdfplumber>=0.9.0