Refine V1 curator task context

This commit is contained in:
Sai-karthik
2026-06-14 15:59:59 +00:00
parent 036aff1d1d
commit 4a20816ba0
3 changed files with 128 additions and 11 deletions

View File

@@ -28,6 +28,37 @@ function sanitize(text: string) {
.trim();
}
function fallbackReply(task: Awaited<ReturnType<typeof buildCuratorTasks>>[number] | undefined, latest: string) {
const lower = latest.toLowerCase();
if (task?.serviceId === "resume-service") {
if (lower.includes("target role") || lower.includes("goal")) {
return "What target role are you aiming for, and what kind of resume change would help most right now: stronger proof, clearer skills, or better role fit?";
}
if (lower.includes("upload") || lower.includes("paste") || lower.includes("resume")) {
return "Send the resume text or upload the file here. I will read it against your goal and prepare the resume handoff from that context.";
}
if (lower.includes("change")) {
return "Which part do you want changed first: summary, experience bullets, skills, projects, or role alignment?";
}
return "Tell me the target role first. Then I will ask for the resume and the exact changes you want.";
}
if (task?.serviceId === "interview-service") {
if (lower.includes("role") || lower.includes("round")) {
return "What role and interview round should this practice room be for?";
}
if (lower.includes("resume") || lower.includes("job")) {
return "Paste the job description or the resume section the interviewer should use as context.";
}
return "What is the one interview skill you want to improve in this setup?";
}
if (task?.serviceId === "roleplay-service") {
if (lower.includes("scenario")) return "What real conversation scenario should we practice?";
if (lower.includes("outcome") || lower.includes("tone")) return "What outcome do you want, and what tone should you practice?";
return "Tell me who you are speaking with and what makes this conversation difficult.";
}
return `What should I capture next for ${task?.title ?? "this curator task"}?`;
}
async function ensureCuratorConversation(input: { userId: string; taskId?: string; date: string }) {
if (!input.taskId) return createConversationPg(input.userId, "V1 Curator chat");
const task = (await buildCuratorTasks(input.userId, input.date)).find((item) => item.id === input.taskId);
@@ -90,12 +121,16 @@ export async function runCuratorChat(input: {
"Use the supplied tools to read tasks, Q-score, service capabilities, reports, and to prepare handoffs.",
"Do not claim a task is completed unless a valid service or platform event exists.",
"Ask a task-specific question, not the same generic question for every task.",
"If the user just opened a task, ask one warm next question based on the exact subtask and available context.",
"Do not list all three subtasks as baked messages. Keep the conversation natural.",
"Keep the answer under 80 words. Use ASCII punctuation only. Do not use em dash or en dash.",
].join("\n"),
prompt: [
`Date: ${date}`,
`Task id: ${input.taskId ?? "none"}`,
`Task title: ${task?.title ?? "General curator chat"}`,
`Task context: ${task?.contextNarrative ?? "none"}`,
`Task subtasks: ${task?.subtasks.join(" | ") ?? "none"}`,
`Service: ${task?.serviceName ?? "none"}`,
`Completion events: ${task?.completionEvents.join(", ") ?? "none"}`,
`User message: ${latest}`,
@@ -105,13 +140,7 @@ export async function runCuratorChat(input: {
});
reply = sanitize(result.text);
} catch (error) {
reply = task?.serviceId === "resume-service"
? "Share the resume text or upload the resume file, plus the target role. I will prepare the resume handoff and keep this task tied to service events."
: task?.serviceId === "interview-service"
? "Tell me the role, round type, and one thing you want to improve. I will prepare the interview setup handoff."
: task?.serviceId === "roleplay-service"
? "Tell me the scenario, who you are speaking with, and the outcome you want. I will prepare the roleplay handoff."
: `What should I capture for ${task?.title ?? "this curator task"}?`;
reply = fallbackReply(task, latest);
}
await addMessagePg(input.userId, {

View File

@@ -51,6 +51,82 @@ function serviceFromRole(role?: string, service?: string): CuratorServiceId | un
return coerceServiceId(service);
}
function cleanTitle(text: string) {
return text
.replace(/^final\s+/i, "")
.replace(/\bscore\b/gi, "readiness")
.replace(/\s+/g, " ")
.trim();
}
function taskCopy(input: {
missionTitle: string;
stageTitle: string;
stageDescription: string;
serviceId?: CuratorServiceId;
}) {
const mission = input.missionTitle;
const stage = cleanTitle(input.stageTitle);
if (input.serviceId === "resume-service") {
return {
title: stage.toLowerCase().includes("resume") ? stage : "Shape your resume around your goal",
subtitle: "Connect your target role, current resume, and the exact changes you want before opening the resume service.",
subtasks: [
"Tell the curator your target role and goal",
"Upload or paste your current resume",
"Pick the resume changes you want first",
],
contextNarrative: `This is a resume-first step for ${mission}. The curator is trying to understand what role you want, what your resume currently says, and what should change first. The resume service should only be opened after that context is captured, so the handoff can extract role-fit proof, gaps, and specific rewrite direction instead of asking the same broad question again.`,
};
}
if (input.serviceId === "interview-service") {
return {
title: stage.toLowerCase().includes("interview") ? stage : "Set up the right interview practice",
subtitle: "Tell the curator the role, round type, and one improvement focus before creating the interview room.",
subtasks: [
"Choose the target role and interview round",
"Add resume or job context for the interviewer",
"Preview the interview room setup",
],
contextNarrative: `This step prepares interview practice for ${mission}. The curator should collect the role, round type, and weakness to work on, then hand off to the interview service with enough context to create a useful preview. It is not a completion task by itself, and it should not mark progress until the interview service emits a real setup or review event.`,
};
}
if (input.serviceId === "roleplay-service") {
return {
title: stage.toLowerCase().includes("roleplay") ? stage : "Practice a real conversation scenario",
subtitle: "Define the situation, audience, and outcome before opening roleplay.",
subtasks: [
"Describe the conversation scenario",
"Set the desired outcome and tone",
"Open the roleplay practice with context",
],
contextNarrative: `This is a roleplay preparation step for ${mission}. The curator needs the conversation scenario, who the user is speaking with, and what outcome they want. The roleplay service should receive that setup and return practice feedback through service events.`,
};
}
if (input.serviceId === "matchmaking-service") {
return {
title: stage.toLowerCase().includes("role") || stage.toLowerCase().includes("path") ? stage : "Clarify your target direction",
subtitle: "Capture target role, constraints, and preferences so the next service action is relevant.",
subtasks: [
"Name the roles you are considering",
"Add constraints like location, salary, or timeline",
"Save the direction for the next service step",
],
contextNarrative: `This step is about direction before execution. The curator is collecting role preferences and constraints for ${mission}, so future resume, interview, and pathway suggestions can be grounded in what the user actually wants.`,
};
}
return {
title: stage || "Clarify the next useful action",
subtitle: input.stageDescription || `Continue ${mission} with the next useful action.`,
subtasks: [
"Tell the curator what you want to achieve",
"Add the missing context for this step",
"Confirm the next service action",
],
contextNarrative: `This step belongs to ${mission}. The curator is collecting enough user context to decide the next useful action and avoid generic tasks. Completion should come from a real platform or service event, not from simply clicking through the checklist.`,
};
}
function taskFromStage(input: {
userId: string;
date: string;
@@ -68,11 +144,17 @@ function taskFromStage(input: {
const serviceId = input.serviceId ?? serviceFromRole(input.role);
const id = `curator:${input.date}:${input.missionInstanceId ?? input.missionId}:${input.stageId ?? input.index}`;
const route = serviceRoute({ serviceId, missionId: input.missionId, missionInstanceId: input.missionInstanceId, stageId: input.stageId, taskId: id });
const copy = taskCopy({
missionTitle: input.missionTitle,
stageTitle: input.stageTitle,
stageDescription: input.stageDescription,
serviceId,
});
return {
id,
date: input.date,
title: input.stageTitle,
subtitle: input.stageDescription || `Continue ${input.missionTitle}`,
title: copy.title,
subtitle: copy.subtitle,
missionId: input.missionId,
missionInstanceId: input.missionInstanceId,
stageId: input.stageId,
@@ -88,9 +170,12 @@ function taskFromStage(input: {
cta: serviceId ? `Open ${serviceName(serviceId).replace(" service", "")}` : "Open mission",
context: [
{ label: "Mission", value: input.missionTitle },
{ label: "Stage", value: input.stageTitle },
{ label: "Current focus", value: copy.title },
{ label: "Service handoff", value: serviceName(serviceId, input.role) },
{ label: "Source", value: input.missionInstanceId ? "Active mission" : "Mission registry" },
],
contextNarrative: copy.contextNarrative,
subtasks: copy.subtasks,
signals: [input.missionTitle, input.stageTitle, serviceName(serviceId, input.role)],
completionEvents: completionEventsForService(serviceId),
source: input.missionInstanceId ? "curator-v1" : "mission-registry",
@@ -133,6 +218,7 @@ export async function buildCuratorTasks(userId: string, date = todayIso()): Prom
const snapshot = item.snapshot;
const stages = (snapshot?.stages ?? [])
.filter((stage) => stage.status !== "locked" && stage.status !== "done")
.filter((stage) => serviceFromRole(stage.role) !== "qscore-service")
.sort((a, b) => {
if (a.id === snapshot?.currentStageId) return -1;
if (b.id === snapshot?.currentStageId) return 1;
@@ -161,7 +247,7 @@ export async function buildCuratorTasks(userId: string, date = todayIso()): Prom
if (tasks.length < 3) {
const fallbackModules = listMissionDefinitions().flatMap((mission) =>
mission.modules.map((module) => ({ mission, module, serviceId: serviceFromRole(module.role, module.service) })),
);
).filter((item) => item.serviceId !== "qscore-service");
const seenServices = new Set(tasks.map((task) => task.serviceId).filter(Boolean));
for (const item of fallbackModules) {

View File

@@ -38,6 +38,8 @@ export const curatorTaskSchema = z.object({
route: z.string(),
cta: z.string(),
context: z.array(z.object({ label: z.string(), value: z.string() })),
contextNarrative: z.string(),
subtasks: z.array(z.string()).min(1),
signals: z.array(z.string()),
completionEvents: z.array(z.string()),
source: z.enum(["curator-v1", "mission-registry", "service-registry"]),