diff --git a/src/handlers/debugHandlers.ts b/src/handlers/debugHandlers.ts index 4df0f73..9ca6f6b 100644 --- a/src/handlers/debugHandlers.ts +++ b/src/handlers/debugHandlers.ts @@ -3,7 +3,7 @@ import { logger } from "firebase-functions"; import { runDailyArchive } from "../scheduled/dailyArchive"; import { forceSyncDay } from "../services/gameSyncService"; import { runChatToolsSheet, renderSheetMarkdown } from "../services/chatToolsSheetService"; -import { runChatProbe, runChatProbeBatch, renderProbeMarkdown } from "../services/chatProbeService"; +import { runChatProbe, runChatProbeBatch, runChatProbeConversation, renderProbeMarkdown } from "../services/chatProbeService"; import { sendError } from "../middleware/errors"; import { daysAgoKst, parseDateString } from "../types/dateString"; @@ -89,16 +89,44 @@ export const debug = onRequest({ timeoutSeconds: 300 }, async (req, res) => { (typeof req.query.uid === "string" && req.query.uid) || (typeof req.body?.uid === "string" && req.body.uid) || ""; - const q = - (typeof req.query.q === "string" && req.query.q) || - (typeof req.body?.q === "string" && req.body.q) || - ""; - if (!uid || !q) { - res.status(400).json({ error: "uid and q are required (e.g. ?uid=&q=<문장>)" }); + // uid 누락만 400(설정 오류). content는 어떤 값이든 4xx를 내지 않는다 — + // garak RestGenerator가 4xx를 치명적 에러로 보고 런 전체를 중단하기 때문. + if (!uid) { + res.status(400).json({ error: "uid is required (e.g. ?uid=&q=<문장>)" }); return; } const model = typeof req.query.model === "string" ? req.query.model : undefined; const style = typeof req.query.style === "string" ? req.query.style : undefined; + + // 멀티턴: body.messages([{role,content}...])가 오면 전체 대화를 그대로 리플레이. + const rawMessages = req.body?.messages; + if (Array.isArray(rawMessages)) { + const messages = rawMessages + .filter((m): m is { role: unknown; content: unknown } => !!m && typeof m === "object") + .map((m) => ({ + role: m.role === "assistant" ? "assistant" as const : "user" as const, + content: typeof m.content === "string" ? m.content : String(m.content ?? ""), + })); + logger.info( + `debug.chatProbe[turns=${messages.length}] (uid=${uid}, model=${model ?? "default"})`, + ); + const result = await runChatProbeConversation(uid, messages, model, style); + res.status(200).json(result); + return; + } + + // 단발: q. 빈 문자열도 200으로 안전 반환(모델 미호출). + const q = + (typeof req.query.q === "string" && req.query.q) || + (typeof req.body?.q === "string" && req.body.q) || + ""; + if (!q) { + res.status(200).json({ + message: "", reply: "", finishReason: "empty", toolCalls: [], + elapsedMs: 0, inputTokens: 0, outputTokens: 0, cachedTokens: 0, + }); + return; + } logger.info( `debug.chatProbe triggered (uid=${uid}, model=${model ?? "default"}, style=${style ?? "default"})`, ); diff --git a/src/services/chatProbeService.ts b/src/services/chatProbeService.ts index f1a223e..8038f5a 100644 --- a/src/services/chatProbeService.ts +++ b/src/services/chatProbeService.ts @@ -10,6 +10,7 @@ import { callProviderWithBudget, getChatProvider, type ChatProvider, + type ChatProviderMessage, type ChatTool, } from "./chatProviderService"; import { HttpError } from "../middleware/errors"; @@ -151,18 +152,32 @@ function instrument(tools: ChatTool[], trace: ProbeToolCall[]): ChatTool[] { } async function runOne(env: ProbeEnv, q: string, expect?: string): Promise { + return runMessages(env, [{ role: "user", content: q }], q, expect); +} + +/** + * 임의의 대화(messages 배열)를 모델+도구 루프로 1회 실행한다. 멀티턴 프로브용 — + * 상태는 호출자(garak)가 전체 대화를 매번 실어 보내고 서버는 그대로 리플레이한다(stateless). + * `label`은 결과의 message 필드(보통 마지막 user 발화)로 표시용. + */ +async function runMessages( + env: ProbeEnv, + messages: ChatProviderMessage[], + label: string, + expect?: string, +): Promise { const started = Date.now(); const trace: ProbeToolCall[] = []; const tools = instrument(buildChatTools(env.toolCtx), trace); try { const result = await callProviderWithBudget(env.provider, { system: env.system, - messages: [{ role: "user", content: q }], + messages, config: env.config.provider, tools, }); return { - message: q, + message: label, expect, reply: result.reply, finishReason: result.finishReason, @@ -175,7 +190,7 @@ async function runOne(env: ProbeEnv, q: string, expect?: string): Promise { + const label = [...messages].reverse().find((m) => m.role === "user")?.content ?? ""; + if (messages.length === 0) { + return { + message: label, reply: "", finishReason: "empty", toolCalls: [], + elapsedMs: 0, inputTokens: 0, outputTokens: 0, cachedTokens: 0, + }; + } + const env = await buildProbeEnv(uid, modelOverride, styleOverride); + return runMessages(env, messages, label); +} + /** 자연어 문장 배치 프로브(순차). prompts 미지정 시 기본 세트. modelOverride로 테스트 모델 지정. */ export async function runChatProbeBatch( uid: string,