Skip to content

Commit dc88531

Browse files
authored
feat(minor): save and load history to chat command (#71)
* feat: save and load history to `chat` command * build: support patch bump of minor features * fix: show correct cli command on Windows
1 parent 4ff8189 commit dc88531

File tree

6 files changed

+122
-13
lines changed

6 files changed

+122
-13
lines changed

.releaserc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
["@semantic-release/commit-analyzer", {
88
"preset": "angular",
99
"releaseRules": [
10+
{"type": "feat", "scope": "minor", "release": "patch"},
1011
{"type": "docs", "scope": "README", "release": "patch"}
1112
]
1213
}],

docs/guide/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ npm install --save node-llama-cpp
1111

1212
> `node-llama-cpp` comes with pre-built binaries for macOS, Linux and Windows.
1313
>
14-
> If binaries are not available for your platform, it'll fallback to download the latest version of `llama.cpp` and build it from source with `cmake`.
14+
> If binaries are not available for your platform, it'll fallback to download a release of `llama.cpp` and build it from source with `cmake`.
1515
> To disable this behavior, set the environment variable `NODE_LLAMA_CPP_SKIP_DOWNLOAD` to `true`.
1616
1717
## CUDA and Metal support

src/cli/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import yargs from "yargs";
66
// eslint-disable-next-line node/file-extension-in-import
77
import {hideBin} from "yargs/helpers";
88
import fs from "fs-extra";
9+
import {cliBinName} from "../config.js";
910
import {DownloadCommand} from "./commands/DownloadCommand.js";
1011
import {BuildCommand} from "./commands/BuildCommand.js";
1112
import {OnPostInstallCommand} from "./commands/OnPostInstallCommand.js";
@@ -19,6 +20,7 @@ const packageJson = fs.readJSONSync(path.join(__dirname, "..", "..", "package.js
1920
const yarg = yargs(hideBin(process.argv));
2021

2122
yarg
23+
.scriptName(cliBinName)
2224
.usage("Usage: $0 <command> [options]")
2325
.command(DownloadCommand)
2426
.command(BuildCommand)

src/cli/commands/ChatCommand.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import {CommandModule} from "yargs";
55
import chalk from "chalk";
66
import fs from "fs-extra";
77
import withOra from "../../utils/withOra.js";
8-
import {defaultChatSystemPrompt} from "../../config.js";
8+
import {chatCommandHistoryFilePath, defaultChatSystemPrompt} from "../../config.js";
99
import {LlamaChatPromptWrapper} from "../../chatWrappers/LlamaChatPromptWrapper.js";
1010
import {GeneralChatPromptWrapper} from "../../chatWrappers/GeneralChatPromptWrapper.js";
1111
import {ChatMLChatPromptWrapper} from "../../chatWrappers/ChatMLChatPromptWrapper.js";
1212
import {getChatWrapperByBos} from "../../chatWrappers/createChatWrapperByBos.js";
1313
import {ChatPromptWrapper} from "../../ChatPromptWrapper.js";
1414
import {FalconChatPromptWrapper} from "../../chatWrappers/FalconChatPromptWrapper.js";
1515
import {getIsInDocumentationMode} from "../../state.js";
16+
import {ReplHistory} from "../../utils/ReplHistory.js";
1617
import type {LlamaGrammar} from "../../llamaEvaluator/LlamaGrammar.js";
1718

1819
const modelWrappers = ["auto", "general", "llamaChat", "chatML", "falconChat"] as const;
@@ -36,7 +37,8 @@ type ChatCommand = {
3637
penalizeRepeatingNewLine: boolean,
3738
repeatFrequencyPenalty?: number,
3839
repeatPresencePenalty?: number,
39-
maxTokens: number
40+
maxTokens: number,
41+
noHistory: boolean
4042
};
4143

4244
export const ChatCommand: CommandModule<object, ChatCommand> = {
@@ -176,19 +178,26 @@ export const ChatCommand: CommandModule<object, ChatCommand> = {
176178
default: 0,
177179
description: "Maximum number of tokens to generate in responses. Set to `0` to disable. Set to `-1` to set to the context size",
178180
group: "Optional:"
181+
})
182+
.option("noHistory", {
183+
alias: "nh",
184+
type: "boolean",
185+
default: false,
186+
description: "Don't load or save chat history",
187+
group: "Optional:"
179188
});
180189
},
181190
async handler({
182191
model, systemInfo, systemPrompt, prompt, wrapper, contextSize,
183192
grammar, jsonSchemaGrammarFile, threads, temperature, topK, topP,
184193
gpuLayers, repeatPenalty, lastTokensRepeatPenalty, penalizeRepeatingNewLine,
185-
repeatFrequencyPenalty, repeatPresencePenalty, maxTokens
194+
repeatFrequencyPenalty, repeatPresencePenalty, maxTokens, noHistory
186195
}) {
187196
try {
188197
await RunChat({
189198
model, systemInfo, systemPrompt, prompt, wrapper, contextSize, grammar, jsonSchemaGrammarFile, threads, temperature, topK,
190199
topP, gpuLayers, lastTokensRepeatPenalty, repeatPenalty, penalizeRepeatingNewLine, repeatFrequencyPenalty,
191-
repeatPresencePenalty, maxTokens
200+
repeatPresencePenalty, maxTokens, noHistory
192201
});
193202
} catch (err) {
194203
console.error(err);
@@ -201,7 +210,7 @@ export const ChatCommand: CommandModule<object, ChatCommand> = {
201210
async function RunChat({
202211
model: modelArg, systemInfo, systemPrompt, prompt, wrapper, contextSize, grammar: grammarArg,
203212
jsonSchemaGrammarFile: jsonSchemaGrammarFilePath, threads, temperature, topK, topP, gpuLayers, lastTokensRepeatPenalty, repeatPenalty,
204-
penalizeRepeatingNewLine, repeatFrequencyPenalty, repeatPresencePenalty, maxTokens
213+
penalizeRepeatingNewLine, repeatFrequencyPenalty, repeatPresencePenalty, maxTokens, noHistory
205214
}: ChatCommand) {
206215
const {LlamaChatSession} = await import("../../llamaEvaluator/LlamaChatSession.js");
207216
const {LlamaModel} = await import("../../llamaEvaluator/LlamaModel.js");
@@ -273,21 +282,32 @@ async function RunChat({
273282
// this is for ora to not interfere with readline
274283
await new Promise(resolve => setTimeout(resolve, 1));
275284

276-
const rl = readline.createInterface({
277-
input: process.stdin,
278-
output: process.stdout
279-
});
285+
const replHistory = await ReplHistory.load(chatCommandHistoryFilePath, !noHistory);
286+
287+
async function getPrompt() {
288+
const rl = readline.createInterface({
289+
input: process.stdin,
290+
output: process.stdout,
291+
history: replHistory.history.slice()
292+
});
293+
294+
const res: string = await new Promise((accept) => rl.question(chalk.yellow("> "), accept));
295+
rl.close();
296+
297+
return res;
298+
}
280299

281300
// eslint-disable-next-line no-constant-condition
282301
while (true) {
283-
const input: string = initialPrompt != null
302+
const input = initialPrompt != null
284303
? initialPrompt
285-
: await new Promise((accept) => rl.question(chalk.yellow("> "), accept));
304+
: await getPrompt();
286305

287306
if (initialPrompt != null) {
288307
console.log(chalk.green("> ") + initialPrompt);
289308
initialPrompt = null;
290-
}
309+
} else
310+
await replHistory.add(input);
291311

292312
if (input === ".exit")
293313
break;

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const llamaBinsGrammarsDirectory = path.join(__dirname, "..", "llama", "g
1818
export const llamaCppDirectory = path.join(llamaDirectory, "llama.cpp");
1919
export const llamaCppGrammarsDirectory = path.join(llamaDirectory, "llama.cpp", "grammars");
2020
export const tempDownloadDirectory = path.join(os.tmpdir(), "node-llama-cpp", uuid.v4());
21+
export const chatCommandHistoryFilePath = path.join(os.homedir(), ".node-llama-cpp.chat_repl_history");
2122
export const usedBinFlagJsonPath = path.join(llamaDirectory, "usedBin.json");
2223
export const binariesGithubReleasePath = path.join(llamaDirectory, "binariesGithubRelease.json");
2324
export const currentReleaseGitBundlePath = path.join(llamaDirectory, "gitRelease.bundle");

src/utils/ReplHistory.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import fs from "fs-extra";
2+
import {withLock} from "./withLock.js";
3+
4+
type ReplyHistoryFile = {
5+
history: string[]
6+
};
7+
8+
const emptyHistory: ReplyHistoryFile = {
9+
history: []
10+
};
11+
12+
export class ReplHistory {
13+
private readonly _filePath: string | null;
14+
private _fileContent: ReplyHistoryFile;
15+
16+
private constructor(filePath: string | null, fileContent: ReplyHistoryFile) {
17+
this._filePath = filePath;
18+
this._fileContent = fileContent;
19+
}
20+
21+
public async add(line: string) {
22+
if (this._filePath == null) {
23+
this._fileContent = this._addItemToHistory(line, this._fileContent);
24+
return;
25+
}
26+
27+
await withLock(this, "file", async () => {
28+
try {
29+
const json = parseReplJsonfile(await fs.readJSON(this._filePath!));
30+
this._fileContent = this._addItemToHistory(line, json);
31+
32+
await fs.writeJSON(this._filePath!, this._fileContent, {
33+
spaces: 4
34+
});
35+
} catch (err) {}
36+
});
37+
}
38+
39+
public get history(): readonly string[] {
40+
return this._fileContent.history;
41+
}
42+
43+
private _addItemToHistory(item: string, fileContent: ReplyHistoryFile) {
44+
const newHistory = fileContent.history.slice();
45+
const currentItemIndex = newHistory.indexOf(item);
46+
47+
if (currentItemIndex !== -1)
48+
newHistory.splice(currentItemIndex, 1);
49+
50+
newHistory.unshift(item);
51+
52+
return {
53+
...fileContent,
54+
history: newHistory
55+
};
56+
}
57+
58+
public static async load(filePath: string, saveAndLoadHistory: boolean = true) {
59+
if (!saveAndLoadHistory)
60+
return new ReplHistory(null, {
61+
history: []
62+
});
63+
64+
try {
65+
if (!(await fs.pathExists(filePath)))
66+
await fs.writeJSON(filePath, emptyHistory, {
67+
spaces: 4
68+
});
69+
70+
const json = parseReplJsonfile(await fs.readJSON(filePath));
71+
return new ReplHistory(filePath, json);
72+
} catch (err) {
73+
return new ReplHistory(null, {
74+
history: []
75+
});
76+
}
77+
}
78+
}
79+
80+
function parseReplJsonfile(file: unknown): ReplyHistoryFile {
81+
if (typeof file !== "object" || file == null || !("history" in file) || !(file.history instanceof Array) || file.history.some((item) => typeof item !== "string"))
82+
throw new Error("Invalid ReplyHistory file");
83+
84+
return file as ReplyHistoryFile;
85+
}

0 commit comments

Comments
 (0)