@lzhzzzzwill/cofos 1.0.4 → 1.1.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 CHANGED
@@ -24,9 +24,9 @@ The package does not include model weights or KG/BM25 data. Those are resolved l
24
24
 
25
25
  | Resource | Default | Local location |
26
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/` |
27
+ | Student QA model | `Willlzh/COFOS` | `./.cofos/huggingface/` |
28
+ | Runtime KG/BM25 | `Willlzh/COFOS_data/runtime` | `./.cofos/data/kg/`, `./.cofos/retrieval_store/bm25/` |
29
+ | Local PDF chunks / manifest | user `--pdf-dir` | `./.cofos/data/processed/` |
30
30
  | Conversation history | local file | `~/.cofos_history.jsonl` |
31
31
 
32
32
  “Lazy” means the CLI checks local files first. It downloads or rebuilds only missing or changed resources.
@@ -49,6 +49,12 @@ Force PDF reparsing and BM25 rebuilding:
49
49
  npx @lzhzzzzwill/cofos --pdf-dir ./new_pdfs --rebuild-pdf-index
50
50
  ```
51
51
 
52
+ Put model and RAG data somewhere explicit:
53
+
54
+ ```bash
55
+ npx @lzhzzzzwill/cofos --cache-dir ./cofos_cache
56
+ ```
57
+
52
58
  Use a local merged model instead of Hugging Face:
53
59
 
54
60
  ```bash
@@ -66,15 +72,15 @@ npx @lzhzzzzwill/cofos --no-rag
66
72
  On startup, the CLI does the following:
67
73
 
68
74
  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.
75
+ 2. Uses the bundled `runtime/src` and `runtime/config` (staged from the project's `src/` and `config/` at publish time), so a full COFOS git checkout is not required.
76
+ 3. Checks the current folder cache, `./.cofos/`, for model and KG/BM25 runtime data.
71
77
  4. Downloads missing runtime files from `Willlzh/COFOS_data` only if needed.
72
78
  5. If `--pdf-dir` is provided, scans PDF names, sizes, and mtimes.
73
79
  6. Re-parses PDFs only when the folder contents changed or `--rebuild-pdf-index` is passed.
74
80
  7. Merges ROS-related PDF chunks into BM25 retrieval.
75
81
  8. Loads the model once and keeps the Python backend alive for the whole chat session.
76
82
 
77
- PDF files are read locally and are not uploaded anywhere.
83
+ PDF files are read locally and are not uploaded anywhere. Set `COFOS_CACHE_DIR` to override the COFOS cache root, or `HF_HOME` to override only the Hugging Face model cache.
78
84
 
79
85
  ## Chat Commands
80
86
 
@@ -82,8 +88,8 @@ PDF files are read locally and are not uploaded anywhere.
82
88
  |---|---|
83
89
  | `/help` | Show help |
84
90
  | `/exit` | Quit |
85
- | `/clear` | Clear conversation history for the session |
86
- | `/save` | Report persisted history path |
91
+ | `/clear` | Clear conversation history, in the session *and* on disk |
92
+ | `/save` | Write the current conversation history to `~/.cofos_history.jsonl` |
87
93
  | `/info` | Show model and RAG info |
88
94
  | `/topk <n>` | Change retrieval top-k |
89
95
  | `/rag off` | Disable RAG for this session |
@@ -98,6 +104,7 @@ cofos \
98
104
  --model Willlzh/COFOS \
99
105
  --config config/config.yaml \
100
106
  --pdf-dir ./new_pdfs \
107
+ --cache-dir ./.cofos \
101
108
  --top-k 5
102
109
  ```
103
110
 
@@ -106,6 +113,7 @@ cofos \
106
113
  | `--model`, `--merged-model-path` | Hugging Face repo id or local merged model path |
107
114
  | `--config` | Config path; defaults to bundled runtime config |
108
115
  | `--pdf-dir` | Folder of PDFs to add as extra BM25 evidence |
116
+ | `--cache-dir` | Folder for model, KG/BM25, and parsed PDF cache; defaults to `./.cofos` |
109
117
  | `--rebuild-pdf-index` | Force PDF parsing and BM25 rebuild |
110
118
  | `--top-k` | Retrieval top-k |
111
119
  | `--max-new-tokens` | Override generation length |
@@ -122,12 +130,30 @@ cofos --help
122
130
  cofos --pdf-dir ../data/raw_pdfs
123
131
  ```
124
132
 
133
+ ### `runtime/` is generated — do not edit it
134
+
135
+ `runtime/src` and `runtime/config` are **build output**, staged from the
136
+ project's `src/` and `config/`. Those are the single source of truth; the copy
137
+ under `runtime/` exists only so the published package is self-contained.
138
+
139
+ - Fix bugs in `src/` (or `config/`), never in `runtime/`.
140
+ - `npm pack` / `npm publish` rebuild `runtime/` automatically via the package-local
141
+ `prepack` wrapper when the full source checkout is present.
142
+ - Rebuild by hand with `npm run build`, or
143
+ `python scripts/sync_fwdemo_runtime.py`.
144
+ - Check for drift with `python scripts/sync_fwdemo_runtime.py --check`.
145
+
146
+ The published package requires its bundled `runtime/`; the backend does not fall
147
+ back to parent checkout folders. This keeps package tests from accidentally using
148
+ files outside `fwdemo/`.
149
+
125
150
  Before publishing:
126
151
 
127
152
  ```bash
128
153
  node --check bin/cofos.js
129
154
  python -m py_compile scripts/chat.py
130
- npm pack --dry-run
155
+ python -m pytest ../tests -q
156
+ npm pack --dry-run # runs prepack, rebuilding runtime/
131
157
  ```
132
158
 
133
- `npm pack --dry-run` should include `runtime/config`, `runtime/src`, `bin`, `scripts`, and `requirements.txt`, but should not include `__pycache__` or `*.pyc` files.
159
+ `npm pack --dry-run` should include `runtime/config`, `runtime/src`, `bin`, `scripts`, `RUNTIME.md`, and `requirements.txt`, but should not include `__pycache__` or `*.pyc` files.
package/RUNTIME.md ADDED
@@ -0,0 +1,38 @@
1
+ # COFOS fwdemo runtime model configuration
2
+
3
+ This file describes the bundled runtime used by the standalone `@lzhzzzzwill/cofos` npm package.
4
+
5
+ ## Standalone QA path
6
+
7
+ The standalone CLI does use Hugging Face by default:
8
+
9
+ - Student QA model: `Willlzh/COFOS`
10
+ - Runtime KG/BM25 data: `Willlzh/COFOS_data` under `runtime/`
11
+ - Local cache/write location: `./.cofos/` in the folder where the user runs `cofos`
12
+
13
+ On startup, fwdemo checks the current folder cache first. It downloads only missing model/data files. Use `--cache-dir`, `COFOS_CACHE_DIR`, or `HF_HOME` to override the defaults.
14
+
15
+ ```bash
16
+ cofos
17
+ cofos --pdf-dir ./new_pdfs
18
+ cofos --cache-dir ./cofos_cache
19
+ cofos --model /path/to/local/merged_model
20
+ ```
21
+
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
+
24
+ ## Runtime config scope
25
+
26
+ 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.
27
+
28
+ The npm package does not ship internal Ollama teacher settings. The standalone QA model is loaded through Transformers from `Willlzh/COFOS` unless the user passes a local `--model` path. Runtime KG/BM25 files are downloaded from `Willlzh/COFOS_data`.
29
+
30
+ ## Retrieval
31
+
32
+ BM25 is enabled by default. FAISS remains disabled in the standalone CLI; the packaged RAG path uses KG + BM25.
33
+
34
+ ```yaml
35
+ retrieval:
36
+ enable_faiss: false
37
+ enable_bm25: true
38
+ ```
package/bin/cofos.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn, spawnSync } from "node:child_process";
3
3
  import { createInterface } from "node:readline";
4
- import { existsSync } from "node:fs";
4
+ import { existsSync, readFileSync } from "node:fs";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
@@ -9,6 +9,7 @@ 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 pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
12
13
  const DEFAULT_MODEL = process.env.COFOS_MODEL || "Willlzh/COFOS";
13
14
  const DEFAULT_CONFIG = process.env.COFOS_CONFIG || "config/config.yaml";
14
15
 
@@ -75,9 +76,12 @@ function render() {
75
76
  function cmdExist(c) { return spawnSync(c, ["--version"], { encoding: "utf-8", stdio: "pipe" }).status === 0; }
76
77
  function findPy() { return cmdExist("python3") ? "python3" : cmdExist("python") ? "python" : null; }
77
78
 
79
+ // Only the modules the chat backend actually imports for the standalone merged-model path.
80
+ const REQUIRED_MODULES = "import transformers, torch, yaml, huggingface_hub, pdfplumber, rank_bm25";
81
+
78
82
  function ensureDeps(py) {
79
83
  console.log("[cofos] Checking Python dependencies …");
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;
84
+ if (spawnSync(py, ["-c", `${REQUIRED_MODULES}; print('ok')`], { encoding: "utf-8", stdio: "pipe" }).status === 0) return;
81
85
  console.log("[cofos] Installing Python dependencies (first run, may take a while) …");
82
86
  const r = spawnSync(py, ["-m", "pip", "install", "-r", reqFile], { encoding: "utf-8", stdio: "inherit" });
83
87
  if (r.status !== 0) { console.error("[cofos] Failed to install Python dependencies."); process.exit(1); }
@@ -88,7 +92,7 @@ function ensureDeps(py) {
88
92
  function main() {
89
93
  if (process.argv.includes("-h") || process.argv.includes("--help")) {
90
94
  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");
95
+ console.log(" Usage: cofos [--model PATH] [--config PATH] [--pdf-dir DIR] [--cache-dir DIR] [--top-k N] [--no-rag]\n");
92
96
  console.log(" Commands in chat:");
93
97
  console.log(" /help Show this help");
94
98
  console.log(" /exit Quit");
@@ -99,48 +103,86 @@ function main() {
99
103
  console.log(" /rag on|off Enable / disable RAG\n");
100
104
  console.log(" Startup options:");
101
105
  console.log(" --pdf-dir DIR Add PDFs from DIR as extra BM25 evidence");
106
+ console.log(" --cache-dir DIR Store model/data cache here (default: ./.cofos)");
102
107
  console.log(" --rebuild-pdf-index Force reparsing PDFs and rebuilding BM25\n");
103
108
  console.log(" Multi-line: end a line with \\ to continue on the next line.\n");
104
109
  process.exit(0);
105
110
  }
106
111
 
107
- render();
108
-
109
- const py = findPy();
110
- if (!py) { console.error("[cofos] Python not found."); process.exit(1); }
111
- if (!existsSync(chatScript)) { console.error("[cofos] Missing script:", chatScript); process.exit(1); }
112
- ensureDeps(py);
113
-
114
- console.log(" Loading COFOS 9B + RAG backend from Hugging Face … first run may take a while\n");
115
-
112
+ // Parse and validate before the banner and the dependency check, so a bad
113
+ // flag fails immediately instead of after a slow pip round-trip.
116
114
  const cliArgs = process.argv.slice(2);
117
115
  let modelPath = DEFAULT_MODEL;
118
116
  let configPath = DEFAULT_CONFIG;
119
117
  let topK = "5";
120
118
  let pdfDir = null;
119
+ let cacheDir = process.env.COFOS_CACHE_DIR ? resolve(process.env.COFOS_CACHE_DIR) : resolve(process.cwd(), ".cofos");
121
120
  let rebuildPdfIndex = false;
122
121
  const backendArgs = [chatScript];
122
+
123
+ const die = (msg) => { console.error("[cofos] " + msg); process.exit(1); };
124
+ // A flag placed last used to read `undefined` as its value, which then made
125
+ // spawn() throw an opaque TypeError instead of reporting the bad usage.
126
+ const value = (i, flag) => {
127
+ const v = cliArgs[i];
128
+ if (v === undefined || v.startsWith("--")) die(`Missing value for ${flag}`);
129
+ return v;
130
+ };
131
+
123
132
  for (let i = 0; i < cliArgs.length; i++) {
124
133
  const arg = cliArgs[i];
125
134
  if (arg === "--model" || arg === "--merged-model-path") {
126
- modelPath = cliArgs[++i];
135
+ modelPath = value(++i, arg);
127
136
  } else if (arg === "--config") {
128
- configPath = cliArgs[++i];
137
+ configPath = value(++i, arg);
129
138
  } else if (arg === "--top-k") {
130
- topK = cliArgs[++i];
139
+ topK = value(++i, arg);
140
+ if (!/^\d+$/.test(topK) || Number(topK) < 1) die(`--top-k must be a positive integer, got "${topK}"`);
131
141
  } else if (arg === "--pdf-dir") {
132
- pdfDir = cliArgs[++i];
142
+ pdfDir = resolve(value(++i, arg));
143
+ if (!existsSync(pdfDir)) die(`--pdf-dir does not exist: ${pdfDir}`);
144
+ } else if (arg === "--cache-dir") {
145
+ cacheDir = resolve(value(++i, arg));
133
146
  } else if (arg === "--rebuild-pdf-index") {
134
147
  rebuildPdfIndex = true;
135
148
  } else if (arg === "--no-rag") {
136
149
  backendArgs.push("--no-rag");
150
+ } else {
151
+ die(`Unknown option: ${arg}\n Run "cofos --help" for usage.`);
137
152
  }
138
153
  }
139
154
  backendArgs.push("--merged-model-path", modelPath, "--config", configPath, "--top-k", topK);
140
155
  if (pdfDir) backendArgs.push("--pdf-dir", pdfDir);
141
156
  if (rebuildPdfIndex) backendArgs.push("--rebuild-pdf-index");
142
157
 
143
- const proc = spawn(py, backendArgs, { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" });
158
+ render();
159
+
160
+ const py = findPy();
161
+ if (!py) die("Python not found. Install Python 3.9+ and re-run.");
162
+ if (!existsSync(chatScript)) die(`Missing script: ${chatScript}`);
163
+ ensureDeps(py);
164
+
165
+ console.log(" Loading COFOS 9B + RAG backend from Hugging Face … first run may take a while\n");
166
+
167
+ const backendEnv = {
168
+ ...process.env,
169
+ COFOS_CACHE_DIR: cacheDir,
170
+ HF_HOME: process.env.HF_HOME || join(cacheDir, "huggingface"),
171
+ HF_HUB_DISABLE_XET: process.env.HF_HUB_DISABLE_XET || "1",
172
+ HF_HUB_ENABLE_HF_TRANSFER: process.env.HF_HUB_ENABLE_HF_TRANSFER || "0",
173
+ HF_HUB_DOWNLOAD_TIMEOUT: process.env.HF_HUB_DOWNLOAD_TIMEOUT || "120",
174
+ HF_HUB_ETAG_TIMEOUT: process.env.HF_HUB_ETAG_TIMEOUT || "30",
175
+ };
176
+ const proc = spawn(py, backendArgs, {
177
+ stdio: ["pipe", "pipe", "pipe"],
178
+ env: backendEnv,
179
+ });
180
+
181
+ // `encoding` is not a spawn() option — without setEncoding each Buffer chunk
182
+ // is decoded on its own, so a multi-byte character (·OH, ¹O₂, any Chinese)
183
+ // straddling a chunk boundary comes out as mojibake.
184
+ proc.stdout.setEncoding("utf-8");
185
+ proc.stderr.setEncoding("utf-8");
144
186
 
145
187
  let buf = "", ready = false, generating = false, closed = false;
146
188
  const inputQ = [];
@@ -162,7 +204,9 @@ function main() {
162
204
  }
163
205
  return;
164
206
  }
165
- if (!generating) return;
207
+ // Anything arriving while no answer is in flight is not part of a response;
208
+ // dropping it keeps it from being prepended to the next one.
209
+ if (!generating) { buf = ""; return; }
166
210
 
167
211
  // Scan for DONE marker — stream everything before it.
168
212
  const di = buf.indexOf(DONE);
@@ -196,9 +240,23 @@ function main() {
196
240
  closed = true;
197
241
  if (c && c !== 0) console.error("[cofos] Python exited with code", c);
198
242
  try { rl.close(); } catch (_) {}
199
- process.exit(c ?? 0);
243
+ // Let queued stdout writes flush before tearing the process down.
244
+ process.exitCode = c ?? 0;
200
245
  });
201
246
 
247
+ function shutdown() {
248
+ if (closed) return;
249
+ closed = true;
250
+ try { proc.stdin.end(); } catch (_) {}
251
+ try { rl.close(); } catch (_) {}
252
+ proc.kill("SIGTERM");
253
+ }
254
+
255
+ // Ctrl+C and Ctrl+D should stop the backend instead of orphaning it.
256
+ rl.on("SIGINT", () => { console.log("\n[cofos] Interrupted."); shutdown(); });
257
+ rl.on("close", () => { if (!closed) shutdown(); });
258
+ process.on("SIGTERM", shutdown);
259
+
202
260
  function drain() {
203
261
  while (inputQ.length) {
204
262
  const line = inputQ.shift();
@@ -244,9 +302,11 @@ function main() {
244
302
  return;
245
303
  }
246
304
  if (/^\/(info)$/.test(input)) {
247
- console.log("\n COFOS CLI — v1.0.0");
305
+ console.log(`\n COFOS CLI — v${pkg.version}`);
248
306
  console.log(" Model: Willlzh/COFOS (Hugging Face)");
249
307
  console.log(" RAG: Willlzh/COFOS_data/runtime (KG + BM25) + optional --pdf-dir BM25 evidence");
308
+ console.log(` Cache: ${cacheDir}`);
309
+ console.log(` HF: ${backendEnv.HF_HOME}`);
250
310
  console.log(" Engine: StudentAdapterInference + transformers + PyTorch");
251
311
  console.log(" Hist: ~/.cofos_history.jsonl\n");
252
312
  ask(); return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzhzzzzwill/cofos",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "COFOS 9B + RAG chat CLI with optional local PDF ingestion.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,16 +9,24 @@
9
9
  "files": [
10
10
  "bin",
11
11
  "scripts/chat.py",
12
+ "scripts/sync_runtime.py",
13
+ "RUNTIME.md",
12
14
  "runtime/config/*.yaml",
13
- "runtime/config/*.md",
14
- "runtime/src/data_processing/*.py",
15
+ "runtime/src/data_processing/__init__.py",
16
+ "runtime/src/data_processing/parse_pdf_text.py",
17
+ "runtime/src/data_processing/utils.py",
15
18
  "runtime/src/inference/*.py",
16
- "runtime/src/retrieval/*.py",
19
+ "runtime/src/retrieval/__init__.py",
20
+ "runtime/src/retrieval/build_bm25_index.py",
21
+ "runtime/src/retrieval/retrieve_evidence.py",
22
+ "runtime/src/retrieval/text_search.py",
17
23
  "requirements.txt"
18
24
  ],
19
25
  "scripts": {
20
26
  "start": "node ./bin/cofos.js",
21
- "test": "node ./bin/cofos.js --help"
27
+ "test": "node ./bin/cofos.js --help",
28
+ "build": "python3 scripts/sync_runtime.py",
29
+ "prepack": "python3 scripts/sync_runtime.py"
22
30
  },
23
31
  "keywords": [
24
32
  "cli",
package/requirements.txt CHANGED
@@ -1,10 +1,7 @@
1
1
  transformers>=4.40.0
2
2
  torch
3
3
  accelerate
4
- peft
5
4
  PyYAML
6
5
  rank-bm25
7
6
  huggingface_hub>=0.20.0
8
- requests>=2.31.0
9
- numpy>=1.24.0
10
7
  pdfplumber>=0.9.0