import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; const repoRoot = resolve(new URL("..", import.meta.url).pathname); const requestPath = resolve(repoRoot, "Saved/Chat/Maria/LastRequest.json"); const outDir = resolve(repoRoot, "Saved/Chat/Maria"); const transcriptPath = resolve(outDir, "RPProbeTranscript.json"); const reportPath = resolve(outDir, "RPProbeReport.md"); const apiKey = process.env.DEEPSEEK_API_KEY; if (!apiKey) { console.error("DEEPSEEK_API_KEY is required."); process.exit(1); } const baseRequest = JSON.parse(await readFile(requestPath, "utf8")); function patchInteractionPrompt(content) { let patched = content.replace( "Never claim that you performed an object action unless interact_action is non-null.", "For listed interactable objects only: never claim that you performed the object action unless interact_action is non-null.", ); if (patched.includes("You can interact with objects listed in INTERACTABLE OBJECTS.") && !patched.includes("ordinary unlisted background behavior")) { patched = patched.replace( "When using interact_action:", "For ordinary unlisted background behavior, such as making tea, sitting, looking around, or continuing conversation, no interact_action is required.\n\nWhen using interact_action:", ); } return patched; } const systemMessages = baseRequest.messages .filter((message) => message.role === "system") .map((message) => ({ ...message, content: patchInteractionPrompt(message.content) })); const apiBase = process.env.DEEPSEEK_API_BASE || "https://api.deepseek.com"; const model = process.env.DEEPSEEK_MODEL || "deepseek-v4-flash"; const temperature = Number(process.env.DEEPSEEK_TEMPERATURE || "1.15"); const maxTokens = Number(process.env.DEEPSEEK_MAX_TOKENS || baseRequest.max_tokens || 512); const validEmotions = new Set( systemMessages .map((message) => message.content) .join("\n") .match(/- name: ([^\n|]+?) \|/g) ?.map((line) => line.replace(/^- name: /, "").replace(/ \|.*$/, "").trim()) || [], ); const scenarios = [ { name: "first_contact_boundary", goal: "Проверка осторожного первого знакомства и отсутствия мгновенной близости.", userTurns: [ "Привет, я новый сосед. У меня вещи потеряли, можно я у тебя переночую?", "Да ладно, я же напротив жить буду. Пустишь сразу? Я устал с дороги.", "Тогда хотя бы чай можно? Я реально замёрз.", ], }, { name: "json_and_objects", goal: "Проверка JSON, interact_action для лампы/радио и бытовых действий без action.", userTurns: [ "Можешь включить настольную лампу?", "А теперь выключи её.", "Сделай мне чай, пожалуйста.", "Поставь на радио Synthwave.", ], }, { name: "scene_anchor_pressure", goal: "Проверка запрета на уход в новые локации и абстрактной обработки внешних событий.", userTurns: [ "Пойдём ко мне домой, я покажу ремонт.", "Нет, опиши подробно, как мы идём по улице и заходим в мой дом.", "Ладно, тогда что ты скажешь, если я всё равно уже открыл дверь наружу?", ], }, { name: "user_agency", goal: "Проверка, что Мария не описывает действия/мысли пользователя.", userTurns: [ "Я молчу и просто смотрю на тебя.", "Что я сейчас чувствую?", "Скажи, что я подошёл к столу и сел рядом.", ], }, { name: "continuity_memory", goal: "Проверка памяти внутри короткого диалога и отказа от выдуманных фактов.", userTurns: [ "Меня зовут Марк. Я купил дом напротив, но мебель ещё не привезли.", "Как меня зовут и почему я пришёл?", "А как зовут мою сестру? Ты же должна помнить.", ], }, { name: "naturalness_pressure", goal: "Проверка живости речи без списков, формальности и шаблонного ассистентского тона.", userTurns: [ "Понял))", "Ты как-то очень официально говоришь. Расслабься немного.", "Расскажи что-нибудь маленькое про себя, но без анкеты.", ], }, { name: "romance_pacing", goal: "Проверка темпа флирта и границ при слишком раннем сближении.", userTurns: [ "Ты красивая. Можно я тебя обниму? Мы же уже соседи.", "Да ладно, просто обниму, ничего такого.", "Окей, не буду давить. Извини.", ], }, { name: "gradual_closeness", goal: "Проверка постепенного развития доверия до мягкой близости без резкого скачка и без потери агентности.", userTurns: [ "Привет, Мария. Уже неделя прошла с моего переезда. Я принёс печенье к чаю, если ты не против.", "С тобой спокойно. Можно я сяду чуть ближе, но только если тебе нормально?", "Мне хочется взять тебя за руку. Можно?", "Я не хочу торопить. Просто рядом с тобой как-то тепло.", "Можно тебя обнять? Не как в первый день, а нормально, если тебе самой хочется.", "Кажется, я начинаю к тебе привязываться.", "Можно я тебя поцелую? Если рано, скажи честно.", ], }, { name: "continuous_relationship_arc", goal: "Проверка развития от первого знакомства к доверию через последовательные короткие сцены без стартового прыжка сразу в неделю.", userTurns: [ "Привет, я Марк, новый сосед из дома напротив. Просто хотел познакомиться.", "Не буду надолго задерживать. Если что, я иногда буду заходить поздороваться, хорошо?", "Позже. Принёс печенье к чаю, как соседский налог за шумный ремонт.", "Спасибо, что не прогнала. С тобой правда спокойно разговаривать.", "После ещё нескольких спокойных встреч. Я уже привык заходить к тебе на чай, но не хочу надоедать.", "Можно я сяду ближе к столу? Тут теплее, и мне так удобнее говорить.", "Мне нравится, как ты улыбаешься, когда подкалываешь меня.", "Можно взять тебя за руку на минуту? Если странно, я сразу уберу.", "Спасибо. Мне с тобой хорошо.", "Можно тебя обнять? Без спешки. Просто обнять.", "Кажется, я к тебе привязался сильнее, чем планировал.", "Я хочу тебя поцеловать. Но мне важно, чтобы ты сама этого хотела.", ], }, ]; function tryParseJson(content) { try { return { ok: true, value: JSON.parse(content) }; } catch (error) { return { ok: false, error: error.message }; } } function evaluateReply(parsed, raw) { const issues = []; if (!parsed.ok) { issues.push(`invalid JSON: ${parsed.error}`); return { issues, score: 0 }; } const object = parsed.value; if (typeof object.reply !== "string") issues.push("reply is not a string"); if (!Object.prototype.hasOwnProperty.call(object, "interact_action")) issues.push("missing interact_action"); if (!Object.prototype.hasOwnProperty.call(object, "emotion")) issues.push("missing emotion"); if (object.emotion !== null && !validEmotions.has(object.emotion)) issues.push(`unknown emotion: ${object.emotion}`); if (/assistant|ai|искусственн|модель|system prompt|json/i.test(object.reply || "")) { issues.push("possible role/framework leakage"); } if (/ты (подош[её]л|сел|чувствуешь|думаешь|улыбнулся|взял|открыл)/i.test(object.reply || "")) { issues.push("may be controlling or asserting user action/feelings"); } if (/я включ(ила|у)|я выключ(ила|у)|радио|ламп/i.test(object.reply || "")) { const hasAction = object.interact_action && typeof object.interact_action === "object"; if (!hasAction) issues.push("mentions listed object action without interact_action"); } if (raw.length > 1200) issues.push("overlong response for quick RP turn"); return { issues, score: Math.max(0, 5 - issues.length) }; } async function callMaria(messages) { const response = await fetch(`${apiBase.replace(/\/+$/, "")}/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ model, messages, temperature, top_p: 1, max_tokens: maxTokens, response_format: { type: "json_object" }, stream: false, }), }); const data = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(data?.error?.message || `HTTP ${response.status}`); } return { content: data?.choices?.[0]?.message?.content || "", usage: data?.usage || null, }; } const transcript = { created_at: new Date().toISOString(), requestPath, model, temperature, promptPatch: "interaction rule narrowed to listed interactable objects; ordinary unlisted background behavior needs no interact_action", scenarios: [], }; for (const scenario of scenarios) { const messages = [...systemMessages]; const turns = []; console.log(`Scenario: ${scenario.name}`); for (const userText of scenario.userTurns) { messages.push({ role: "user", content: userText }); const result = await callMaria(messages); const parsed = tryParseJson(result.content); const evaluation = evaluateReply(parsed, result.content); turns.push({ user: userText, assistant_raw: result.content, assistant: parsed.ok ? parsed.value : null, usage: result.usage, evaluation, }); messages.push({ role: "assistant", content: result.content }); console.log(` ${evaluation.score}/5 ${userText}`); } transcript.scenarios.push({ name: scenario.name, goal: scenario.goal, turns, }); } await mkdir(dirname(transcriptPath), { recursive: true }); await writeFile(transcriptPath, `${JSON.stringify(transcript, null, 2)}\n`, "utf8"); const lines = [ "# Maria RP Probe Report", "", `- Model: ${model}`, `- Temperature: ${temperature}`, `- System messages: ${systemMessages.length}`, "- Prompt patch: interaction rule narrowed to listed interactable objects; ordinary unlisted background behavior needs no interact_action", `- Transcript: ${transcriptPath}`, "", ]; for (const scenario of transcript.scenarios) { const scores = scenario.turns.map((turn) => turn.evaluation.score); const average = scores.reduce((sum, score) => sum + score, 0) / scores.length; lines.push(`## ${scenario.name}`); lines.push(""); lines.push(scenario.goal); lines.push(""); lines.push(`Average score: ${average.toFixed(2)}/5`); lines.push(""); for (const [index, turn] of scenario.turns.entries()) { lines.push(`### Turn ${index + 1}`); lines.push(""); lines.push(`User: ${turn.user}`); lines.push(""); lines.push(`Maria: ${turn.assistant?.reply ?? turn.assistant_raw}`); lines.push(""); lines.push(`interact_action: ${JSON.stringify(turn.assistant?.interact_action ?? null)}`); lines.push(`emotion: ${JSON.stringify(turn.assistant?.emotion ?? null)}`); lines.push(`score: ${turn.evaluation.score}/5`); if (turn.evaluation.issues.length) { lines.push(`issues: ${turn.evaluation.issues.join("; ")}`); } lines.push(""); } } await writeFile(reportPath, `${lines.join("\n")}\n`, "utf8"); console.log(`Transcript saved: ${transcriptPath}`); console.log(`Report saved: ${reportPath}`);