Fix curator prompt leakage
This commit is contained in:
@@ -39,7 +39,19 @@ function buildId(prefix: string) {
|
||||
}
|
||||
|
||||
function sanitize(text: string) {
|
||||
return text
|
||||
const withoutControlLines = text
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return true;
|
||||
if (/^(date|curator task id|focused subtask|curator task title|curator task context|curator task subtasks|curator service|expected completion events|captured task memory|task title|task service|task context|all task subtasks|visible history):/i.test(trimmed)) return false;
|
||||
if (/^```/.test(trimmed)) return false;
|
||||
return true;
|
||||
})
|
||||
.join("\n")
|
||||
.trim();
|
||||
const withoutJsonEnvelope = withoutControlLines.replace(/^\s*\{[\s\S]*"reply"\s*:\s*"([^"]+)"[\s\S]*\}\s*$/i, "$1");
|
||||
return withoutJsonEnvelope
|
||||
.replace(/[\u2013\u2014]/g, "-")
|
||||
.replace(/[\u2018\u2019]/g, "'")
|
||||
.replace(/[\u201C\u201D]/g, '"')
|
||||
@@ -48,6 +60,19 @@ function sanitize(text: string) {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function pushField(lines: string[], label: string, value?: string | number | null) {
|
||||
if (value === undefined || value === null) return;
|
||||
const stringValue = String(value).trim();
|
||||
if (!stringValue) return;
|
||||
lines.push(`${label}: ${stringValue}`);
|
||||
}
|
||||
|
||||
function pushList(lines: string[], label: string, values?: string[]) {
|
||||
const cleanValues = values?.map((value) => value.trim()).filter(Boolean) ?? [];
|
||||
if (cleanValues.length === 0) return;
|
||||
lines.push(`${label}: ${cleanValues.join(" | ")}`);
|
||||
}
|
||||
|
||||
type CuratorMessage = Awaited<ReturnType<typeof listMessagesPg>>[number];
|
||||
|
||||
async function capturedSubtaskMemory(userId: string, taskId?: string) {
|
||||
@@ -131,7 +156,7 @@ function curatorSystemAddendum(input: {
|
||||
task?: Awaited<ReturnType<typeof buildCuratorTasks>>[number];
|
||||
taskMemory?: Array<{ subtaskIndex?: number; subtask?: string; summary?: string }>;
|
||||
}) {
|
||||
return [
|
||||
const lines = [
|
||||
"You are currently speaking as the GrowQR V1 Curator through the Conversation Actor.",
|
||||
"The V1 Curator owns 30 day direction, streak continuity, and service handoff decisions.",
|
||||
"Carry state from the conversation history. If the user gives a short answer like a role name, accept it and ask for the next missing slot.",
|
||||
@@ -144,17 +169,26 @@ function curatorSystemAddendum(input: {
|
||||
"When the user has answered the focused subtask enough, summarize what was captured and stop. Do not ask the next subtask question.",
|
||||
"If more detail is needed, ask exactly one follow-up question for the focused subtask only.",
|
||||
"Use captured task memory from previous subtasks as context. Do not ask the user to repeat details already captured there.",
|
||||
`Date: ${input.date}`,
|
||||
`Curator task id: ${input.taskId ?? "none"}`,
|
||||
`Focused subtask index: ${Number.isInteger(input.subtaskIndex) ? input.subtaskIndex : "none"}`,
|
||||
`Focused subtask title: ${input.subtask ?? "none"}`,
|
||||
`Curator task title: ${input.task?.title ?? "General curator chat"}`,
|
||||
`Curator task context: ${input.task?.contextNarrative ?? "none"}`,
|
||||
`Curator task subtasks: ${input.task?.subtasks.join(" | ") ?? "none"}`,
|
||||
`Curator service: ${input.task?.serviceName ?? "none"}`,
|
||||
`Expected completion events: ${input.task?.completionEvents.join(", ") ?? "none"}`,
|
||||
`Captured task memory: ${input.taskMemory?.map((item) => `[${item.subtaskIndex ?? "?"}] ${item.subtask ?? "Subtask"}: ${item.summary}`).join(" | ") || "none"}`,
|
||||
].join("\n");
|
||||
];
|
||||
pushField(lines, "Date", input.date);
|
||||
pushField(lines, "Curator task id", input.taskId);
|
||||
pushField(lines, "Focused subtask index", Number.isInteger(input.subtaskIndex) ? input.subtaskIndex : undefined);
|
||||
pushField(lines, "Focused subtask title", input.subtask);
|
||||
pushField(lines, "Curator task title", input.task?.title);
|
||||
pushField(lines, "Curator task context", input.task?.contextNarrative);
|
||||
pushList(lines, "Curator task subtasks", input.task?.subtasks);
|
||||
pushField(lines, "Curator service", input.task?.serviceName);
|
||||
pushList(lines, "Expected completion events", input.task?.completionEvents);
|
||||
const memory = input.taskMemory
|
||||
?.map((item) => {
|
||||
if (!item.summary) return "";
|
||||
const subtask = item.subtask?.trim() || "Subtask";
|
||||
const index = Number.isInteger(item.subtaskIndex) ? `[${item.subtaskIndex}] ` : "";
|
||||
return `${index}${subtask}: ${item.summary}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
pushList(lines, "Captured task memory", memory);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function curatorTaskKey(taskId?: string, subtaskIndex?: number) {
|
||||
@@ -174,7 +208,9 @@ function firstTurnPrompt(input: {
|
||||
}
|
||||
|
||||
function isExplicitHandoffRequest(text: string) {
|
||||
return /\b(start|open|launch|begin|set up|setup|create|generate)\b/i.test(text);
|
||||
const trimmed = text.trim();
|
||||
if (/^start$/i.test(trimmed)) return false;
|
||||
return /\b(start|open|launch|begin|set up|setup|create|generate)\b/i.test(trimmed);
|
||||
}
|
||||
|
||||
async function evaluateSubtaskStatus(input: {
|
||||
@@ -204,17 +240,19 @@ async function evaluateSubtaskStatus(input: {
|
||||
"Never mark ready just because one message exists.",
|
||||
"Use ASCII punctuation only.",
|
||||
].join("\n"),
|
||||
prompt: [
|
||||
`Task title: ${input.task?.title ?? "none"}`,
|
||||
`Task service: ${input.task?.serviceName ?? "none"}`,
|
||||
`Focused subtask index: ${Number.isInteger(input.subtaskIndex) ? input.subtaskIndex : "none"}`,
|
||||
`Focused subtask: ${input.subtask}`,
|
||||
`Task context: ${input.task?.contextNarrative ?? "none"}`,
|
||||
`All task subtasks: ${input.task?.subtasks.join(" | ") ?? "none"}`,
|
||||
`Latest user answer: ${input.latest}`,
|
||||
`Assistant reply: ${input.reply}`,
|
||||
`Visible history: ${input.history.map((message) => `${message.role}: ${message.content}`).join("\n")}`,
|
||||
].join("\n"),
|
||||
prompt: (() => {
|
||||
const lines: string[] = [];
|
||||
pushField(lines, "Task title", input.task?.title);
|
||||
pushField(lines, "Task service", input.task?.serviceName);
|
||||
pushField(lines, "Focused subtask index", Number.isInteger(input.subtaskIndex) ? input.subtaskIndex : undefined);
|
||||
pushField(lines, "Focused subtask", input.subtask);
|
||||
pushField(lines, "Task context", input.task?.contextNarrative);
|
||||
pushList(lines, "All task subtasks", input.task?.subtasks);
|
||||
pushField(lines, "Latest user answer", input.latest);
|
||||
pushField(lines, "Assistant reply", input.reply);
|
||||
pushField(lines, "Visible history", input.history.map((message) => `${message.role}: ${message.content}`).join("\n"));
|
||||
return lines.join("\n");
|
||||
})(),
|
||||
});
|
||||
return subtaskStatusUpdateSchema.parse(parseJsonObject(result.text));
|
||||
} catch (error) {
|
||||
@@ -294,7 +332,14 @@ export async function runCuratorChat(input: {
|
||||
"Return JSON only: {\"summary\": string, \"userGoal\"?: string, \"serviceIntent\"?: string, \"shouldPrepareHandoff\": boolean}.",
|
||||
"Use ASCII punctuation only.",
|
||||
].join("\n"),
|
||||
prompt: `Task: ${task?.title ?? "General curator chat"}\nSubtask: ${input.subtask ?? "none"}\nService: ${task?.serviceName ?? "none"}\nMessage: ${latest}`,
|
||||
prompt: (() => {
|
||||
const lines: string[] = [];
|
||||
pushField(lines, "Task", task?.title);
|
||||
pushField(lines, "Subtask", input.subtask);
|
||||
pushField(lines, "Service", task?.serviceName);
|
||||
pushField(lines, "Message", latest);
|
||||
return lines.join("\n");
|
||||
})(),
|
||||
});
|
||||
const parsedExtract = chatExtractSchema.parse(parseJsonObject(extract.text));
|
||||
await emitCuratorEvent({
|
||||
|
||||
@@ -92,13 +92,14 @@ type GeneratedTaskCopy = z.infer<typeof generatedTaskCopySchema>;
|
||||
const taskCopyCache = new Map<string, GeneratedTaskCopy>();
|
||||
|
||||
function parseJsonObject(text: string) {
|
||||
const trimmed = text.trim();
|
||||
const trimmed = text.trim().replace(/^```(?:json)?/i, "").replace(/```$/i, "").trim();
|
||||
try {
|
||||
return JSON.parse(trimmed);
|
||||
} catch {
|
||||
const match = trimmed.match(/\{[\s\S]*\}/);
|
||||
if (!match) throw new Error("model_did_not_return_json");
|
||||
return JSON.parse(match[0]);
|
||||
const start = trimmed.indexOf("{");
|
||||
const end = trimmed.lastIndexOf("}");
|
||||
if (start === -1 || end === -1 || end <= start) throw new Error("model_did_not_return_json");
|
||||
return JSON.parse(trimmed.slice(start, end + 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,11 +121,9 @@ async function taskCopy(input: {
|
||||
if (cached) return cached;
|
||||
|
||||
try {
|
||||
const result = await generateText({
|
||||
model: getConversationModel(),
|
||||
system: [
|
||||
const system = [
|
||||
"You generate GrowQR V1 curator task copy.",
|
||||
"Return JSON only. Do not wrap it in markdown.",
|
||||
"Return valid JSON only. Do not wrap it in markdown.",
|
||||
"The JSON shape must be: {\"title\": string, \"subtitle\": string, \"subtasks\": [string, string, string], \"contextNarrative\": string}.",
|
||||
"Do not invent new missions or services. Use the provided mission, stage, role, and service only.",
|
||||
"Generate exactly three sequential subtasks that make sense for a real user.",
|
||||
@@ -133,37 +132,36 @@ async function taskCopy(input: {
|
||||
"Do not include rewards, coins, scores, or generic readiness-score language.",
|
||||
"Use ASCII punctuation only.",
|
||||
"For early users, prefer practical context capture: goal, current background, resume/input, constraints, or service setup.",
|
||||
].join("\n"),
|
||||
prompt: [
|
||||
`Mission: ${input.missionTitle}`,
|
||||
`Stage: ${input.stageTitle}`,
|
||||
`Stage description: ${input.stageDescription || "none"}`,
|
||||
`Stage role: ${input.role ?? "none"}`,
|
||||
`Service: ${input.serviceId ? serviceName(input.serviceId) : "Mission Planner"}`,
|
||||
`Detected intent: ${stageIntent(input)}`,
|
||||
].join("\n"),
|
||||
});
|
||||
let rawCopy = result.text;
|
||||
let parsedCopy: unknown;
|
||||
try {
|
||||
parsedCopy = parseJsonObject(rawCopy);
|
||||
} catch (parseError) {
|
||||
const repaired = await generateText({
|
||||
].join("\n");
|
||||
const basePrompt = (() => {
|
||||
const lines = [
|
||||
`Mission: ${input.missionTitle}`,
|
||||
`Stage: ${input.stageTitle}`,
|
||||
`Service: ${input.serviceId ? serviceName(input.serviceId) : "Mission Planner"}`,
|
||||
`Detected intent: ${stageIntent(input)}`,
|
||||
];
|
||||
if (input.stageDescription.trim()) lines.push(`Stage description: ${input.stageDescription}`);
|
||||
if (input.role?.trim()) lines.push(`Stage role: ${input.role}`);
|
||||
return lines.join("\n");
|
||||
})();
|
||||
let lastError = "";
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
const result = await generateText({
|
||||
model: getConversationModel(),
|
||||
system: [
|
||||
"Repair invalid JSON into valid JSON only.",
|
||||
"Do not add markdown.",
|
||||
"Do not add or invent new task content.",
|
||||
"Keep the exact object shape: {\"title\": string, \"subtitle\": string, \"subtasks\": [string, string, string], \"contextNarrative\": string}.",
|
||||
].join("\n"),
|
||||
prompt: rawCopy,
|
||||
system,
|
||||
prompt: attempt === 0
|
||||
? basePrompt
|
||||
: `${basePrompt}\n\nPrevious JSON was invalid: ${lastError}\nRegenerate the same task copy as valid JSON only. Do not add comments or markdown.`,
|
||||
});
|
||||
rawCopy = repaired.text;
|
||||
parsedCopy = parseJsonObject(rawCopy);
|
||||
try {
|
||||
const copy = generatedTaskCopySchema.parse(parseJsonObject(result.text));
|
||||
taskCopyCache.set(cacheKey, copy);
|
||||
return copy;
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
const copy = generatedTaskCopySchema.parse(parsedCopy);
|
||||
taskCopyCache.set(cacheKey, copy);
|
||||
return copy;
|
||||
throw new Error(`curator_task_copy_invalid_json: ${lastError}`);
|
||||
} catch (error) {
|
||||
console.warn("curator task copy generation failed", {
|
||||
missionTitle: input.missionTitle,
|
||||
|
||||
Reference in New Issue
Block a user