Fix curator task handoffs and profile state

This commit is contained in:
Sai-karthik
2026-07-13 10:11:27 +00:00
committed by Sai-karthik
parent 9cb1027f4c
commit 301dba8c50
4 changed files with 67 additions and 18 deletions
+12 -3
View File
@@ -519,6 +519,9 @@ async function resolveGrowUserContext(req: Request, userId: string): Promise<Rec
if (resumeState.current_company && !userContext.current_company) userContext.current_company = resumeState.current_company;
if (Array.isArray(resumeState.experience_history)) userContext.experience_history = resumeState.experience_history;
if (Array.isArray(resumeState.education)) userContext.education = resumeState.education;
userContext.resume_available = Number(resumeState.resume_count ?? 0) > 0;
const resumeSummary = getString(resumeState.resume_summary ?? resumeState.resumeSummary);
if (resumeSummary) userContext.resume_summary = resumeSummary;
}
if (socialState) {
@@ -534,7 +537,9 @@ async function resolveGrowUserContext(req: Request, userId: string): Promise<Rec
function hasResumeContext(userContext: Record<string, unknown>): boolean {
return Boolean(
(Array.isArray(userContext.experience_history) && userContext.experience_history.length > 0) ||
userContext.resume_available ||
getString(userContext.resume_summary) ||
(Array.isArray(userContext.experience_history) && userContext.experience_history.length > 0) ||
(Array.isArray(userContext.skills) && userContext.skills.length > 0) ||
getString(userContext.current_role)
);
@@ -556,6 +561,9 @@ function composeCandidateProfile(userContext: Record<string, unknown>): string {
if (currentRole) lines.push(`Current role: ${currentRole}`);
if (headline && headline.toLowerCase() !== currentRole?.toLowerCase()) lines.push(`LinkedIn headline: ${headline}`);
const resumeSummary = getString(userContext.resume_summary);
if (resumeSummary) lines.push(`Primary resume summary: ${resumeSummary.length > 600 ? `${resumeSummary.slice(0, 600).trimEnd()}…` : resumeSummary}`);
const skills = stringArray(userContext.skills);
if (skills.length) lines.push(`Key skills: ${skills.slice(0, 12).join(", ")}`);
@@ -591,7 +599,7 @@ function composeCandidateProfile(userContext: Record<string, unknown>): string {
if (education.length) lines.push(`Education: ${education.slice(0, 3).join("; ")}`);
const summary = getString(userContext.linkedin_summary);
if (summary) lines.push(`Profile summary: ${summary.length > 300 ? `${summary.slice(0, 300).trimEnd()}…` : summary}`);
if (summary && summary !== resumeSummary) lines.push(`LinkedIn summary: ${summary.length > 300 ? `${summary.slice(0, 300).trimEnd()}…` : summary}`);
const text = lines.join("\n");
return text.length > 1200 ? `${text.slice(0, 1200).trimEnd()}…` : text;
@@ -613,7 +621,8 @@ async function buildPersonalizedConfigurePayload(req: Request, body: JsonObject,
difficulty: getString(incomingContext.difficulty) ?? "medium",
};
if (incomingContext.personalize) {
const curatorHandoff = getString(incomingContext.source) === "curator-v1" || Boolean(getString(incomingContext.curatorTaskId));
if (incomingContext.personalize || curatorHandoff) {
const candidateProfile = composeCandidateProfile(userContext);
if (candidateProfile) context.candidate_profile = candidateProfile;
}
+7 -3
View File
@@ -962,11 +962,15 @@ export function buildCuratorServiceRoute(input: CuratorRouteInput) {
if (input.taskType) state.taskType = input.taskType;
if (service.id === "interview-service") {
state.role = input.targetRole?.trim() || "Product Manager";
state.type = "behavioral";
const taskText = `${input.taskTitle ?? ""} ${input.taskSubtitle ?? ""}`.toLowerCase();
const isIntroductionTask = /(?:60|90)[ -]?second|self[ -]?introduction|professional introduction|client pitch|executive introduction/.test(taskText);
state.role = isIntroductionTask
? `${input.targetRole?.trim() || "Professional"} self-introduction`
: input.targetRole?.trim() || "General professional";
state.type = isIntroductionTask ? "warm_up" : "behavioral";
state.persona = input.personaId ?? "payal";
state.duration = input.durationMinutes ?? 5;
state.difficulty = input.difficulty ?? "medium";
state.difficulty = input.difficulty ?? (isIntroductionTask ? "easy" : "medium");
state.media = input.requestedMode ?? "video";
}
+15 -9
View File
@@ -5,7 +5,7 @@ import { listMissionDefinitions } from "../../missions/registry.js";
import { listServiceCapabilities } from "../../workflows/service-capabilities.js";
import { DEFAULT_QSCORE_ORG_ID, getQscoreFromService } from "../../services/qscore-proxy.js";
import { emitCuratorEvent } from "./curator-events.js";
import { buildCuratorUserContext } from "./curator-user-context.js";
import { buildCuratorUserContext, type CuratorUserContext } from "./curator-user-context.js";
import { isCuratorIcpId, type CuratorIcpId } from "./icp-registry.js";
import type {
CuratorPlan,
@@ -1155,8 +1155,9 @@ function buildTask(
recentRows: Awaited<ReturnType<typeof loadRecentContextRows>>,
focusDate: string,
durationDays: number,
targetRole?: string,
userContext: CuratorUserContext,
): CuratorTask {
const targetRole = userContext.targetRole ?? undefined;
seedTask = {
...normalizeActiveCuratorTaskSeed(seedTask),
isTrialTask: seedTask.isTrialTask,
@@ -1211,10 +1212,15 @@ function buildTask(
isPaidTask: seedTask.isPaidTask !== false,
isRecoveryPractice: Boolean(seedTask.isRecoveryPractice),
};
const isProfileTask = /complete (?:your )?(?:growqr |professional |executive |freelancer )?profile/i.test(`${task.title} ${task.subtitle}`);
const profileComplete = isProfileTask && (userContext.profile.resumeAvailable || userContext.profile.linkedinAvailable);
const routeTask = isProfileTask && userContext.profile.linkedinAvailable && !userContext.profile.resumeAvailable
? { ...task, serviceId: "resume-service" as const }
: task;
const taskWithRoute = {
...task,
route: buildCuratorTaskDeepLink(task, targetRole),
status: classifyTaskStatus({ task, focusDate, completionRows, recentRows }),
route: buildCuratorTaskDeepLink(routeTask, targetRole),
status: profileComplete ? "completed" as const : classifyTaskStatus({ task, focusDate, completionRows, recentRows }),
} satisfies CuratorTask;
return taskWithRoute;
}
@@ -1227,7 +1233,7 @@ function buildTasksForPlanDay(
recentRows: Awaited<ReturnType<typeof loadRecentContextRows>>,
focusDate: string,
durationDays: number,
targetRole?: string,
userContext: CuratorUserContext,
) {
return planDay.plannedTasks.map((task) => (
buildTask(
@@ -1242,7 +1248,7 @@ function buildTasksForPlanDay(
recentRows,
focusDate,
durationDays,
targetRole,
userContext,
)
));
}
@@ -1266,7 +1272,7 @@ export async function buildCuratorTasks(userId: string, date = todayIso()): Prom
);
const planDay = adaptedPlanDays[dayIndex - 1];
if (!planDay) return [];
return buildTasksForPlanDay(sprintStartDate, date, planDay, completionRows, recentRows, date, sprintState.durationDays, userContext.targetRole ?? undefined);
return buildTasksForPlanDay(sprintStartDate, date, planDay, completionRows, recentRows, date, sprintState.durationDays, userContext);
}
export async function buildCuratorStreak(userId: string): Promise<CuratorStreak> {
@@ -1333,7 +1339,7 @@ async function buildCuratorSprintInternal(
const date = addDaysIso(sprintStartDate, index);
const planDay = planDays[index]!;
const tasks = date <= focusDate
? buildTasksForPlanDay(sprintStartDate, date, planDay, completionRows, recentRows, focusDate, sprintState.durationDays, userContext.targetRole ?? undefined)
? buildTasksForPlanDay(sprintStartDate, date, planDay, completionRows, recentRows, focusDate, sprintState.durationDays, userContext)
: [];
const completedCount = tasks.filter((task) => task.status === "completed").length;
const unlockState = focusDate > date ? "completed" : focusDate === date ? "active" : "upcoming";
@@ -1616,7 +1622,7 @@ export async function buildServiceCurationPreview(input: ServiceCurationPreviewI
return {
...planDay,
date,
tasks: buildTasksForPlanDay(startDate, date, planDay, completionRows, [], date, FULL_SPRINT_DURATION_DAYS, userContext.targetRole ?? undefined),
tasks: buildTasksForPlanDay(startDate, date, planDay, completionRows, [], date, FULL_SPRINT_DURATION_DAYS, userContext as CuratorUserContext),
};
});
+33 -3
View File
@@ -3,6 +3,7 @@ import { db } from "../../db/client.js";
import { growEvents, onboarding } from "../../db/schema.js";
import { asRecord, getString } from "../../events/envelope.js";
import { DEFAULT_QSCORE_ORG_ID, getQscoreFromService } from "../../services/qscore-proxy.js";
import { config } from "../../config.js";
import type { CuratorTask } from "./curator-types.js";
function isRecord(value: unknown): value is Record<string, unknown> {
@@ -26,6 +27,10 @@ export type CuratorUserContext = {
latestSummary?: string;
skills: string[];
};
profile: {
resumeAvailable: boolean;
linkedinAvailable: boolean;
};
goals: string[];
pastActivity: {
recentEventCount: number;
@@ -173,7 +178,11 @@ export async function buildCuratorUserContext(userId: string): Promise<CuratorUs
.orderBy(desc(growEvents.occurredAt))
.limit(80);
const qscoreResult = await getQscoreFromService(userId, DEFAULT_QSCORE_ORG_ID);
const [qscoreResult, resumeState, socialState] = await Promise.all([
getQscoreFromService(userId, DEFAULT_QSCORE_ORG_ID),
fetchProfileState(config.resumeServiceUrl, userId),
fetchProfileState(config.socialBrandingServiceUrl, userId),
]);
const breakdown = qscoreResult?.breakdown ?? {};
const breakdownSignals = Array.isArray(breakdown.signals) ? breakdown.signals : [];
@@ -184,19 +193,25 @@ export async function buildCuratorUserContext(userId: string): Promise<CuratorUs
...rows.flatMap((row) => goalsFromPayload(row.payload)),
]);
const skills = uniqueStrings(rows.flatMap((row) => stringArray(asRecord(row.payload).skills))).slice(0, 12);
const latestResume = rows
const latestResume = getString(resumeState.resume_summary ?? resumeState.resumeSummary) ?? rows
.map((row) => resumeSummaryFromPayload(row.payload))
.find(Boolean);
const resumeAvailable = Number(resumeState.resume_count ?? 0) > 0 || Boolean(latestResume);
const linkedinAvailable = Boolean(
socialState.linkedin_connected ||
getString(socialState.linkedin_profile_id ?? socialState.profile_url ?? socialState.headline ?? socialState.summary),
);
return {
userId,
targetRole,
experienceLevel: experienceLevelFromOnboarding(onboardingPayload) ?? inferExperienceLevel(corpus),
resume: {
available: Boolean(latestResume || /\bresume|cv|linkedin\b/i.test(corpus)),
available: resumeAvailable,
latestSummary: latestResume,
skills,
},
profile: { resumeAvailable, linkedinAvailable },
goals: goals.length ? goals.slice(0, 8) : targetRole ? [targetRole] : [],
pastActivity: {
recentEventCount: rows.length,
@@ -217,6 +232,21 @@ export async function buildCuratorUserContext(userId: string): Promise<CuratorUs
};
}
async function fetchProfileState(baseUrl: string, userId: string): Promise<Record<string, unknown>> {
if (!baseUrl) return {};
try {
const response = await fetch(new URL(`/api/state/${encodeURIComponent(userId)}`, baseUrl), {
headers: config.a2aAllowedKey ? { authorization: `Bearer ${config.a2aAllowedKey}` } : undefined,
signal: AbortSignal.timeout(2_500),
});
if (!response.ok) return {};
const value: unknown = await response.json();
return isRecord(value) ? value : {};
} catch {
return {};
}
}
function experienceLevelFromOnboarding(payload: Record<string, unknown>): CuratorUserContext["experienceLevel"] | null {
const icp = getString(payload.onboarding_icp);
if (icp === "student") return "student";