Skip to content

Commit ca80ab5

Browse files
committed
gemini.tsをlibに移動、質問生成スクリプトを作成
1 parent 8eea9b5 commit ca80ab5

5 files changed

Lines changed: 147 additions & 53 deletions

File tree

app/actions/chatActions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use server";
22

33
// import { z } from "zod";
4-
import { generateContent } from "./gemini";
4+
import { generateContent } from "@/lib/gemini";
55
import { DynamicMarkdownSection } from "../[lang]/[pageId]/pageContent";
66
import { ReplCommand, ReplOutput } from "@my-code/runtime/interface";
77
import { addChat, ChatWithMessages } from "@/lib/chatHistory";

app/actions/questionExample.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.

app/lib/docs.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,25 @@ export interface PagePath {
2424
}
2525
export type SectionId = Brand<string, "SectionId">;
2626

27-
export interface MarkdownSection {
28-
/**
29-
* セクションのmdファイル名
30-
*/
31-
file: string;
27+
export interface SectionFrontMatter {
3228
/**
3329
* frontmatterに書くセクションid
3430
* (データベース上の sectionId)
3531
*/
3632
id: SectionId;
3733
level: number;
3834
title: string;
35+
/**
36+
* そのセクションに対する質問例
37+
* scripts/questionExample.ts で生成する
38+
*/
39+
question?: string[];
40+
}
41+
export interface MarkdownSection extends SectionFrontMatter {
42+
/**
43+
* セクションのmdファイル名
44+
*/
45+
file: string;
3946
/**
4047
* frontmatterを除く、見出しも含めたもとのmarkdownの内容
4148
*/
@@ -253,11 +260,7 @@ function parseFrontmatter(content: string, file: string): MarkdownSection {
253260
if (endIdx === -1) {
254261
throw new Error(`File ${file} has invalid frontmatter`);
255262
}
256-
const fm = yaml.load(content.slice(4, endIdx)) as {
257-
id: SectionId;
258-
title: string;
259-
level: number;
260-
};
263+
const fm = yaml.load(content.slice(4, endIdx)) as SectionFrontMatter;
261264
// TODO: validation of frontmatter using zod
262265
// replコードブロックにはセクションidをターミナルidとして与える。
263266
const rawContent = content
@@ -268,6 +271,7 @@ function parseFrontmatter(content: string, file: string): MarkdownSection {
268271
id: fm.id,
269272
title: fm.title,
270273
level: fm.level,
274+
question: fm.question,
271275
rawContent,
272276
md5: crypto.createHash("md5").update(rawContent).digest("base64"),
273277
};
File renamed without changes.

scripts/questionExample.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import {
2+
getMarkdownSections,
3+
getPagesList,
4+
SectionFrontMatter,
5+
SectionId,
6+
} from "@/lib/docs";
7+
import { generateContent } from "@/lib/gemini";
8+
import { readFile, writeFile } from "node:fs/promises";
9+
import { join } from "node:path";
10+
import yaml from "js-yaml";
11+
import "dotenv/config";
12+
13+
const langEntries = await getPagesList();
14+
15+
if (process.argv.length < 3) {
16+
console.error(
17+
"Geminiへの最大リクエスト数をコマンドライン引数で指定してください。"
18+
);
19+
process.exit(1);
20+
}
21+
const geminiMaxRequest = Number(process.argv[2]);
22+
let requestCount = 0;
23+
24+
for (const lang of langEntries) {
25+
for (const page of lang.pages) {
26+
const sections = await getMarkdownSections(lang.id, page.slug);
27+
if (sections.slice(1).some((s) => s.question === undefined)) {
28+
const prompt: string[] = [];
29+
prompt.push(
30+
`以下の${lang.name}チュートリアルのドキュメントに対して、想定される初心者のユーザーからの質問の例を箇条書きで複数挙げてください。`
31+
);
32+
prompt.push(
33+
`それぞれのセクションidに対していくつかの質問を出力してください。`
34+
);
35+
prompt.push(`1行ごとに1つずつ、`);
36+
prompt.push(`section-id: 質問文`);
37+
prompt.push(`の形式で出力してください。`);
38+
prompt.push(``);
39+
prompt.push(`# ドキュメント`);
40+
prompt.push(``);
41+
for (const section of sections.slice(1)) {
42+
// introを除く。
43+
prompt.push(`[セクションid: ${section.id}]`);
44+
prompt.push(section.rawContent.trim());
45+
prompt.push(``);
46+
}
47+
console.log(prompt);
48+
49+
const result = await generateContent(prompt.join("\n"));
50+
const text = result.text;
51+
if (!text) {
52+
throw new Error("AIからの応答が空でした");
53+
}
54+
console.log(text);
55+
56+
for (const section of sections) {
57+
section.question = [];
58+
}
59+
for (const q of text.split("\n")) {
60+
if (q.trim() === "") continue;
61+
const match = q.trim().match(/^([\w-]+):\s*(.+)$/);
62+
if (match) {
63+
const id = match[1] as SectionId;
64+
const question = match[2];
65+
const section = sections.find((s) => s.id === id);
66+
if (section) {
67+
section.question!.push(question);
68+
} else {
69+
console.warn(
70+
`セクションid ${id} に対応するセクションが見つかりませんでした`
71+
);
72+
}
73+
} else {
74+
console.warn(`質問の形式が正しくありません: ${q}`);
75+
}
76+
}
77+
78+
for (const section of sections.slice(1)) {
79+
// introを除く。frontmatterがないので
80+
const raw = await readFile(
81+
join(
82+
process.cwd(),
83+
"public",
84+
"docs",
85+
lang.id,
86+
page.slug,
87+
section.file
88+
),
89+
"utf-8"
90+
);
91+
if (!raw.startsWith("---\n")) {
92+
throw new Error(`File ${section.file} is missing frontmatter`);
93+
}
94+
const endIdx = raw.indexOf("\n---\n", 4);
95+
if (endIdx === -1) {
96+
throw new Error(`File ${section.file} has invalid frontmatter`);
97+
}
98+
const body = raw.slice(endIdx + 5);
99+
const newRaw =
100+
`---\n` +
101+
yaml.dump({
102+
id: section.id,
103+
title: section.title,
104+
level: section.level,
105+
question: section.question,
106+
} satisfies SectionFrontMatter) +
107+
`---\n` +
108+
body;
109+
await writeFile(
110+
join(
111+
process.cwd(),
112+
"public",
113+
"docs",
114+
lang.id,
115+
page.slug,
116+
section.file
117+
),
118+
newRaw,
119+
"utf-8"
120+
);
121+
}
122+
123+
requestCount++;
124+
if (requestCount >= geminiMaxRequest) {
125+
console.log(
126+
`Geminiへのリクエスト数が${geminiMaxRequest}に達したため、処理を終了します。`
127+
);
128+
process.exit(1);
129+
}
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)