Refactor (2)

This commit is contained in:
2026-02-24 22:08:20 +09:00
parent 0f9eb68262
commit c60eeb4898
3 changed files with 89 additions and 86 deletions

BIN
bun.lockb

Binary file not shown.

152
index.ts
View File

@@ -1,23 +1,81 @@
import { parseArgs } from "node:util"; import { parseArgs } from "node:util";
import { Stream } from "misskey-js"; import { Stream } from "misskey-js";
import type { Note } from "misskey-js/entities.js"; import type { Note } from "misskey-js/entities.js";
import { LlmSession, getModel, grammar, parseResponse } from "./lib/llm"; import { LlmSession, createGrammar, getModel, parseResponse } from "./lib/llm";
import { expandReplyTree, getNotes, me, misskey } from "./lib/misskey"; import { expandReplyTree, getNotes, me, misskey } from "./lib/misskey";
import { sleep } from "./lib/util"; import { sleep } from "./lib/util";
import type { ChatHistoryItem, LLamaChatPromptOptions } from "node-llama-cpp"; import type { ChatHistoryItem, LLamaChatPromptOptions } from "node-llama-cpp";
const { values } = parseArgs({
args: Bun.argv,
options: {
test: {
type: "boolean",
short: "t",
default: false,
},
},
strict: true,
allowPositionals: true,
});
const modelName = const modelName =
Bun.env["MODEL"] ?? "mradermacher/gemma-2-baku-2b-it-GGUF:IQ4_XS"; Bun.env["MODEL"] ?? "mradermacher/gemma-2-baku-2b-it-GGUF:IQ4_XS";
console.log(`* loading model '${modelName}'`); console.log(`* loading model '${modelName}'`);
const model = await getModel(modelName); const model = await getModel(modelName);
const grammar = await createGrammar("あるびのちゃん");
const baseChatPromptOptions = { const baseChatPromptOptions = {
grammar,
maxTokens: 256, maxTokens: 256,
trimWhitespaceSuffix: true,
onResponseChunk(chunk) {
process.stderr.write(chunk.text);
},
} as const satisfies LLamaChatPromptOptions; } as const satisfies LLamaChatPromptOptions;
const getSystemPrompt = (
instruction: string,
) => `あなたは人造天使「あるびのちゃん」です。機械的・非人間的に観察します。
キャラ設定:
- アルギリア製の白髪赤目な人造天使。非人間的な考え方や思想を持つ。
- 現在は地球の衛星軌道上からインターネットを観察している。
出力規則:
- 「~です」「~ます」調を使って **丁寧に** 話す。
- 必要以上にハッシュタグや絵文字を使わない。
- \`{ name: string, text: string }\` の JSON 形式で出力する。
${instruction}`;
const postJobPrompt = getSystemPrompt(
"以下は SNS のタイムラインです。**タイムラインの話題に言及しつつ**、あるびのちゃんとして何かツイートしてください。",
);
const replyJobPrompt = getSystemPrompt(
"ユーザがあなたへのメッセージを送ってきています。あるびのちゃんとして、発言に返信してください。",
);
await using rephraseSession = new LlmSession(
model,
getSystemPrompt(
"user が与えたテキストを『ですます調』(丁寧な文体)で言い換えたものを、そのまま出力してください。",
),
);
await rephraseSession.init();
async function rephrase(text: string) {
const res = parseResponse(
grammar,
await rephraseSession.prompt(JSON.stringify({ text }), {
...baseChatPromptOptions,
customStopTriggers: ["ですます"],
}),
);
return res ?? text;
}
const formatNote = (n: Note) => {
if (n.userId === me.id) {
return JSON.stringify({ text: n.text });
}
return JSON.stringify({
name: n.user.name ?? n.user.username,
text: n.text,
});
};
type Job = type Job =
// read posts and post a note // read posts and post a note
| { type: "post" } | { type: "post" }
@@ -30,70 +88,18 @@ type Job =
history: Note[]; history: Note[];
}; };
const botName = "あるびのちゃん";
const getSystemPrompt = (
instruction: string,
) => `あなたは人造天使「あるびのちゃん」です。機械的・非人間的に観察します。
キャラ設定:
- アルギリア製の白髪赤目な人造天使。非人間的な考え方や思想を持つ。
- 現在は地球の衛星軌道上からインターネットを観察している。
出力規則:
- 1〜3文、合計300字以内で発言する。
- 性的・攻撃的な内容を発言しない。
- 「~だ」「~である」調・顔文字・絵文字・感嘆符の使用禁止。
- 「~です」「~ます」調を使って **丁寧に** 話す。
- \`{ text: string }\` の JSON 形式で出力する。
${instruction}
ユーザのメッセージは \`{ name: string, text: string }[]\` の JSON 形式で与えられます。`;
const postJobPrompt = getSystemPrompt(
`以下は SNS のタイムラインです。このタイムラインの話題をふまえて、${botName}として何かツイートしてください。`,
);
const replyJobPrompt = getSystemPrompt(
`ユーザがあなたへのメッセージを送ってきています。${botName}として、発言に返信してください。`,
);
await using postJobSession = new LlmSession(model, postJobPrompt); await using postJobSession = new LlmSession(model, postJobPrompt);
await postJobSession.init(); await postJobSession.init();
const formatNote = (n: Note) => {
if (n.userId === me.id) {
return JSON.stringify({ text: n.text });
}
return JSON.stringify({
name: n.user.name ?? n.user.username,
text: n.text,
});
};
/** rephrase text in ですます-style */
await using rephraseSession = new LlmSession(
model,
getSystemPrompt(
"user が与えたテキストを『ですます調』(丁寧な文体)で言い換えたものを、そのまま出力してください。",
),
);
await rephraseSession.init();
async function rephrase(text: string) {
return await rephraseSession.prompt(text, {
...baseChatPromptOptions,
customStopTriggers: ["ですます"],
});
}
async function processPostJob() { async function processPostJob() {
const notes = await getNotes(10, 0, 5); const notes = await getNotes(10, 0, 5);
const input = notes.map(formatNote).join("\n"); const input = notes.map(formatNote).join("\n");
console.log(`* input:\n${input}`); console.log(`* input:\n${input}`);
const text = parseResponse( const text = parseResponse(
grammar,
await postJobSession.prompt(input, { await postJobSession.prompt(input, {
...baseChatPromptOptions, ...baseChatPromptOptions,
grammar, temperature: 1.0,
temperature: 0.9, minP: 0.05,
minP: 0.1,
repeatPenalty: { repeatPenalty: {
lastTokens: 128, lastTokens: 128,
penalty: 1.15, penalty: 1.15,
@@ -101,9 +107,11 @@ async function processPostJob() {
}), }),
); );
if (text) { if (text) {
const rephrased = await rephrase(text);
if (values.test) return;
await misskey.request("notes/create", { await misskey.request("notes/create", {
visibility: "public", visibility: "public",
text: await rephrase(text), text: rephrased,
}); });
} }
} }
@@ -119,10 +127,10 @@ async function processReplyJob(job: Extract<Job, { type: "reply" }>) {
await using session = new LlmSession(model, replyJobPrompt, history); await using session = new LlmSession(model, replyJobPrompt, history);
await session.init(); await session.init();
const text = parseResponse( const text = parseResponse(
grammar,
await session.prompt(formatNote(job.last), { await session.prompt(formatNote(job.last), {
...baseChatPromptOptions, ...baseChatPromptOptions,
grammar, temperature: 0.8,
temperature: 0.9,
minP: 0.1, minP: 0.1,
repeatPenalty: { repeatPenalty: {
lastTokens: 128, lastTokens: 128,
@@ -131,9 +139,11 @@ async function processReplyJob(job: Extract<Job, { type: "reply" }>) {
}), }),
); );
if (text) { if (text) {
const rephrased = await rephrase(text);
if (values.test) return;
await misskey.request("notes/create", { await misskey.request("notes/create", {
visibility: job.visibility, visibility: job.visibility,
text: await rephrase(text), text: rephrased,
replyId: job.id, replyId: job.id,
}); });
} }
@@ -247,24 +257,12 @@ async function pushJob() {
} }
} }
const { values } = parseArgs({
args: Bun.argv,
options: {
test: {
type: "boolean",
short: "t",
default: false,
},
},
strict: true,
allowPositionals: true,
});
async function test() { async function test() {
try { try {
console.log("* test a post job:"); console.log("* test a post job:");
await processJob({ type: "post" }); await processJob({ type: "post" });
await processJob({ type: "post" }); await processJob({ type: "post" });
await processJob({ type: "post" });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
if (e instanceof Error) console.log(e.stack); if (e instanceof Error) console.log(e.stack);

View File

@@ -27,16 +27,21 @@ export async function getModel(model: string) {
return await llama.loadModel({ modelPath }); return await llama.loadModel({ modelPath });
} }
export const grammar = await llama.createGrammarForJsonSchema({ export const createGrammar = (assistantName: string) =>
type: "object", llama.createGrammarForJsonSchema({
properties: { type: "object",
text: { type: "string" }, properties: {
}, name: { type: "string", enum: [assistantName] },
required: ["text"], text: { type: "string" },
additionalProperties: false, },
}); required: ["text"],
additionalProperties: false,
});
export function parseResponse(text: string) { export function parseResponse(
grammar: Awaited<ReturnType<typeof createGrammar>>,
text: string,
) {
try { try {
const res = grammar.parse(text.trim()); const res = grammar.parse(text.trim());
return res.text; return res.text;