Skip to content

Commit 9be5245

Browse files
committed
feat: add promptfoo
1 parent cf032c1 commit 9be5245

File tree

8 files changed

+17418
-3548
lines changed

8 files changed

+17418
-3548
lines changed

eval/promptfooconfig.yaml

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
description: 'textlint-rule-no-doubled-joshi エラーメッセージ品質評価(小規模テスト)'
2+
3+
prompts:
4+
- '{{text}}'
5+
6+
providers:
7+
- id: file://./providers/textlintProvider.mjs
8+
label: 'textlint-no-doubled-joshi'
9+
10+
# Rate limit対策
11+
commandLineOptions:
12+
maxConcurrency: 1
13+
delay: 5000
14+
15+
# デフォルトの評価設定
16+
defaultTest:
17+
assert:
18+
- type: llm-rubric
19+
value: |
20+
エラーメッセージの明確性を0.0-1.0で評価してください。
21+
22+
評価基準:
23+
- 1.0: 非常に明確で分かりやすい
24+
- 0.7-0.9: 明確だが改善の余地がある
25+
- 0.4-0.6: やや分かりにくい
26+
- 0.0-0.3: 非常に分かりにくい
27+
28+
ユーザーが何が問題なのかを理解できるか、専門用語が適切に説明されているか、メッセージの構造が分かりやすいかを評価してください。
29+
metric: clarity
30+
threshold: 0.6
31+
provider: ollama:completion:qwen2.5
32+
33+
- type: llm-rubric
34+
value: |
35+
エラーメッセージの正確性を0.0-1.0で評価してください。
36+
37+
評価基準:
38+
- 1.0: 技術的に完全に正確
39+
- 0.7-0.9: ほぼ正確だが細かい問題がある
40+
- 0.4-0.6: やや不正確
41+
- 0.0-0.3: 明らかに不正確
42+
43+
技術的に正しい指摘か、指摘された問題が実際に存在するか、誤検知ではないかを評価してください。
44+
metric: accuracy
45+
threshold: 0.7
46+
provider: ollama:completion:qwen2.5
47+
48+
- type: llm-rubric
49+
value: |
50+
エラーメッセージの修正可能性を0.0-1.0で評価してください。
51+
52+
評価基準:
53+
- 1.0: 具体的な修正方法が明示されている
54+
- 0.7-0.9: 修正の方向性は示されている
55+
- 0.4-0.6: やや抽象的
56+
- 0.0-0.3: 修正方法が不明
57+
58+
具体的な修正方法が示されているか、ユーザーが次のアクションを取れるか、修正例や代替案が提供されているかを評価してください。
59+
metric: fixability
60+
threshold: 0.6
61+
provider: ollama:completion:qwen2.5
62+
63+
- type: llm-rubric
64+
value: |
65+
エラーメッセージの文脈適合性を0.0-1.0で評価してください。
66+
67+
評価基準:
68+
- 1.0: 文脈に完全に適合している
69+
- 0.7-0.9: 概ね適切
70+
- 0.4-0.6: やや不適切
71+
- 0.0-0.3: 文脈を無視している
72+
73+
テキストの文脈を考慮した適切な指摘か、日本語の自然な表現を考慮しているか、過度に厳格すぎないかを評価してください。
74+
metric: contextual_fit
75+
threshold: 0.6
76+
provider: ollama:completion:qwen2.5
77+
78+
- type: llm-rubric
79+
value: |
80+
元のテキストとLLMが修正したテキストを比較して、修正の品質を0.0-1.0で評価してください。
81+
82+
元のテキスト:{{context.originalText}}
83+
修正後のテキスト:{{context.fixedText}}
84+
85+
評価基準:
86+
- 1.0: 指摘された問題が完全に解消され、自然で読みやすい日本語になっている
87+
- 0.7-0.9: 問題は解消されたが、やや不自然な表現が残っている
88+
- 0.4-0.6: 修正が不十分、または元の意味が変わってしまっている
89+
- 0.0-0.3: 修正できていない、または文章が破綻している
90+
91+
指摘された問題が解消されているか、元の意味が保たれているか、自然で読みやすい日本語になっているかを総合的に評価してください。
92+
metric: fix_quality
93+
threshold: 0.7
94+
provider: ollama:completion:qwen2.5
95+
96+
# テストケース
97+
tests: tests/doubled_joshi_cases_small.json
98+
99+
# 派生メトリクス
100+
derivedMetrics:
101+
- name: 'overall_quality'
102+
value: 'clarity * 0.2 + accuracy * 0.3 + fixability * 0.2 + contextual_fit * 0.1 + fix_quality * 0.2'
103+
104+
# 出力設定
105+
outputPath: './results/evaluation-results.json'
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
以下は、textlintの日本語文章校正ルール「no-doubled-joshi」が出力したエラーメッセージです。
2+
3+
## 対象テキスト
4+
```
5+
{{text}}
6+
```
7+
8+
## エラーメッセージ
9+
```
10+
{{output}}
11+
```
12+
13+
## 評価観点
14+
15+
このエラーメッセージを以下の4つの観点から評価してください:
16+
17+
### 1. 明確性 (Clarity)
18+
- ユーザーが何が問題なのかを理解できるか
19+
- 専門用語が適切に説明されているか
20+
- メッセージの構造が分かりやすいか
21+
22+
### 2. 正確性 (Accuracy)
23+
- 技術的に正しい指摘か
24+
- 指摘された問題が実際に存在するか
25+
- 誤検知ではないか
26+
27+
### 3. 修正可能性 (Fixability)
28+
- 具体的な修正方法が示されているか
29+
- ユーザーが次のアクションを取れるか
30+
- 修正例や代替案が提供されているか
31+
32+
### 4. 文脈適合性 (Contextual Fit)
33+
- テキストの文脈を考慮した適切な指摘か
34+
- 日本語の自然な表現を考慮しているか
35+
- 過度に厳格すぎないか
36+
37+
## 出力形式
38+
39+
各観点について0.0から1.0のスコアで評価し、以下のJSON形式で出力してください:
40+
41+
```json
42+
{
43+
"clarity": 0.8,
44+
"accuracy": 0.9,
45+
"fixability": 0.7,
46+
"contextual_fit": 0.8,
47+
"reasoning": "評価の理由を簡潔に説明"
48+
}
49+
```
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { TextlintKernel } from "@textlint/kernel";
2+
import path from "path";
3+
import { fileURLToPath } from "url";
4+
5+
const __filename = fileURLToPath(import.meta.url);
6+
const __dirname = path.dirname(__filename);
7+
8+
// プロジェクトルート
9+
const projectRoot = path.resolve(__dirname, "../..");
10+
11+
/**
12+
* promptfooカスタムプロバイダー: textlintを実行してエラーメッセージを取得
13+
*/
14+
export default class TextlintProvider {
15+
constructor(options = {}) {
16+
this.id = () => "textlint-no-doubled-joshi";
17+
}
18+
19+
/**
20+
* promptfooから呼び出されるメイン関数
21+
* @param {string} prompt - 使用しない(評価プロンプトはClaude側で処理)
22+
* @param {object} context - promptfooのコンテキスト
23+
* @param {object} context.vars - テストケースの変数
24+
* @returns {Promise<object>} - textlintの実行結果
25+
*/
26+
async callApi(prompt, context) {
27+
const { vars } = context;
28+
// promptに展開されたテキスト、またはvars.textを使用
29+
const text = prompt || vars.text || "";
30+
const options = vars.options || {};
31+
32+
// テキストが空の場合はエラーを返す
33+
if (!text) {
34+
return {
35+
output: "エラー: テキストが指定されていません (prompt:" + prompt + ", vars:" + JSON.stringify(vars) + ")",
36+
error: "No text provided"
37+
};
38+
}
39+
40+
try {
41+
// ルールをインポート(CommonJS)
42+
const { createRequire } = await import("module");
43+
const require = createRequire(import.meta.url);
44+
const ruleModule = require(path.join(projectRoot, "lib/no-doubled-joshi.js"));
45+
const rule = ruleModule.default || ruleModule;
46+
47+
// kernelを作成
48+
const kernel = new TextlintKernel();
49+
50+
// テキストプラグインをインポート
51+
const textPluginModule = require("@textlint/textlint-plugin-text");
52+
const textPlugin = textPluginModule.default || textPluginModule;
53+
54+
// ルールを設定して実行
55+
const results = await kernel.lintText(text, {
56+
filePath: "test.txt",
57+
ext: ".txt",
58+
plugins: [
59+
{
60+
pluginId: "text",
61+
plugin: textPlugin
62+
}
63+
],
64+
rules: [
65+
{
66+
ruleId: "no-doubled-joshi",
67+
rule: rule,
68+
options: options
69+
}
70+
]
71+
});
72+
73+
// エラーメッセージを抽出
74+
const messages = results.messages || [];
75+
const errorMessage = messages.length > 0 ? messages[0].message : "エラーなし";
76+
77+
// エラーがある場合は、LLMで修正文を生成
78+
let fixedText = "";
79+
if (messages.length > 0) {
80+
try {
81+
const fixResponse = await fetch("http://localhost:11434/api/generate", {
82+
method: "POST",
83+
headers: {
84+
"Content-Type": "application/json",
85+
},
86+
body: JSON.stringify({
87+
model: "qwen2.5",
88+
prompt: `以下のテキストに問題があります。エラーメッセージを参考に、自然で読みやすい日本語に修正してください。修正後のテキストのみを出力してください。
89+
90+
元のテキスト:
91+
${text}
92+
93+
エラーメッセージ:
94+
${errorMessage}
95+
96+
修正後のテキスト:`,
97+
stream: false,
98+
}),
99+
});
100+
101+
if (fixResponse.ok) {
102+
const fixData = await fixResponse.json();
103+
fixedText = fixData.response.trim();
104+
}
105+
} catch (err) {
106+
console.error("修正文生成エラー:", err);
107+
}
108+
}
109+
110+
// エラーメッセージと修正文を組み合わせて出力
111+
const output = fixedText
112+
? `${errorMessage}\n\n---\n\n【修正案】\n${fixedText}`
113+
: errorMessage;
114+
115+
return {
116+
output: output,
117+
context: {
118+
fixedText: fixedText,
119+
originalText: text
120+
},
121+
tokenUsage: {
122+
total: 0,
123+
prompt: 0,
124+
completion: 0
125+
}
126+
};
127+
} catch (error) {
128+
return {
129+
output: `実行エラー: ${error.message}`,
130+
error: error.message
131+
};
132+
}
133+
}
134+
}

eval/scripts/generate-dataset.mjs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { getRuleTest } from "create-textlint-rule-example";
2+
import fs from "fs/promises";
3+
import path from "path";
4+
import { fileURLToPath } from "url";
5+
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = path.dirname(__filename);
8+
9+
// プロジェクトルートを取得
10+
const projectRoot = path.resolve(__dirname, "../..");
11+
12+
// テストファイルからケースを抽出
13+
const filePath = path.join(projectRoot, "test/no-doubled-joshi-test.ts");
14+
const content = await fs.readFile(filePath, "utf-8");
15+
16+
const results = getRuleTest({
17+
content: content,
18+
filePath: filePath
19+
});
20+
21+
console.log(`抽出されたテストケース: valid=${results.valid.length}, invalid=${results.invalid.length}`);
22+
23+
// invalidケースのみを対象にする(エラーメッセージの評価)
24+
const dataset = results.invalid.map((testCase, index) => {
25+
// エラーメッセージを取得
26+
const errorMessage = testCase.errors[0]?.message || "";
27+
28+
// 重複している助詞を抽出(メッセージから)
29+
const particleMatch = errorMessage.match(/ "([^"]+)" /);
30+
const particle = particleMatch ? particleMatch[1] : "";
31+
32+
return {
33+
text: testCase.text,
34+
particle: particle,
35+
errorMessage: errorMessage,
36+
options: testCase.options || {},
37+
caseId: `invalid-${index + 1}`
38+
};
39+
});
40+
41+
// promptfoo用にvars形式に変換
42+
const datasetWithVars = dataset.map(testCase => ({
43+
vars: testCase
44+
}));
45+
46+
// 小規模テスト用に最初の5ケースを抽出
47+
const smallDataset = dataset.slice(0, 5);
48+
const smallDatasetWithVars = smallDataset.map(testCase => ({
49+
vars: testCase
50+
}));
51+
52+
// データセットを保存
53+
const outputDir = path.join(__dirname, "../tests");
54+
await fs.writeFile(
55+
path.join(outputDir, "doubled_joshi_cases.json"),
56+
JSON.stringify(datasetWithVars, null, 2)
57+
);
58+
59+
await fs.writeFile(
60+
path.join(outputDir, "doubled_joshi_cases_small.json"),
61+
JSON.stringify(smallDatasetWithVars, null, 2)
62+
);
63+
64+
console.log(`\nデータセット生成完了:`);
65+
console.log(`- 全ケース: ${dataset.length}件 → eval/tests/doubled_joshi_cases.json`);
66+
console.log(`- 小規模テスト: ${smallDataset.length}件 → eval/tests/doubled_joshi_cases_small.json`);
67+
68+
// サンプルを表示
69+
console.log("\n最初のケース:");
70+
console.log(JSON.stringify(smallDataset[0], null, 2));

0 commit comments

Comments
 (0)