diff --git a/agents/job-search.md b/agents/job-search.md index 214567d..c4d5689 100644 --- a/agents/job-search.md +++ b/agents/job-search.md @@ -2,6 +2,7 @@ id: job-search name: Job Search Agent role: Opportunity Scout +service: matchmaking-service tools: - search_jobs - rank_opportunities diff --git a/src/actors/user-actor.ts b/src/actors/user-actor.ts index 81c5cfa..2984170 100644 --- a/src/actors/user-actor.ts +++ b/src/actors/user-actor.ts @@ -11,6 +11,7 @@ import { getSubAgentModules, } from "../lib/prompt-loader.js"; import { + buildServiceSessionUrl, runServiceAgentProbe, type ServiceAgentResult, } from "../services/service-agents.js"; @@ -532,11 +533,9 @@ export const userActor = actor({ moduleName: m.name, status: m.status, sessionId: detail?.session_id as string | undefined, - sessionUrl: m.service === "interview-service" - ? `http://localhost:8007/api/v1/demo?session_id=${detail?.session_id ?? ""}` - : m.service === "roleplay-service" - ? `http://localhost:8008/api/v1/demo?session_id=${detail?.session_id ?? ""}` - : undefined, + sessionUrl: typeof detail?.ui_session_url === "string" + ? detail.ui_session_url + : buildServiceSessionUrl(m.service, detail, c.state.workflowGoal), summary: m.lastResult?.summary, }; }), diff --git a/src/config.ts b/src/config.ts index c96e0fb..98736d5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -56,6 +56,12 @@ export const config = { process.env.QSCORE_SERVICE_URL ?? "http://localhost:8000", resumeServiceUrl: process.env.RESUME_SERVICE_URL ?? "http://localhost:8002", + matchmakingServiceUrl: + process.env.MATCHMAKING_SERVICE_URL ?? "http://localhost:8006", + workflowsDashboardUrl: + process.env.WORKFLOWS_DASHBOARD_URL ?? + process.env.FRONTEND_ORIGIN ?? + "http://localhost:3000", // ── Central Gitea (one org-wide instance, changes.md §2A) ── giteaUrl: process.env.GITEA_URL ?? "http://127.0.0.1:3001", diff --git a/src/lib/prompt-loader.ts b/src/lib/prompt-loader.ts index 78a6178..4ef3a58 100644 --- a/src/lib/prompt-loader.ts +++ b/src/lib/prompt-loader.ts @@ -9,7 +9,7 @@ export type SubAgentModule = { name: string; role: string; description: string; - service?: "interview-service" | "roleplay-service" | "qscore-service" | "resume-service"; + service?: "interview-service" | "roleplay-service" | "qscore-service" | "resume-service" | "matchmaking-service"; toolNames: string[]; }; @@ -122,7 +122,8 @@ export async function loadPromptsFromDisk(): Promise { service !== "interview-service" && service !== "roleplay-service" && service !== "qscore-service" && - service !== "resume-service" + service !== "resume-service" && + service !== "matchmaking-service" ) { log.warn({ file: filename, service }, "unknown service value — treating as no service"); } @@ -133,7 +134,7 @@ export async function loadPromptsFromDisk(): Promise { role: data.role ?? data.name, description: body || `Agent module: ${data.name}`, service: service && - ["interview-service", "roleplay-service", "qscore-service", "resume-service"].includes(service) + ["interview-service", "roleplay-service", "qscore-service", "resume-service", "matchmaking-service"].includes(service) ? (service as SubAgentModule["service"]) : undefined, toolNames: data.tools ?? [], diff --git a/src/routes/chat.ts b/src/routes/chat.ts index 3aa8814..5de38a8 100644 --- a/src/routes/chat.ts +++ b/src/routes/chat.ts @@ -8,6 +8,7 @@ import type { LlmMessage } from "../lib/llm.js"; import { createChatCompletion } from "../lib/llm.js"; import { buildUnifiedSystemPrompt } from "../agents/catalog.js"; import { + buildServiceSessionUrl, runServiceAgentProbe, type ServiceAgentResult, } from "../services/service-agents.js"; @@ -210,7 +211,9 @@ export function chatRoutes() { moduleName: "Sara", status: "done", sessionId: detail.session_id as string, - sessionUrl: `http://localhost:8007/api/v1/demo?session_id=${detail.session_id ?? ""}`, + sessionUrl: typeof detail.ui_session_url === "string" + ? detail.ui_session_url + : buildServiceSessionUrl("interview-service", detail, String(toolCall.arguments.target_role ?? "general preparation")), summary: toolResult.summary, }); } @@ -228,7 +231,9 @@ export function chatRoutes() { moduleName: "Emily", status: "done", sessionId: detail.session_id as string, - sessionUrl: `http://localhost:8008/api/v1/demo?session_id=${detail.session_id ?? ""}`, + sessionUrl: typeof detail.ui_session_url === "string" + ? detail.ui_session_url + : buildServiceSessionUrl("roleplay-service", detail, String(toolCall.arguments.goal ?? "general practice")), summary: toolResult.summary, }); } @@ -240,7 +245,16 @@ export function chatRoutes() { { userId, goal: String(toolCall.arguments.goal ?? "general") }, ); if (toolResult.status === "ok") { - sessions.push({ moduleId: "resume", moduleName: "Resume Agent", status: "done", summary: toolResult.summary }); + const detail = toolResult.detail as Record | undefined; + sessions.push({ + moduleId: "resume", + moduleName: "Resume Agent", + status: "done", + sessionUrl: typeof detail?.ui_session_url === "string" + ? detail.ui_session_url + : buildServiceSessionUrl("resume-service", detail, String(toolCall.arguments.goal ?? "general")), + summary: toolResult.summary, + }); } break; } diff --git a/src/services/service-agents.ts b/src/services/service-agents.ts index f11d4a4..fdc510e 100644 --- a/src/services/service-agents.ts +++ b/src/services/service-agents.ts @@ -23,6 +23,39 @@ export type ServiceAgentContext = { goal: string; }; +export function buildServiceSessionUrl( + service: string | undefined, + detail: Record | undefined, + goal?: string, +): string | undefined { + const base = config.workflowsDashboardUrl.replace(/\/$/, ""); + const sessionId = detail?.session_id ?? detail?.sessionId; + const params = new URLSearchParams(); + if (sessionId && typeof sessionId === "string") params.set("session_id", sessionId); + if (goal) params.set("goal", goal); + + if (service === "interview-service") { + if (!sessionId || typeof sessionId !== "string") return undefined; + params.set("role", String(detail?.target_role ?? goal ?? "Interview practice")); + params.set("type", String(detail?.interview_type ?? "behavioral")); + return `${base}/v2/service-sessions/interview?${params.toString()}`; + } + + if (service === "roleplay-service") { + if (!sessionId || typeof sessionId !== "string") return undefined; + params.set("role", String(detail?.target_role ?? goal ?? "Roleplay practice")); + params.set("type", String(detail?.roleplay_type ?? "custom")); + return `${base}/v2/service-sessions/roleplay?${params.toString()}`; + } + + if (service === "resume-service") { + if (goal) params.set("role", goal); + return `${base}/v2/service-sessions/resume${params.size ? `?${params.toString()}` : ""}`; + } + + return undefined; +} + function stableUuid(input: string): string { const hex = createHash("sha256").update(input).digest("hex").slice(0, 32); return [ @@ -97,7 +130,12 @@ async function runSaraInterview(ctx: ServiceAgentContext): Promise ?? {}), goal: ctx.goal, + ui_session_url: buildServiceSessionUrl("resume-service", undefined, ctx.goal), recommendation: "Use the AI analysis and copilot tools to tailor bullet points, add missing keywords, and optimize for ATS.", }, }; @@ -289,6 +333,46 @@ async function runResumeTailor(ctx: ServiceAgentContext): Promise { + const matchmakingUserId = stableUuid(ctx.userId); + const query = new URLSearchParams({ + user_id: matchmakingUserId, + top_n: "6", + threshold: "60", + recompute: "true", + }); + + try { + const detail = await serviceJson>( + config.matchmakingServiceUrl, + `/api/v1/feed?${query.toString()}`, + { method: "GET" }, + ); + const items = Array.isArray(detail.items) + ? detail.items + : Array.isArray(detail.feed_items) + ? detail.feed_items + : []; + + return { + status: "ok", + summary: items.length > 0 + ? `Scout pulled ${items.length} ranked opportunities from matchmaking for ${ctx.goal}.` + : `Scout asked matchmaking to refresh opportunities for ${ctx.goal}. Add preferences if the feed is empty.`, + detail: { + ...detail, + matchmaking_user_id: matchmakingUserId, + goal: ctx.goal, + }, + }; + } catch (err) { + return { + status: "unavailable", + summary: `Matchmaking service unavailable: ${err instanceof Error ? err.message : String(err)}`, + }; + } +} + export async function runServiceAgentProbe( agent: ServiceAgentRef, ctx?: ServiceAgentContext, @@ -311,6 +395,10 @@ export async function runServiceAgentProbe( return ctx ? await runResumeTailor(ctx) : healthCheck(config.resumeServiceUrl, "Resume Agent / resume-service"); + case "matchmaking-service": + return ctx + ? await runMatchmaking(ctx) + : healthCheck(config.matchmakingServiceUrl, "Scout / matchmaking-service"); default: return { status: "local",