|
|
|
|
@@ -1,6 +1,70 @@
|
|
|
|
|
import { config } from "../config.js";
|
|
|
|
|
import type { CuratorServiceId, CuratorTask } from "../v1/curator/curator-types.js";
|
|
|
|
|
|
|
|
|
|
type QueryValue = string | number | undefined | null;
|
|
|
|
|
export type QueryValue = string | number | boolean | undefined | null;
|
|
|
|
|
export type QueryState = Record<string, QueryValue>;
|
|
|
|
|
|
|
|
|
|
export type ServiceId =
|
|
|
|
|
| "interview-service"
|
|
|
|
|
| "roleplay-service"
|
|
|
|
|
| "courses-service"
|
|
|
|
|
| "assessment-service"
|
|
|
|
|
| "matchmaking-service"
|
|
|
|
|
| "pathways-service"
|
|
|
|
|
| "resume-service"
|
|
|
|
|
| "cover-letter-service"
|
|
|
|
|
| "qscore-service"
|
|
|
|
|
| "social-branding-service";
|
|
|
|
|
|
|
|
|
|
export type ServiceCategory = "practice" | "learning" | "opportunity" | "document" | "measurement" | "profile";
|
|
|
|
|
|
|
|
|
|
export type ServiceEndpoint = {
|
|
|
|
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
|
|
|
path: string;
|
|
|
|
|
contract: string;
|
|
|
|
|
usage: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type ServiceFrontendPage = {
|
|
|
|
|
path: string;
|
|
|
|
|
aliases?: string[];
|
|
|
|
|
queryParams: string[];
|
|
|
|
|
usage: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type ServiceRecord = {
|
|
|
|
|
id: ServiceId;
|
|
|
|
|
label: string;
|
|
|
|
|
description: string;
|
|
|
|
|
category: ServiceCategory;
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
featureId: string;
|
|
|
|
|
promptModulePath: string;
|
|
|
|
|
aliases?: string[];
|
|
|
|
|
backend: {
|
|
|
|
|
baseUrl?: string;
|
|
|
|
|
publicUrl?: string;
|
|
|
|
|
healthPath: string;
|
|
|
|
|
endpoints: Record<string, ServiceEndpoint>;
|
|
|
|
|
usage: string;
|
|
|
|
|
};
|
|
|
|
|
frontend: {
|
|
|
|
|
baseUrl: string;
|
|
|
|
|
pages: Record<string, ServiceFrontendPage>;
|
|
|
|
|
usage: string;
|
|
|
|
|
};
|
|
|
|
|
curator: {
|
|
|
|
|
defaultPage: string;
|
|
|
|
|
defaultActionLabel: string;
|
|
|
|
|
defaultQueryState?: QueryState;
|
|
|
|
|
actionLabels?: Record<string, string>;
|
|
|
|
|
toolName: string;
|
|
|
|
|
completionEvents: string[];
|
|
|
|
|
qscoreSignals?: string[];
|
|
|
|
|
usage: string;
|
|
|
|
|
};
|
|
|
|
|
usageDocs: string[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type MissionServiceId = Extract<CuratorServiceId, "interview-service" | "roleplay-service" | "resume-service">;
|
|
|
|
|
|
|
|
|
|
@@ -26,10 +90,16 @@ type CuratorRouteInput = {
|
|
|
|
|
roleplayBrief?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function appendQuery(
|
|
|
|
|
pathname: string,
|
|
|
|
|
params: Record<string, QueryValue>,
|
|
|
|
|
) {
|
|
|
|
|
function endpoint(
|
|
|
|
|
method: ServiceEndpoint["method"],
|
|
|
|
|
path: string,
|
|
|
|
|
contract: string,
|
|
|
|
|
usage: string,
|
|
|
|
|
): ServiceEndpoint {
|
|
|
|
|
return { method, path, contract, usage };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendQuery(pathname: string, params: QueryState = {}) {
|
|
|
|
|
const search = new URLSearchParams();
|
|
|
|
|
for (const [key, value] of Object.entries(params)) {
|
|
|
|
|
if (value === undefined || value === null || value === "") continue;
|
|
|
|
|
@@ -47,6 +117,720 @@ function getSessionId(detail?: Record<string, unknown>) {
|
|
|
|
|
return getString(detail?.session_id ?? detail?.sessionId ?? detail?.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const frontendBaseUrl = config.workflowsDashboardUrl.replace(/\/$/, "");
|
|
|
|
|
|
|
|
|
|
const serviceRegistry: ServiceRecord[] = [
|
|
|
|
|
{
|
|
|
|
|
id: "interview-service",
|
|
|
|
|
label: "Interview",
|
|
|
|
|
description: "Configure, practice, review, and score mock interview sessions.",
|
|
|
|
|
category: "practice",
|
|
|
|
|
enabled: Boolean(config.interviewServiceUrl),
|
|
|
|
|
featureId: "mock-interview",
|
|
|
|
|
promptModulePath: "agents/interview.md",
|
|
|
|
|
backend: {
|
|
|
|
|
baseUrl: config.interviewServiceUrl,
|
|
|
|
|
publicUrl: config.interviewPublicUrl,
|
|
|
|
|
healthPath: "/health",
|
|
|
|
|
endpoints: {
|
|
|
|
|
health: endpoint("GET", "/health", "Readiness probe.", "Check service availability before a handoff."),
|
|
|
|
|
pageState: endpoint("GET", "/api/v1/interviews/page-state?user_id=:userId", "Returns usage and personalization state.", "Hydrate interview landing/setup pages."),
|
|
|
|
|
configure: endpoint("POST", "/api/v1/configure", "Creates an interview plan from user, org, persona, type, duration, and context.", "Use for committed setup requests."),
|
|
|
|
|
preview: endpoint("POST", "/api/v1/configure/preview", "Creates a preview plan without starting live practice.", "Use for curator/dashboard previews."),
|
|
|
|
|
questions: endpoint("POST", "/api/v1/configure/questions", "Edits generated questions for a session.", "Use after preview edits."),
|
|
|
|
|
approve: endpoint("POST", "/api/v1/configure/approve", "Approves a generated session by session_id.", "Use when a user accepts a preview."),
|
|
|
|
|
assignments: endpoint("GET", "/api/v1/interviews/assignments", "Lists interview assignments by email/status/limit.", "Render assigned practice work."),
|
|
|
|
|
createAssignments: endpoint("POST", "/api/v1/interviews/assignments", "Creates interview assignments for assignee emails.", "Admin or organization handoffs."),
|
|
|
|
|
unassign: endpoint("POST", "/api/v1/interviews/assignments/unassign", "Removes interview assignments.", "Admin cleanup."),
|
|
|
|
|
resultsBulk: endpoint("POST", "/api/v1/interviews/results:bulk", "Fetches result summaries for multiple sessions.", "Dashboard history and summaries."),
|
|
|
|
|
review: endpoint("GET", "/api/v1/review/:sessionId", "Returns review/status for a session.", "Poll or open feedback."),
|
|
|
|
|
leaderboard: endpoint("GET", "/api/v1/leaderboard", "Returns interview leaderboard.", "Leaderboard widgets."),
|
|
|
|
|
artifact: endpoint("GET", "/api/v1/artifacts/:sessionId/:artifactType", "Returns session artifacts.", "Fetch transcript, report, or media artifacts."),
|
|
|
|
|
videoUploadUrl: endpoint("POST", "/api/v1/sessions/:sessionId/video/upload-url", "Returns signed upload instructions.", "Browser upload setup."),
|
|
|
|
|
markVideoUploaded: endpoint("POST", "/api/v1/sessions/:sessionId/video/uploaded", "Marks uploaded video as available.", "Complete upload flow."),
|
|
|
|
|
},
|
|
|
|
|
usage: "Backend callers should use the gateway /services/interview/* routes when user auth, mission correlation, and event recording are required.",
|
|
|
|
|
},
|
|
|
|
|
frontend: {
|
|
|
|
|
baseUrl: frontendBaseUrl,
|
|
|
|
|
pages: {
|
|
|
|
|
discovery: {
|
|
|
|
|
path: "/upskilling/interview",
|
|
|
|
|
aliases: ["/agents/interview"],
|
|
|
|
|
queryParams: ["fresh"],
|
|
|
|
|
usage: "Entry screen for role selection and resume of in-progress interview work.",
|
|
|
|
|
},
|
|
|
|
|
setup: {
|
|
|
|
|
path: "/upskilling/interview/setup",
|
|
|
|
|
aliases: ["/agents/interview/setup"],
|
|
|
|
|
queryParams: ["role", "type", "from_assignment"],
|
|
|
|
|
usage: "Collects interview role, type, duration, persona, media mode, and personalization consent.",
|
|
|
|
|
},
|
|
|
|
|
preview: {
|
|
|
|
|
path: "/upskilling/interview/preview",
|
|
|
|
|
aliases: ["/agents/interview/preview"],
|
|
|
|
|
queryParams: ["role", "type", "persona", "duration", "difficulty", "media", "vip", "from_assignment", "personalize", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
|
|
|
usage: "Curator default handoff. The page configures the session and opens the launch overlay.",
|
|
|
|
|
},
|
|
|
|
|
feedback: {
|
|
|
|
|
path: "/upskilling/interview/feedback",
|
|
|
|
|
queryParams: ["sessionId"],
|
|
|
|
|
usage: "Opens feedback/review for a completed or processing session.",
|
|
|
|
|
},
|
|
|
|
|
session: {
|
|
|
|
|
path: "/v2/service-sessions/interview",
|
|
|
|
|
queryParams: ["session_id", "goal", "role", "type"],
|
|
|
|
|
usage: "Legacy service-session launcher used by service agent results.",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
usage: "Prefer preview for curator links and setup for mission CTAs that still need user choices.",
|
|
|
|
|
},
|
|
|
|
|
curator: {
|
|
|
|
|
defaultPage: "preview",
|
|
|
|
|
defaultActionLabel: "Open interview preview",
|
|
|
|
|
actionLabels: {
|
|
|
|
|
start: "Start mock",
|
|
|
|
|
review: "Review interview",
|
|
|
|
|
},
|
|
|
|
|
defaultQueryState: {
|
|
|
|
|
type: "behavioral",
|
|
|
|
|
persona: "payal",
|
|
|
|
|
duration: 5,
|
|
|
|
|
difficulty: "medium",
|
|
|
|
|
media: "video",
|
|
|
|
|
},
|
|
|
|
|
toolName: "prepare_interview_preview",
|
|
|
|
|
completionEvents: ["interview.configured", "interview.review_completed", "interview.completed"],
|
|
|
|
|
qscoreSignals: ["communication.interview", "proof.story_bank", "readiness.practice"],
|
|
|
|
|
usage: "Include missionInstanceId, missionId, stageId, curatorTaskId, role, and media when building stateful handoffs.",
|
|
|
|
|
},
|
|
|
|
|
usageDocs: [
|
|
|
|
|
"Call buildServiceLink('interview-service', 'preview', state) for curator handoffs.",
|
|
|
|
|
"Call getServiceEndpoint('interview-service', 'configure') for backend contract metadata.",
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "roleplay-service",
|
|
|
|
|
label: "Roleplay",
|
|
|
|
|
description: "Practice negotiations, recruiter calls, stakeholder conversations, and workplace scenarios.",
|
|
|
|
|
category: "practice",
|
|
|
|
|
enabled: Boolean(config.roleplayServiceUrl),
|
|
|
|
|
featureId: "mock-roleplay",
|
|
|
|
|
promptModulePath: "agents/roleplay.md",
|
|
|
|
|
backend: {
|
|
|
|
|
baseUrl: config.roleplayServiceUrl,
|
|
|
|
|
publicUrl: config.roleplayPublicUrl,
|
|
|
|
|
healthPath: "/health",
|
|
|
|
|
endpoints: {
|
|
|
|
|
health: endpoint("GET", "/health", "Readiness probe.", "Check service availability before a handoff."),
|
|
|
|
|
pageState: endpoint("GET", "/api/v1/roleplays/page-state?user_id=:userId", "Returns usage and personalization state.", "Hydrate roleplay landing/setup pages."),
|
|
|
|
|
configure: endpoint("POST", "/api/v1/roleplays/configure", "Creates a roleplay scenario from user, org, persona, duration, brief, metadata, qscore, and user_context.", "Use for committed scenario generation."),
|
|
|
|
|
preview: endpoint("POST", "/api/v1/roleplays/configure/preview", "Creates a roleplay preview.", "Use for curator/dashboard previews."),
|
|
|
|
|
questions: endpoint("POST", "/api/v1/roleplays/configure/questions", "Edits generated roleplay questions or beats.", "Use after preview edits."),
|
|
|
|
|
approve: endpoint("POST", "/api/v1/roleplays/configure/approve", "Approves a generated roleplay by session_id.", "Use when a user accepts a preview."),
|
|
|
|
|
assignments: endpoint("GET", "/api/v1/roleplays/assignments", "Lists roleplay assignments by email/status/limit.", "Render assigned drills."),
|
|
|
|
|
createAssignments: endpoint("POST", "/api/v1/roleplays/assignments", "Creates roleplay assignments.", "Admin or organization handoffs."),
|
|
|
|
|
unassign: endpoint("POST", "/api/v1/roleplays/assignments/unassign", "Removes roleplay assignments.", "Admin cleanup."),
|
|
|
|
|
resultsBulk: endpoint("POST", "/api/v1/roleplays/results:bulk", "Fetches result summaries for multiple sessions.", "Dashboard history and summaries."),
|
|
|
|
|
review: endpoint("GET", "/api/v1/roleplays/review/:sessionId", "Returns review/status for a roleplay session.", "Poll or open feedback."),
|
|
|
|
|
leaderboard: endpoint("GET", "/api/v1/roleplays/leaderboard", "Returns roleplay leaderboard.", "Leaderboard widgets."),
|
|
|
|
|
artifact: endpoint("GET", "/api/v1/artifacts/:sessionId/:artifactType", "Returns session artifacts.", "Fetch transcript, report, or media artifacts."),
|
|
|
|
|
videoUploadUrl: endpoint("POST", "/api/v1/sessions/:sessionId/video/upload-url", "Returns signed upload instructions.", "Browser upload setup."),
|
|
|
|
|
markVideoUploaded: endpoint("POST", "/api/v1/sessions/:sessionId/video/uploaded", "Marks uploaded video as available.", "Complete upload flow."),
|
|
|
|
|
},
|
|
|
|
|
usage: "Backend callers should use the gateway /services/roleplay/* routes when user auth, mission correlation, and event recording are required.",
|
|
|
|
|
},
|
|
|
|
|
frontend: {
|
|
|
|
|
baseUrl: frontendBaseUrl,
|
|
|
|
|
pages: {
|
|
|
|
|
discovery: {
|
|
|
|
|
path: "/upskilling/roleplay",
|
|
|
|
|
aliases: ["/agents/roleplay"],
|
|
|
|
|
queryParams: ["fresh"],
|
|
|
|
|
usage: "Entry screen for scenario discovery and resume of in-progress roleplay work.",
|
|
|
|
|
},
|
|
|
|
|
setup: {
|
|
|
|
|
path: "/upskilling/roleplay/setup",
|
|
|
|
|
aliases: ["/agents/roleplay/setup"],
|
|
|
|
|
queryParams: ["scenario", "scenario_text", "scenario_name", "from_assignment"],
|
|
|
|
|
usage: "Collects roleplay scenario details and stores configure parameters for the builder.",
|
|
|
|
|
},
|
|
|
|
|
builder: {
|
|
|
|
|
path: "/upskilling/roleplay/builder",
|
|
|
|
|
aliases: ["/agents/roleplay/builder"],
|
|
|
|
|
queryParams: ["sessionId", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "role", "persona", "duration", "mode", "brief"],
|
|
|
|
|
usage: "Curator default handoff for generating or resuming a roleplay plan.",
|
|
|
|
|
},
|
|
|
|
|
feedback: {
|
|
|
|
|
path: "/upskilling/roleplay/feedback",
|
|
|
|
|
queryParams: ["sessionId"],
|
|
|
|
|
usage: "Opens feedback/review for a completed or processing session.",
|
|
|
|
|
},
|
|
|
|
|
session: {
|
|
|
|
|
path: "/v2/service-sessions/roleplay",
|
|
|
|
|
queryParams: ["session_id", "goal", "role", "type"],
|
|
|
|
|
usage: "Legacy service-session launcher used by service agent results.",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
usage: "Prefer builder for curator links and setup for mission CTAs that still need user choices.",
|
|
|
|
|
},
|
|
|
|
|
curator: {
|
|
|
|
|
defaultPage: "builder",
|
|
|
|
|
defaultActionLabel: "Open roleplay preview",
|
|
|
|
|
actionLabels: {
|
|
|
|
|
start: "Run drill",
|
|
|
|
|
review: "Review roleplay",
|
|
|
|
|
},
|
|
|
|
|
defaultQueryState: {
|
|
|
|
|
role: "Professional",
|
|
|
|
|
persona: "emma",
|
|
|
|
|
duration: 5,
|
|
|
|
|
mode: "video",
|
|
|
|
|
},
|
|
|
|
|
toolName: "prepare_roleplay_preview",
|
|
|
|
|
completionEvents: ["roleplay.configured", "roleplay.review_completed", "roleplay.completed"],
|
|
|
|
|
qscoreSignals: ["communication.roleplay", "networking.conversation", "readiness.practice"],
|
|
|
|
|
usage: "Include role, brief, mission state, and curatorTaskId when building stateful handoffs.",
|
|
|
|
|
},
|
|
|
|
|
usageDocs: [
|
|
|
|
|
"Call buildServiceLink('roleplay-service', 'builder', state) for curator handoffs.",
|
|
|
|
|
"Call getServiceEndpoint('roleplay-service', 'configure') for backend contract metadata.",
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "resume-service",
|
|
|
|
|
label: "Resume",
|
|
|
|
|
description: "Build, tailor, analyze, version, and preview resumes.",
|
|
|
|
|
category: "document",
|
|
|
|
|
enabled: Boolean(config.resumeServiceUrl),
|
|
|
|
|
featureId: "resume-building",
|
|
|
|
|
promptModulePath: "agents/resume.md",
|
|
|
|
|
backend: {
|
|
|
|
|
baseUrl: config.resumeServiceUrl,
|
|
|
|
|
publicUrl: config.resumePublicUrl,
|
|
|
|
|
healthPath: "/health",
|
|
|
|
|
endpoints: {
|
|
|
|
|
health: endpoint("GET", "/health", "Readiness probe.", "Check service availability before a handoff."),
|
|
|
|
|
state: endpoint("GET", "/api/state/:clerkId", "Returns user resume-builder state.", "Hydrate profile and personalization context."),
|
|
|
|
|
templates: endpoint("GET", "/api/v1/templates", "Lists resume templates.", "Render template gallery."),
|
|
|
|
|
a2aTask: endpoint("POST", "/a2a/tasks", "Runs resume-builder agent actions for a user_id.", "Agent/curator orchestrated work."),
|
|
|
|
|
listResumes: endpoint("GET", "/api/v1/resumes?clerk_id=:clerkId", "Lists resumes for a Clerk user.", "Resume hub."),
|
|
|
|
|
createResume: endpoint("POST", "/api/v1/resumes", "Creates a resume.", "Resume creation."),
|
|
|
|
|
getResume: endpoint("GET", "/api/v1/resumes/:resumeId", "Reads a resume.", "Resume editor."),
|
|
|
|
|
updateResume: endpoint("PUT", "/api/v1/resumes/:resumeId", "Updates a resume.", "Resume editor saves."),
|
|
|
|
|
analyzeResume: endpoint("POST", "/api/v1/ai/analyze/:resumeId", "Runs AI analysis for a resume.", "Resume score and improvement plan."),
|
|
|
|
|
suggestions: endpoint("GET", "/api/v1/ai/suggestions/:resumeId", "Returns AI suggestions.", "Editor improvement rail."),
|
|
|
|
|
copilot: endpoint("POST", "/api/v1/ai/copilot", "Runs resume copilot.", "Inline editing assistant."),
|
|
|
|
|
versions: endpoint("GET", "/api/v1/resumes/:resumeId/versions", "Lists resume versions.", "Version history."),
|
|
|
|
|
preview: endpoint("GET", "/api/v1/export/resumes/:resumeId/preview", "Returns resume preview.", "PDF/preview surface."),
|
|
|
|
|
},
|
|
|
|
|
usage: "Use gateway /services/resume/* for browser-authenticated requests so Clerk bearer tokens are preserved.",
|
|
|
|
|
},
|
|
|
|
|
frontend: {
|
|
|
|
|
baseUrl: frontendBaseUrl,
|
|
|
|
|
pages: {
|
|
|
|
|
workspace: {
|
|
|
|
|
path: "/opportunities/resume",
|
|
|
|
|
aliases: ["/agents/resume"],
|
|
|
|
|
queryParams: ["tab", "section", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
|
|
|
usage: "Resume hub. Use tab=resumes by default and section to deep-link editor panels.",
|
|
|
|
|
},
|
|
|
|
|
editor: {
|
|
|
|
|
path: "/opportunities/resume/:resumeId",
|
|
|
|
|
queryParams: ["section"],
|
|
|
|
|
usage: "Resume editor for a known resume.",
|
|
|
|
|
},
|
|
|
|
|
templates: {
|
|
|
|
|
path: "/opportunities/resume/templates",
|
|
|
|
|
queryParams: [],
|
|
|
|
|
usage: "Template gallery.",
|
|
|
|
|
},
|
|
|
|
|
session: {
|
|
|
|
|
path: "/v2/service-sessions/resume",
|
|
|
|
|
queryParams: ["goal", "role"],
|
|
|
|
|
usage: "Legacy service-session launcher used by service agent results.",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
usage: "Curator links should open the workspace unless a concrete resumeId is known.",
|
|
|
|
|
},
|
|
|
|
|
curator: {
|
|
|
|
|
defaultPage: "workspace",
|
|
|
|
|
defaultActionLabel: "Open resume workspace",
|
|
|
|
|
actionLabels: {
|
|
|
|
|
start: "Open resume",
|
|
|
|
|
review: "Review resume",
|
|
|
|
|
},
|
|
|
|
|
defaultQueryState: {
|
|
|
|
|
tab: "resumes",
|
|
|
|
|
},
|
|
|
|
|
toolName: "prepare_resume_upload",
|
|
|
|
|
completionEvents: ["resume.analysis_completed", "resume.parsed", "resume.updated"],
|
|
|
|
|
qscoreSignals: ["proof.resume", "readiness.ats", "profile.skills"],
|
|
|
|
|
usage: "Include mission state and optional section when linking into resume work.",
|
|
|
|
|
},
|
|
|
|
|
usageDocs: [
|
|
|
|
|
"Call buildServiceLink('resume-service', 'workspace', { tab: 'resumes' }) for curator handoffs.",
|
|
|
|
|
"Use the resume gateway proxy for browser calls that need Clerk auth.",
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "cover-letter-service",
|
|
|
|
|
label: "Cover Letter",
|
|
|
|
|
description: "Generate, tailor, analyze, version, and preview cover letters.",
|
|
|
|
|
category: "document",
|
|
|
|
|
enabled: Boolean(config.resumeServiceUrl),
|
|
|
|
|
featureId: "cover-letter",
|
|
|
|
|
promptModulePath: "agents/cover-letter.md",
|
|
|
|
|
aliases: ["coverletter-service"],
|
|
|
|
|
backend: {
|
|
|
|
|
baseUrl: config.resumeServiceUrl,
|
|
|
|
|
publicUrl: config.resumePublicUrl,
|
|
|
|
|
healthPath: "/health",
|
|
|
|
|
endpoints: {
|
|
|
|
|
health: endpoint("GET", "/health", "Readiness probe inherited from resume-builder.", "Check resume-builder availability."),
|
|
|
|
|
listCoverLetters: endpoint("GET", "/api/v1/cover-letters", "Lists cover letters.", "Cover-letter hub."),
|
|
|
|
|
createCoverLetter: endpoint("POST", "/api/v1/cover-letters", "Creates a cover letter.", "Manual creation."),
|
|
|
|
|
getCoverLetter: endpoint("GET", "/api/v1/cover-letters/:coverLetterId", "Reads a cover letter.", "Cover-letter editor."),
|
|
|
|
|
updateCoverLetter: endpoint("PUT", "/api/v1/cover-letters/:coverLetterId", "Updates a cover letter.", "Editor saves."),
|
|
|
|
|
generate: endpoint("POST", "/api/v1/cover-letters/generate", "Generates a tailored cover letter.", "Job application handoff."),
|
|
|
|
|
tailor: endpoint("POST", "/api/v1/cover-letters/:coverLetterId/tailor", "Tailors an existing cover letter.", "Application-specific rewrite."),
|
|
|
|
|
analyze: endpoint("POST", "/api/v1/cover-letters/:coverLetterId/analyze", "Analyzes a cover letter.", "Strength and fit scoring."),
|
|
|
|
|
copilot: endpoint("POST", "/api/v1/cover-letters/copilot", "Runs cover-letter copilot.", "Inline editing assistant."),
|
|
|
|
|
preview: endpoint("GET", "/api/v1/export/cover-letters/:coverLetterId/preview", "Returns cover-letter preview.", "Preview/PDF surface."),
|
|
|
|
|
},
|
|
|
|
|
usage: "Cover letters currently live behind resume-builder and the /services/resume/* proxy.",
|
|
|
|
|
},
|
|
|
|
|
frontend: {
|
|
|
|
|
baseUrl: frontendBaseUrl,
|
|
|
|
|
pages: {
|
|
|
|
|
workspace: {
|
|
|
|
|
path: "/opportunities/resume",
|
|
|
|
|
queryParams: ["tab", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
|
|
|
usage: "Open with tab=cover-letters to land in the cover-letter list.",
|
|
|
|
|
},
|
|
|
|
|
generator: {
|
|
|
|
|
path: "/opportunities/cover-letter",
|
|
|
|
|
queryParams: [],
|
|
|
|
|
usage: "Standalone generation entry point.",
|
|
|
|
|
},
|
|
|
|
|
editor: {
|
|
|
|
|
path: "/opportunities/resume/cover-letters/:coverLetterId",
|
|
|
|
|
queryParams: [],
|
|
|
|
|
usage: "Cover-letter editor for a known coverLetterId.",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
usage: "Use workspace with tab=cover-letters for general handoffs.",
|
|
|
|
|
},
|
|
|
|
|
curator: {
|
|
|
|
|
defaultPage: "workspace",
|
|
|
|
|
defaultActionLabel: "Open cover letters",
|
|
|
|
|
actionLabels: {
|
|
|
|
|
start: "Write cover letter",
|
|
|
|
|
review: "Review cover letter",
|
|
|
|
|
},
|
|
|
|
|
defaultQueryState: {
|
|
|
|
|
tab: "cover-letters",
|
|
|
|
|
},
|
|
|
|
|
toolName: "prepare_cover_letter_handoff",
|
|
|
|
|
completionEvents: ["cover_letter.generated", "cover_letter.updated", "cover_letter.analysis_completed"],
|
|
|
|
|
qscoreSignals: ["proof.cover_letter", "readiness.application"],
|
|
|
|
|
usage: "Use for application-specific artifact tasks; share mission/job context in query or payload.",
|
|
|
|
|
},
|
|
|
|
|
usageDocs: [
|
|
|
|
|
"Call buildServiceLink('cover-letter-service', 'workspace', { tab: 'cover-letters' }) for curator handoffs.",
|
|
|
|
|
"Backend endpoint metadata maps to resume-builder cover-letter APIs.",
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "courses-service",
|
|
|
|
|
label: "Courses",
|
|
|
|
|
description: "Create, list, and open upskilling courses.",
|
|
|
|
|
category: "learning",
|
|
|
|
|
enabled: Boolean(config.coursesServiceUrl),
|
|
|
|
|
featureId: "courses",
|
|
|
|
|
promptModulePath: "agents/courses.md",
|
|
|
|
|
aliases: ["course-service"],
|
|
|
|
|
backend: {
|
|
|
|
|
baseUrl: config.coursesServiceUrl,
|
|
|
|
|
publicUrl: config.coursesPublicUrl,
|
|
|
|
|
healthPath: "/api/v1/health",
|
|
|
|
|
endpoints: {
|
|
|
|
|
health: endpoint("GET", "/api/v1/health", "Readiness probe.", "Check service availability."),
|
|
|
|
|
createCourse: endpoint("POST", "/api/v1/courses", "Creates a course.", "Admin or generated course creation."),
|
|
|
|
|
listCourses: endpoint("GET", "/api/v1/courses", "Lists courses with pagination/query filters.", "Course catalog."),
|
|
|
|
|
getCourse: endpoint("GET", "/api/v1/courses/:courseId", "Reads course details.", "Course detail page."),
|
|
|
|
|
},
|
|
|
|
|
usage: "Use for learning plan/course catalog handoffs; course generation stays in the service.",
|
|
|
|
|
},
|
|
|
|
|
frontend: {
|
|
|
|
|
baseUrl: frontendBaseUrl,
|
|
|
|
|
pages: {
|
|
|
|
|
catalog: {
|
|
|
|
|
path: "/upskilling/course",
|
|
|
|
|
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
|
|
|
usage: "Course catalog and upskilling entry point.",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
usage: "Open catalog for general learning handoffs.",
|
|
|
|
|
},
|
|
|
|
|
curator: {
|
|
|
|
|
defaultPage: "catalog",
|
|
|
|
|
defaultActionLabel: "Open courses",
|
|
|
|
|
actionLabels: {
|
|
|
|
|
start: "Start course",
|
|
|
|
|
},
|
|
|
|
|
toolName: "prepare_course_handoff",
|
|
|
|
|
completionEvents: ["course.started", "course.completed"],
|
|
|
|
|
qscoreSignals: ["skills.learning", "readiness.upskilling"],
|
|
|
|
|
usage: "Use for skill-gap tasks that should become learning work.",
|
|
|
|
|
},
|
|
|
|
|
usageDocs: ["Call buildServiceLink('courses-service', 'catalog', state) for course handoffs."],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "assessment-service",
|
|
|
|
|
label: "Assessment",
|
|
|
|
|
description: "Create, list, read, and submit assessments.",
|
|
|
|
|
category: "measurement",
|
|
|
|
|
enabled: Boolean(config.assessmentServiceUrl),
|
|
|
|
|
featureId: "assessment",
|
|
|
|
|
promptModulePath: "agents/assessment.md",
|
|
|
|
|
backend: {
|
|
|
|
|
baseUrl: config.assessmentServiceUrl,
|
|
|
|
|
publicUrl: config.assessmentPublicUrl,
|
|
|
|
|
healthPath: "/api/v1/health",
|
|
|
|
|
endpoints: {
|
|
|
|
|
health: endpoint("GET", "/api/v1/health", "Readiness probe.", "Check service availability."),
|
|
|
|
|
createAssessment: endpoint("POST", "/api/v1/assessments", "Creates an assessment.", "Admin or generated assessment creation."),
|
|
|
|
|
listAssessments: endpoint("GET", "/api/v1/assessments", "Lists assessments with pagination/query filters.", "Assessment catalog."),
|
|
|
|
|
getAssessment: endpoint("GET", "/api/v1/assessments/:assessmentId", "Reads assessment details.", "Assessment page."),
|
|
|
|
|
submitAssessment: endpoint("POST", "/api/v1/assessments/:assessmentId/submit", "Submits answers and returns assessment state.", "Completion flow."),
|
|
|
|
|
},
|
|
|
|
|
usage: "Use for measurement tasks and proof of skill checks.",
|
|
|
|
|
},
|
|
|
|
|
frontend: {
|
|
|
|
|
baseUrl: frontendBaseUrl,
|
|
|
|
|
pages: {
|
|
|
|
|
assessment: {
|
|
|
|
|
path: "/upskilling/assessment",
|
|
|
|
|
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
|
|
|
usage: "Assessment landing and active assessment surface.",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
usage: "Open assessment for skill/readiness measurement handoffs.",
|
|
|
|
|
},
|
|
|
|
|
curator: {
|
|
|
|
|
defaultPage: "assessment",
|
|
|
|
|
defaultActionLabel: "Open assessment",
|
|
|
|
|
actionLabels: {
|
|
|
|
|
start: "Start assessment",
|
|
|
|
|
review: "Review assessment",
|
|
|
|
|
},
|
|
|
|
|
toolName: "prepare_assessment_handoff",
|
|
|
|
|
completionEvents: ["assessment.started", "assessment.submitted", "assessment.completed"],
|
|
|
|
|
qscoreSignals: ["skills.assessment", "readiness.measurement"],
|
|
|
|
|
usage: "Use when a task needs a scoreable assessment rather than practice.",
|
|
|
|
|
},
|
|
|
|
|
usageDocs: ["Call buildServiceLink('assessment-service', 'assessment', state) for assessment handoffs."],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "matchmaking-service",
|
|
|
|
|
label: "Matchmaking",
|
|
|
|
|
description: "Match users to opportunities, employers, mentors, and networking targets.",
|
|
|
|
|
category: "opportunity",
|
|
|
|
|
enabled: Boolean(config.matchmakingServiceUrl),
|
|
|
|
|
featureId: "matchmaking",
|
|
|
|
|
promptModulePath: "agents/matchmaking.md",
|
|
|
|
|
aliases: ["jobs-service"],
|
|
|
|
|
backend: {
|
|
|
|
|
baseUrl: config.matchmakingServiceUrl,
|
|
|
|
|
publicUrl: config.matchmakingPublicUrl,
|
|
|
|
|
healthPath: "/api/v1/health",
|
|
|
|
|
endpoints: {
|
|
|
|
|
health: endpoint("GET", "/api/v1/health", "Readiness probe.", "Check service availability."),
|
|
|
|
|
preferences: endpoint("GET", "/api/v1/preferences/:userId", "Reads matching preferences.", "Hydrate feed filters and personalization."),
|
|
|
|
|
writePreferences: endpoint("POST", "/api/v1/preferences", "Writes matching preferences.", "Preference onboarding."),
|
|
|
|
|
feed: endpoint("GET", "/api/v1/feed", "Returns matched opportunity feed.", "Job/opportunity feed."),
|
|
|
|
|
feedAction: endpoint("POST", "/api/v1/feed/actions", "Records feed actions.", "Save/apply/dismiss tracking."),
|
|
|
|
|
opportunity: endpoint("GET", "/api/v1/opportunities/:opportunityId", "Reads opportunity details.", "Opportunity detail panel."),
|
|
|
|
|
a2aTask: endpoint("POST", "/a2a/tasks", "Runs matching agent tasks.", "Agent/curator orchestrated work."),
|
|
|
|
|
},
|
|
|
|
|
usage: "Use for opportunity matching and feed intelligence; keep user-specific actions through authenticated gateway routes when added.",
|
|
|
|
|
},
|
|
|
|
|
frontend: {
|
|
|
|
|
baseUrl: frontendBaseUrl,
|
|
|
|
|
pages: {
|
|
|
|
|
jobs: {
|
|
|
|
|
path: "/opportunities/job-matching",
|
|
|
|
|
aliases: ["/pathways"],
|
|
|
|
|
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "query", "role", "location"],
|
|
|
|
|
usage: "Job/opportunity matching dashboard.",
|
|
|
|
|
},
|
|
|
|
|
pathways: {
|
|
|
|
|
path: "/career-pathways",
|
|
|
|
|
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
|
|
|
usage: "Career pathways dashboard and pathway list.",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
usage: "Use jobs for immediate matching and pathways for career-plan handoffs.",
|
|
|
|
|
},
|
|
|
|
|
curator: {
|
|
|
|
|
defaultPage: "jobs",
|
|
|
|
|
defaultActionLabel: "Open matches",
|
|
|
|
|
actionLabels: {
|
|
|
|
|
start: "View matches",
|
|
|
|
|
review: "Review matches",
|
|
|
|
|
},
|
|
|
|
|
toolName: "prepare_matchmaking_handoff",
|
|
|
|
|
completionEvents: ["matchmaking.feed_viewed", "matchmaking.match_saved", "matchmaking.preference_updated"],
|
|
|
|
|
qscoreSignals: ["market.matches", "networking.opportunities"],
|
|
|
|
|
usage: "Use for immediate opportunity matching or mentor/network suggestions.",
|
|
|
|
|
},
|
|
|
|
|
usageDocs: [
|
|
|
|
|
"Call buildServiceLink('matchmaking-service', 'jobs', state) for job matching.",
|
|
|
|
|
"Use pathways-service for generated career pathway plans.",
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "pathways-service",
|
|
|
|
|
label: "Pathways",
|
|
|
|
|
description: "Generate, activate, and report on personalized career pathways.",
|
|
|
|
|
category: "opportunity",
|
|
|
|
|
enabled: Boolean(config.pathwaysServiceUrl),
|
|
|
|
|
featureId: "pathways",
|
|
|
|
|
promptModulePath: "agents/pathways.md",
|
|
|
|
|
aliases: ["career-pathways-service"],
|
|
|
|
|
backend: {
|
|
|
|
|
baseUrl: config.pathwaysServiceUrl,
|
|
|
|
|
publicUrl: config.pathwaysPublicUrl,
|
|
|
|
|
healthPath: "/api/v1/health",
|
|
|
|
|
endpoints: {
|
|
|
|
|
health: endpoint("GET", "/api/v1/health", "Readiness probe.", "Check service availability."),
|
|
|
|
|
state: endpoint("GET", "/api/state/:clerkId", "Reads pathway state.", "Hydrate pathway dashboard."),
|
|
|
|
|
profileIngest: endpoint("POST", "/api/v1/profiles/ingest", "Ingests a profile for pathway generation.", "Profile setup."),
|
|
|
|
|
questionnaire: endpoint("GET", "/api/v1/questionnaires/:userId", "Reads pathway questionnaire.", "Questionnaire resume."),
|
|
|
|
|
createQuestionnaire: endpoint("POST", "/api/v1/questionnaires", "Creates questionnaire answers.", "Pathway onboarding."),
|
|
|
|
|
generatePathway: endpoint("POST", "/api/v1/pathways/generate", "Generates a pathway.", "Career-plan generation."),
|
|
|
|
|
activatePathway: endpoint("POST", "/api/v1/pathways/:pathwayId/activate", "Activates a pathway.", "Commit chosen pathway."),
|
|
|
|
|
getPathway: endpoint("GET", "/api/v1/pathways/:pathwayId", "Reads pathway details.", "Pathway detail."),
|
|
|
|
|
weeklyPlan: endpoint("GET", "/api/v1/pathways/:pathwayId/weekly-plan", "Reads weekly plan.", "Planner UI."),
|
|
|
|
|
report: endpoint("GET", "/api/v1/pathways/:pathwayId/report", "Reads pathway report.", "Progress report."),
|
|
|
|
|
opportunities: endpoint("GET", "/api/v1/pathways/:pathwayId/opportunities", "Reads pathway opportunities.", "Pathway opportunity recommendations."),
|
|
|
|
|
a2aTask: endpoint("POST", "/a2a/tasks", "Runs pathway agent tasks.", "Agent/curator orchestrated work."),
|
|
|
|
|
},
|
|
|
|
|
usage: "Use for generated pathway plans and recommendation context. The live container is currently healthchecked separately from matchmaking.",
|
|
|
|
|
},
|
|
|
|
|
frontend: {
|
|
|
|
|
baseUrl: frontendBaseUrl,
|
|
|
|
|
pages: {
|
|
|
|
|
dashboard: {
|
|
|
|
|
path: "/career-pathways",
|
|
|
|
|
aliases: ["/pathways"],
|
|
|
|
|
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
|
|
|
usage: "Pathway list/dashboard.",
|
|
|
|
|
},
|
|
|
|
|
generate: {
|
|
|
|
|
path: "/career-pathways/generate",
|
|
|
|
|
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
|
|
|
usage: "Pathway generation flow.",
|
|
|
|
|
},
|
|
|
|
|
questionnaire: {
|
|
|
|
|
path: "/career-pathways/questionnaire",
|
|
|
|
|
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
|
|
|
usage: "Pathway questionnaire flow.",
|
|
|
|
|
},
|
|
|
|
|
detail: {
|
|
|
|
|
path: "/career-pathways/dashboard/:pathwayId",
|
|
|
|
|
queryParams: [],
|
|
|
|
|
usage: "Pathway detail dashboard.",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
usage: "Use dashboard for general pathway handoffs and generate/questionnaire for guided setup.",
|
|
|
|
|
},
|
|
|
|
|
curator: {
|
|
|
|
|
defaultPage: "dashboard",
|
|
|
|
|
defaultActionLabel: "Open pathways",
|
|
|
|
|
actionLabels: {
|
|
|
|
|
start: "Build pathway",
|
|
|
|
|
review: "Review pathway",
|
|
|
|
|
},
|
|
|
|
|
toolName: "prepare_pathways_handoff",
|
|
|
|
|
completionEvents: ["pathway.generated", "pathway.activated", "pathway.report_viewed"],
|
|
|
|
|
qscoreSignals: ["readiness.pathway", "market.plan"],
|
|
|
|
|
usage: "Use for strategic career pathway planning rather than immediate job matching.",
|
|
|
|
|
},
|
|
|
|
|
usageDocs: ["Call buildServiceLink('pathways-service', 'dashboard', state) for pathway handoffs."],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "qscore-service",
|
|
|
|
|
label: "QScore",
|
|
|
|
|
description: "Analyze readiness signals and expose score projections.",
|
|
|
|
|
category: "measurement",
|
|
|
|
|
enabled: Boolean(config.qscoreServiceUrl),
|
|
|
|
|
featureId: "q-score",
|
|
|
|
|
promptModulePath: "agents/qscore.md",
|
|
|
|
|
aliases: ["q-score-service"],
|
|
|
|
|
backend: {
|
|
|
|
|
baseUrl: config.qscoreServiceUrl,
|
|
|
|
|
publicUrl: config.qscorePublicUrl,
|
|
|
|
|
healthPath: "/health",
|
|
|
|
|
endpoints: {
|
|
|
|
|
health: endpoint("GET", "/health", "Readiness probe.", "Check service availability."),
|
|
|
|
|
currentGateway: endpoint("GET", "/services/qscore/current", "Backend-projected current score and latest signals.", "Dashboard QScore panel."),
|
|
|
|
|
ingest: endpoint("POST", "/api/v1/signals", "Ingests score signals when available.", "Service-to-service signal updates."),
|
|
|
|
|
compute: endpoint("POST", "/api/v1/score/compute", "Computes or refreshes score when available.", "Score recalculation."),
|
|
|
|
|
},
|
|
|
|
|
usage: "Use backend gateway /services/qscore/current for dashboard-safe reads; direct service APIs vary by QScore deployment.",
|
|
|
|
|
},
|
|
|
|
|
frontend: {
|
|
|
|
|
baseUrl: frontendBaseUrl,
|
|
|
|
|
pages: {
|
|
|
|
|
dashboard: {
|
|
|
|
|
path: "/home",
|
|
|
|
|
aliases: ["/analytics"],
|
|
|
|
|
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
|
|
|
usage: "Home dashboard with QScore panel.",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
usage: "Open home dashboard for QScore review until a dedicated analytics route exists.",
|
|
|
|
|
},
|
|
|
|
|
curator: {
|
|
|
|
|
defaultPage: "dashboard",
|
|
|
|
|
defaultActionLabel: "Review QScore",
|
|
|
|
|
actionLabels: {
|
|
|
|
|
review: "Review QScore",
|
|
|
|
|
},
|
|
|
|
|
toolName: "prepare_qscore_review",
|
|
|
|
|
completionEvents: ["qscore.updated", "qscore.signal_projected"],
|
|
|
|
|
qscoreSignals: ["qscore.updated", "qscore.signal_projected"],
|
|
|
|
|
usage: "Use for measurement and projected readiness review tasks.",
|
|
|
|
|
},
|
|
|
|
|
usageDocs: ["Call buildServiceLink('qscore-service', 'dashboard', state) for QScore handoffs."],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "social-branding-service",
|
|
|
|
|
label: "Social Branding",
|
|
|
|
|
description: "Build and optimize professional profile, LinkedIn, content, and brand signals.",
|
|
|
|
|
category: "profile",
|
|
|
|
|
enabled: Boolean(config.socialBrandingServiceUrl),
|
|
|
|
|
featureId: "social-branding",
|
|
|
|
|
promptModulePath: "agents/social-branding.md",
|
|
|
|
|
aliases: ["social-service"],
|
|
|
|
|
backend: {
|
|
|
|
|
baseUrl: config.socialBrandingServiceUrl,
|
|
|
|
|
publicUrl: config.socialBrandingPublicUrl,
|
|
|
|
|
healthPath: "/health",
|
|
|
|
|
endpoints: {
|
|
|
|
|
health: endpoint("GET", "/health", "Readiness probe.", "Check service availability."),
|
|
|
|
|
state: endpoint("GET", "/api/state/:clerkId", "Reads social/profile state.", "Hydrate personalization context."),
|
|
|
|
|
profile: endpoint("GET", "/api/v1/profile", "Reads profile data when available.", "Social profile page."),
|
|
|
|
|
linkedin: endpoint("POST", "/api/v1/linkedin", "Connects or imports LinkedIn data when available.", "LinkedIn onboarding."),
|
|
|
|
|
analyze: endpoint("POST", "/api/v1/analyze", "Analyzes profile/social brand when available.", "Brand improvement tasks."),
|
|
|
|
|
},
|
|
|
|
|
usage: "Use /services/social/* gateway proxy for browser-authenticated profile calls.",
|
|
|
|
|
},
|
|
|
|
|
frontend: {
|
|
|
|
|
baseUrl: frontendBaseUrl,
|
|
|
|
|
pages: {
|
|
|
|
|
profile: {
|
|
|
|
|
path: "/opportunities/social-media",
|
|
|
|
|
aliases: ["/social"],
|
|
|
|
|
queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"],
|
|
|
|
|
usage: "Social/profile improvement page.",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
usage: "Open profile for branding, LinkedIn, and social proof handoffs.",
|
|
|
|
|
},
|
|
|
|
|
curator: {
|
|
|
|
|
defaultPage: "profile",
|
|
|
|
|
defaultActionLabel: "Open social profile flow",
|
|
|
|
|
actionLabels: {
|
|
|
|
|
start: "Improve profile",
|
|
|
|
|
review: "Review profile",
|
|
|
|
|
},
|
|
|
|
|
toolName: "prepare_social_branding_handoff",
|
|
|
|
|
completionEvents: ["social.profile_updated", "social.linkedin_connected", "social.branding_analyzed"],
|
|
|
|
|
qscoreSignals: ["profile.linkedin", "proof.visibility", "networking.brand"],
|
|
|
|
|
usage: "Use for profile visibility, LinkedIn cleanup, and social proof tasks.",
|
|
|
|
|
},
|
|
|
|
|
usageDocs: ["Call buildServiceLink('social-branding-service', 'profile', state) for social branding handoffs."],
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const serviceAliases = new Map<string, ServiceId>();
|
|
|
|
|
for (const service of serviceRegistry) {
|
|
|
|
|
serviceAliases.set(service.id, service.id);
|
|
|
|
|
for (const alias of service.aliases ?? []) serviceAliases.set(alias, service.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function normalizeServiceId(serviceId?: string | null): ServiceId | undefined {
|
|
|
|
|
if (!serviceId) return undefined;
|
|
|
|
|
return serviceAliases.get(serviceId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function listServices() {
|
|
|
|
|
return serviceRegistry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getService(serviceId?: string | null) {
|
|
|
|
|
const normalized = normalizeServiceId(serviceId);
|
|
|
|
|
return normalized ? serviceRegistry.find((service) => service.id === normalized) : undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getServiceBackend(serviceId?: string | null) {
|
|
|
|
|
return getService(serviceId)?.backend;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getServiceFrontend(serviceId?: string | null) {
|
|
|
|
|
return getService(serviceId)?.frontend;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getServiceEndpoint(serviceId: string | undefined, endpointId: string) {
|
|
|
|
|
return getService(serviceId)?.backend.endpoints[endpointId];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getServiceUsageDocs(serviceId?: string | null) {
|
|
|
|
|
return getService(serviceId)?.usageDocs ?? [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolvePage(service: ServiceRecord, pageId?: string) {
|
|
|
|
|
const selectedPageId = pageId || service.curator.defaultPage;
|
|
|
|
|
const direct = service.frontend.pages[selectedPageId];
|
|
|
|
|
if (direct) return direct;
|
|
|
|
|
return Object.values(service.frontend.pages).find((page) => page.aliases?.includes(selectedPageId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildServiceLink(serviceId: string | undefined, pageId?: string, state: QueryState = {}) {
|
|
|
|
|
const service = getService(serviceId);
|
|
|
|
|
if (!service) return undefined;
|
|
|
|
|
const page = resolvePage(service, pageId);
|
|
|
|
|
if (!page) return undefined;
|
|
|
|
|
const includeDefaultState = !pageId || pageId === service.curator.defaultPage;
|
|
|
|
|
return appendQuery(page.path, {
|
|
|
|
|
...(includeDefaultState ? service.curator.defaultQueryState : {}),
|
|
|
|
|
...state,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function listServicesForCatalog() {
|
|
|
|
|
return serviceRegistry.map((service) => ({
|
|
|
|
|
id: service.id,
|
|
|
|
|
label: service.label,
|
|
|
|
|
description: service.description,
|
|
|
|
|
category: service.category,
|
|
|
|
|
enabled: service.enabled,
|
|
|
|
|
featureId: service.featureId,
|
|
|
|
|
backend: {
|
|
|
|
|
publicUrl: service.backend.publicUrl,
|
|
|
|
|
healthPath: service.backend.healthPath,
|
|
|
|
|
endpoints: service.backend.endpoints,
|
|
|
|
|
usage: service.backend.usage,
|
|
|
|
|
},
|
|
|
|
|
frontend: service.frontend,
|
|
|
|
|
curator: service.curator,
|
|
|
|
|
usageDocs: service.usageDocs,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildServiceSessionPath(
|
|
|
|
|
serviceId: MissionServiceId,
|
|
|
|
|
detail?: Record<string, unknown>,
|
|
|
|
|
@@ -56,7 +840,7 @@ export function buildServiceSessionPath(
|
|
|
|
|
|
|
|
|
|
if (serviceId === "interview-service") {
|
|
|
|
|
if (!sessionId) return undefined;
|
|
|
|
|
return appendQuery("/v2/service-sessions/interview", {
|
|
|
|
|
return buildServiceLink(serviceId, "session", {
|
|
|
|
|
session_id: sessionId,
|
|
|
|
|
goal,
|
|
|
|
|
role: getString(detail?.target_role) ?? goal ?? "Interview practice",
|
|
|
|
|
@@ -66,7 +850,7 @@ export function buildServiceSessionPath(
|
|
|
|
|
|
|
|
|
|
if (serviceId === "roleplay-service") {
|
|
|
|
|
if (!sessionId) return undefined;
|
|
|
|
|
return appendQuery("/v2/service-sessions/roleplay", {
|
|
|
|
|
return buildServiceLink(serviceId, "session", {
|
|
|
|
|
session_id: sessionId,
|
|
|
|
|
goal,
|
|
|
|
|
role: getString(detail?.target_role) ?? goal ?? "Roleplay practice",
|
|
|
|
|
@@ -74,30 +858,23 @@ export function buildServiceSessionPath(
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return appendQuery("/v2/service-sessions/resume", {
|
|
|
|
|
return buildServiceLink(serviceId, "session", {
|
|
|
|
|
goal,
|
|
|
|
|
role: goal,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildMissionServiceRoute(input: MissionRouteInput) {
|
|
|
|
|
const baseParams = {
|
|
|
|
|
const pageId = input.serviceId === "resume-service" ? "workspace" : "setup";
|
|
|
|
|
return buildServiceLink(input.serviceId, pageId, {
|
|
|
|
|
source: "mission",
|
|
|
|
|
missionInstanceId: input.missionInstanceId,
|
|
|
|
|
missionId: input.missionId,
|
|
|
|
|
stageId: input.stageId,
|
|
|
|
|
goal: input.goal,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (input.serviceId === "interview-service") {
|
|
|
|
|
return appendQuery("/agents/interview/setup", baseParams);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (input.serviceId === "roleplay-service") {
|
|
|
|
|
return appendQuery("/agents/roleplay/setup", baseParams);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return appendQuery("/agents/resume", baseParams);
|
|
|
|
|
role: input.goal,
|
|
|
|
|
type: input.serviceId === "interview-service" ? "behavioral" : undefined,
|
|
|
|
|
}) ?? appendQuery("/missions/active", { missionInstanceId: input.missionInstanceId });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function curatorBaseParams(input: CuratorRouteInput) {
|
|
|
|
|
@@ -111,85 +888,64 @@ function curatorBaseParams(input: CuratorRouteInput) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildCuratorServiceRoute(input: CuratorRouteInput) {
|
|
|
|
|
if (input.serviceId === "interview-service") {
|
|
|
|
|
return appendQuery("/agents/interview/preview", {
|
|
|
|
|
...curatorBaseParams(input),
|
|
|
|
|
role: input.targetRole?.trim() || "Product Manager",
|
|
|
|
|
type: "behavioral",
|
|
|
|
|
persona: input.personaId ?? "payal",
|
|
|
|
|
duration: input.durationMinutes ?? 5,
|
|
|
|
|
difficulty: input.difficulty ?? "medium",
|
|
|
|
|
media: input.requestedMode ?? "video",
|
|
|
|
|
});
|
|
|
|
|
const service = getService(input.serviceId);
|
|
|
|
|
if (!service) {
|
|
|
|
|
return input.missionInstanceId
|
|
|
|
|
? appendQuery("/missions/active", { missionInstanceId: input.missionInstanceId })
|
|
|
|
|
: "/missions/active";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (input.serviceId === "roleplay-service") {
|
|
|
|
|
return appendQuery("/agents/roleplay/builder", {
|
|
|
|
|
...curatorBaseParams(input),
|
|
|
|
|
role: input.targetRole?.trim() || "Professional",
|
|
|
|
|
persona: input.personaId ?? "emma",
|
|
|
|
|
duration: input.durationMinutes ?? 5,
|
|
|
|
|
mode: input.requestedMode ?? "video",
|
|
|
|
|
brief: input.roleplayBrief,
|
|
|
|
|
});
|
|
|
|
|
const state: QueryState = {
|
|
|
|
|
...curatorBaseParams(input),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (service.id === "interview-service") {
|
|
|
|
|
state.role = input.targetRole?.trim() || "Product Manager";
|
|
|
|
|
state.type = "behavioral";
|
|
|
|
|
state.persona = input.personaId ?? "payal";
|
|
|
|
|
state.duration = input.durationMinutes ?? 5;
|
|
|
|
|
state.difficulty = input.difficulty ?? "medium";
|
|
|
|
|
state.media = input.requestedMode ?? "video";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (input.serviceId === "resume-service") {
|
|
|
|
|
return appendQuery("/agents/resume", curatorBaseParams(input));
|
|
|
|
|
}
|
|
|
|
|
if (input.serviceId === "qscore-service") {
|
|
|
|
|
return appendQuery("/analytics", curatorBaseParams(input));
|
|
|
|
|
}
|
|
|
|
|
if (input.serviceId === "social-branding-service") {
|
|
|
|
|
return appendQuery("/social", curatorBaseParams(input));
|
|
|
|
|
}
|
|
|
|
|
if (input.serviceId === "matchmaking-service") {
|
|
|
|
|
return appendQuery("/pathways", curatorBaseParams(input));
|
|
|
|
|
if (service.id === "roleplay-service") {
|
|
|
|
|
state.role = input.targetRole?.trim() || "Professional";
|
|
|
|
|
state.persona = input.personaId ?? "emma";
|
|
|
|
|
state.duration = input.durationMinutes ?? 5;
|
|
|
|
|
state.mode = input.requestedMode ?? "video";
|
|
|
|
|
state.brief = input.roleplayBrief;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return input.missionInstanceId
|
|
|
|
|
? appendQuery("/missions/active", { missionInstanceId: input.missionInstanceId })
|
|
|
|
|
: "/missions/active";
|
|
|
|
|
return buildServiceLink(service.id, service.curator.defaultPage, state)
|
|
|
|
|
?? appendQuery("/missions/active", { missionInstanceId: input.missionInstanceId });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getServiceDisplayName(serviceId?: CuratorServiceId, fallback = "Mission planner") {
|
|
|
|
|
if (serviceId === "interview-service") return "Interview service";
|
|
|
|
|
if (serviceId === "roleplay-service") return "Roleplay service";
|
|
|
|
|
if (serviceId === "resume-service") return "Resume service";
|
|
|
|
|
if (serviceId === "qscore-service") return "Q Score service";
|
|
|
|
|
if (serviceId === "social-branding-service") return "Social branding service";
|
|
|
|
|
if (serviceId === "matchmaking-service") return "Pathways service";
|
|
|
|
|
return fallback;
|
|
|
|
|
export function getServiceDisplayName(serviceId?: string, fallback = "Mission planner") {
|
|
|
|
|
return getService(serviceId)?.label ?? fallback;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getServiceToolName(serviceId?: CuratorServiceId) {
|
|
|
|
|
if (serviceId === "interview-service") return "prepare_interview_preview";
|
|
|
|
|
if (serviceId === "roleplay-service") return "prepare_roleplay_preview";
|
|
|
|
|
if (serviceId === "resume-service") return "prepare_resume_upload";
|
|
|
|
|
if (serviceId === "qscore-service") return "prepare_qscore_review";
|
|
|
|
|
return "prepare_mission_step";
|
|
|
|
|
export function getServiceToolName(serviceId?: string) {
|
|
|
|
|
return getService(serviceId)?.curator.toolName ?? "prepare_mission_step";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getServiceCompletionEvents(serviceId?: CuratorServiceId) {
|
|
|
|
|
if (serviceId === "interview-service") {
|
|
|
|
|
return ["interview.configured", "interview.review_completed", "interview.completed"];
|
|
|
|
|
}
|
|
|
|
|
if (serviceId === "roleplay-service") {
|
|
|
|
|
return ["roleplay.configured", "roleplay.review_completed", "roleplay.completed"];
|
|
|
|
|
}
|
|
|
|
|
if (serviceId === "resume-service") {
|
|
|
|
|
return ["resume.analysis_completed", "resume.parsed", "resume.updated"];
|
|
|
|
|
}
|
|
|
|
|
if (serviceId === "qscore-service") {
|
|
|
|
|
return ["qscore.updated", "qscore.signal_projected"];
|
|
|
|
|
}
|
|
|
|
|
return ["curator.task.completed"];
|
|
|
|
|
export function getCompletionEvents(serviceId?: string) {
|
|
|
|
|
return getService(serviceId)?.curator.completionEvents ?? ["curator.task.completed"];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getServiceActionLabel(task: CuratorTask) {
|
|
|
|
|
if (task.serviceId === "interview-service") return "Open interview preview";
|
|
|
|
|
if (task.serviceId === "roleplay-service") return "Open roleplay preview";
|
|
|
|
|
if (task.serviceId === "resume-service") return "Open resume workspace";
|
|
|
|
|
if (task.serviceId === "qscore-service") return "Review Q Score";
|
|
|
|
|
return task.cta || "Open";
|
|
|
|
|
export const getServiceCompletionEvents = getCompletionEvents;
|
|
|
|
|
|
|
|
|
|
export function getServiceActionLabel(serviceId?: string, actionId?: string): string;
|
|
|
|
|
export function getServiceActionLabel(task: Pick<CuratorTask, "serviceId" | "cta">): string;
|
|
|
|
|
export function getServiceActionLabel(
|
|
|
|
|
input?: string | Pick<CuratorTask, "serviceId" | "cta">,
|
|
|
|
|
actionId?: string,
|
|
|
|
|
) {
|
|
|
|
|
if (typeof input === "object") {
|
|
|
|
|
const service = getService(input.serviceId);
|
|
|
|
|
if (actionId && service?.curator.actionLabels?.[actionId]) return service.curator.actionLabels[actionId];
|
|
|
|
|
return service?.curator.defaultActionLabel ?? input.cta ?? "Open";
|
|
|
|
|
}
|
|
|
|
|
const service = getService(input);
|
|
|
|
|
if (actionId && service?.curator.actionLabels?.[actionId]) return service.curator.actionLabels[actionId];
|
|
|
|
|
return service?.curator.defaultActionLabel ?? "Open";
|
|
|
|
|
}
|
|
|
|
|
|