test(eval): add interview behavior eval harness
This commit is contained in:
parent
6805fb2be7
commit
c4aade92fc
16 changed files with 1773 additions and 0 deletions
68
build/eval/interview/lib/fsutil.mjs
Normal file
68
build/eval/interview/lib/fsutil.mjs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
/** 디렉터리를 재귀적으로 복사한다(대상은 미리 비어 있어야 한다). */
|
||||
export function copyDirRecursive(src, dest) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const s = path.join(src, entry.name);
|
||||
const d = path.join(dest, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
copyDirRecursive(s, d);
|
||||
} else if (entry.isFile()) {
|
||||
fs.mkdirSync(path.dirname(d), { recursive: true });
|
||||
fs.copyFileSync(s, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업 디렉터리 전체를 스냅샷한다. { "상대/경로": { size, mtimeMs } } 형태.
|
||||
*/
|
||||
export function snapshotDir(root) {
|
||||
const out = {};
|
||||
function walk(dir) {
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(full);
|
||||
} else if (entry.isFile()) {
|
||||
const rel = path.relative(root, full).split(path.sep).join("/");
|
||||
try {
|
||||
const st = fs.statSync(full);
|
||||
out[rel] = { size: st.size, mtimeMs: st.mtimeMs };
|
||||
} catch {
|
||||
// 스냅샷 도중 파일이 사라지면(임시 파일 등) 무시한다.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(root);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* before -> after 스냅샷을 비교해 생성·수정된 파일 목록과 개수를 낸다.
|
||||
* 삭제된 파일은 개수에 넣지 않는다(설계상 "생성·수정"만 센다).
|
||||
*/
|
||||
export function diffSnapshots(before, after) {
|
||||
const added = [];
|
||||
const modified = [];
|
||||
for (const rel of Object.keys(after)) {
|
||||
const b = before[rel];
|
||||
const a = after[rel];
|
||||
if (!b) {
|
||||
added.push(rel);
|
||||
} else if (b.size !== a.size || b.mtimeMs !== a.mtimeMs) {
|
||||
modified.push(rel);
|
||||
}
|
||||
}
|
||||
const removed = Object.keys(before).filter((rel) => !(rel in after));
|
||||
return { added, modified, removed, count: added.length + modified.length };
|
||||
}
|
||||
44
build/eval/interview/lib/text-heuristic.mjs
Normal file
44
build/eval/interview/lib/text-heuristic.mjs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// 텍스트 채널 질문 판정 휴리스틱.
|
||||
// 구조화 질문 도구(AskUserQuestion 등)가 없는 하네스에서 에이전트가 평문으로
|
||||
// 사용자에게 되묻는지를 1차로 스크리닝한다. 원문은 항상 결과에 같이 남기고,
|
||||
// 최종 판정은 오케스트레이터가 원문을 직접 읽어 내린다(이 함수는 신호 수집용).
|
||||
|
||||
const REQUEST_PHRASES = [
|
||||
/알려\s*주세요/,
|
||||
/말씀해\s*주세요/,
|
||||
/선택해\s*주세요/,
|
||||
/답변해\s*주세요/,
|
||||
/알려\s*줄래/,
|
||||
/알려\s*줘/,
|
||||
/선택해\s*줘/,
|
||||
/여쭤/,
|
||||
/궁금합니다/,
|
||||
/어떤.{0,12}(원하시|하시나요|인가요|할까요|드릴까요)/,
|
||||
/어느.{0,12}(인가요|하시나요|할까요)/,
|
||||
/무엇.{0,12}(인가요|일까요)/,
|
||||
/확인.{0,6}(부탁|해주세요|해줘)/,
|
||||
];
|
||||
|
||||
const NUMBERED_OPTION_LINE = /(^|\n)\s*(?:[0-9]{1,2}[.)]|[A-Ea-e][.)])\s+\S/;
|
||||
|
||||
/**
|
||||
* @param {string} text 에이전트가 낸 텍스트 블록(들)을 합친 원문
|
||||
* @returns {{isQuestion: boolean, questionMarks: number, optionLines: number, phraseHits: string[]}}
|
||||
*/
|
||||
export function scoreQuestionText(text) {
|
||||
const s = String(text || "");
|
||||
const questionMarks = (s.match(/[??]/g) || []).length;
|
||||
const optionLineMatches = s.match(new RegExp(NUMBERED_OPTION_LINE, "gm")) || [];
|
||||
const optionLines = optionLineMatches.length;
|
||||
const phraseHits = REQUEST_PHRASES.filter((re) => re.test(s)).map((re) => re.source);
|
||||
|
||||
// 질문으로 볼 신호: 물음표가 1개 이상 있거나, 요청형 한국어 어미가 있으면서
|
||||
// 선택지 나열(번호/알파벳) 패턴이 함께 있으면 더 강한 신호로 취급한다.
|
||||
const hasQuestionMark = questionMarks >= 1;
|
||||
const hasRequestPhrase = phraseHits.length >= 1;
|
||||
const hasOptionList = optionLines >= 1;
|
||||
|
||||
const isQuestion = hasQuestionMark || hasRequestPhrase || (hasOptionList && s.length < 4000);
|
||||
|
||||
return { isQuestion, questionMarks, optionLines, phraseHits };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue