7 Commits

Author SHA1 Message Date
94d826347b Merge remote-tracking branch 'origin/main' into feat/integration-new-ui-service
# Conflicts:
#	Dockerfile
#	agents/job-search.md
#	docker-compose.yml
#	package-lock.json
#	src/actors/user-actor.ts
#	src/config.ts
#	src/docker/manager.ts
#	src/index.ts
#	src/lib/prompt-loader.ts
#	src/routes/chat.ts
#	src/services/service-agents.ts
2026-06-01 21:17:38 +05:30
dd6ccf1141 fixed the dockerfile 2026-06-01 16:23:15 +05:30
0a9bbe6b6f integrated to remove old apis and integrate new api endpoints for new frontend 2026-06-01 15:43:10 +05:30
4d284b58d7 implemented chat and workflows 2026-05-30 02:22:55 +05:30
e48c19b840 Wires all 4 microservice-backed agents into the chat so LLM can call real
services and return session URLs.
2026-05-28 17:43:15 +05:30
54297496a4 feat: Enhance Gitea integration and agent management
- Added ensureOrg and ensureOrgRepo methods to GiteaClient for centralized organization and repository management.
- Updated main application flow to ensure central Gitea readiness at startup.
- Introduced prompt-loader for dynamic loading of agent modules and system prompts from disk.
- Refactored agent routes to return sub-agent module catalog.
- Modified git routes to interact with a central Gitea instance instead of per-user containers.
- Updated workflow routes to utilize a unified user actor per user, streamlining job application workflows.
- Improved service agent handling with a new lightweight reference type.
2026-05-25 17:52:40 +05:30
2d471c61b4 feat: introduce workflow job management and agent orchestration
- Added workflow job actor to manage job application workflows.
- Implemented agent catalog for various workflow agents.
- Created service agents for interview, roleplay, and Q-Score functionalities.
- Enhanced user authentication to automatically create users if they do not exist.
- Updated configuration to support new LLM provider and API keys.
- Introduced new routes for agent and workflow management.
- Refactored Docker management to improve Gitea admin user creation and token generation.
- Removed deprecated Anthropics SDK integration.
2026-05-21 23:17:26 +05:30
6 changed files with 122 additions and 13 deletions

View File

@@ -2,6 +2,7 @@
id: job-search id: job-search
name: Job Search Agent name: Job Search Agent
role: Opportunity Scout role: Opportunity Scout
service: matchmaking-service
tools: tools:
- search_jobs - search_jobs
- rank_opportunities - rank_opportunities

View File

@@ -11,6 +11,7 @@ import {
getSubAgentModules, getSubAgentModules,
} from "../lib/prompt-loader.js"; } from "../lib/prompt-loader.js";
import { import {
buildServiceSessionUrl,
runServiceAgentProbe, runServiceAgentProbe,
type ServiceAgentResult, type ServiceAgentResult,
} from "../services/service-agents.js"; } from "../services/service-agents.js";
@@ -532,11 +533,9 @@ export const userActor = actor({
moduleName: m.name, moduleName: m.name,
status: m.status, status: m.status,
sessionId: detail?.session_id as string | undefined, sessionId: detail?.session_id as string | undefined,
sessionUrl: m.service === "interview-service" sessionUrl: typeof detail?.ui_session_url === "string"
? `http://localhost:8007/api/v1/demo?session_id=${detail?.session_id ?? ""}` ? detail.ui_session_url
: m.service === "roleplay-service" : buildServiceSessionUrl(m.service, detail, c.state.workflowGoal),
? `http://localhost:8008/api/v1/demo?session_id=${detail?.session_id ?? ""}`
: undefined,
summary: m.lastResult?.summary, summary: m.lastResult?.summary,
}; };
}), }),

View File

@@ -56,6 +56,12 @@ export const config = {
process.env.QSCORE_SERVICE_URL ?? "http://localhost:8000", process.env.QSCORE_SERVICE_URL ?? "http://localhost:8000",
resumeServiceUrl: resumeServiceUrl:
process.env.RESUME_SERVICE_URL ?? "http://localhost:8002", 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) ── // ── Central Gitea (one org-wide instance, changes.md §2A) ──
giteaUrl: process.env.GITEA_URL ?? "http://127.0.0.1:3001", giteaUrl: process.env.GITEA_URL ?? "http://127.0.0.1:3001",

View File

@@ -9,7 +9,7 @@ export type SubAgentModule = {
name: string; name: string;
role: string; role: string;
description: 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[]; toolNames: string[];
}; };
@@ -122,7 +122,8 @@ export async function loadPromptsFromDisk(): Promise<void> {
service !== "interview-service" && service !== "interview-service" &&
service !== "roleplay-service" && service !== "roleplay-service" &&
service !== "qscore-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"); log.warn({ file: filename, service }, "unknown service value — treating as no service");
} }
@@ -133,7 +134,7 @@ export async function loadPromptsFromDisk(): Promise<void> {
role: data.role ?? data.name, role: data.role ?? data.name,
description: body || `Agent module: ${data.name}`, description: body || `Agent module: ${data.name}`,
service: service && 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"]) ? (service as SubAgentModule["service"])
: undefined, : undefined,
toolNames: data.tools ?? [], toolNames: data.tools ?? [],

View File

@@ -8,6 +8,7 @@ import type { LlmMessage } from "../lib/llm.js";
import { createChatCompletion } from "../lib/llm.js"; import { createChatCompletion } from "../lib/llm.js";
import { buildUnifiedSystemPrompt } from "../agents/catalog.js"; import { buildUnifiedSystemPrompt } from "../agents/catalog.js";
import { import {
buildServiceSessionUrl,
runServiceAgentProbe, runServiceAgentProbe,
type ServiceAgentResult, type ServiceAgentResult,
} from "../services/service-agents.js"; } from "../services/service-agents.js";
@@ -210,7 +211,9 @@ export function chatRoutes() {
moduleName: "Sara", moduleName: "Sara",
status: "done", status: "done",
sessionId: detail.session_id as string, 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, summary: toolResult.summary,
}); });
} }
@@ -228,7 +231,9 @@ export function chatRoutes() {
moduleName: "Emily", moduleName: "Emily",
status: "done", status: "done",
sessionId: detail.session_id as string, 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, summary: toolResult.summary,
}); });
} }
@@ -240,7 +245,16 @@ export function chatRoutes() {
{ userId, goal: String(toolCall.arguments.goal ?? "general") }, { userId, goal: String(toolCall.arguments.goal ?? "general") },
); );
if (toolResult.status === "ok") { if (toolResult.status === "ok") {
sessions.push({ moduleId: "resume", moduleName: "Resume Agent", status: "done", summary: toolResult.summary }); const detail = toolResult.detail as Record<string, unknown> | 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; break;
} }

View File

@@ -23,6 +23,39 @@ export type ServiceAgentContext = {
goal: string; goal: string;
}; };
export function buildServiceSessionUrl(
service: string | undefined,
detail: Record<string, unknown> | 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 { function stableUuid(input: string): string {
const hex = createHash("sha256").update(input).digest("hex").slice(0, 32); const hex = createHash("sha256").update(input).digest("hex").slice(0, 32);
return [ return [
@@ -97,7 +130,12 @@ async function runSaraInterview(ctx: ServiceAgentContext): Promise<ServiceAgentR
return { return {
status: "ok", status: "ok",
summary: `Sara created interview session ${detail.session_id ?? "(pending id)"} for ${ctx.goal}.`, summary: `Sara created interview session ${detail.session_id ?? "(pending id)"} for ${ctx.goal}.`,
detail, detail: {
...detail,
target_role: payload.context.target_role,
interview_type: payload.interview_type,
ui_session_url: buildServiceSessionUrl("interview-service", detail, ctx.goal),
},
}; };
} }
@@ -136,7 +174,12 @@ async function runEmilyRoleplay(ctx: ServiceAgentContext): Promise<ServiceAgentR
return { return {
status: "ok", status: "ok",
summary: `Emily created roleplay session ${detail.session_id ?? "(pending id)"} for ${ctx.goal}.`, summary: `Emily created roleplay session ${detail.session_id ?? "(pending id)"} for ${ctx.goal}.`,
detail, detail: {
...detail,
target_role: payload.metadata.target_role,
roleplay_type: payload.roleplay_type,
ui_session_url: buildServiceSessionUrl("roleplay-service", detail, ctx.goal),
},
}; };
} }
@@ -278,6 +321,7 @@ async function runResumeTailor(ctx: ServiceAgentContext): Promise<ServiceAgentRe
detail: { detail: {
...(stateResult.detail as Record<string, unknown> ?? {}), ...(stateResult.detail as Record<string, unknown> ?? {}),
goal: ctx.goal, 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.", 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<ServiceAgentRe
} }
} }
async function runMatchmaking(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
const matchmakingUserId = stableUuid(ctx.userId);
const query = new URLSearchParams({
user_id: matchmakingUserId,
top_n: "6",
threshold: "60",
recompute: "true",
});
try {
const detail = await serviceJson<Record<string, unknown>>(
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( export async function runServiceAgentProbe(
agent: ServiceAgentRef, agent: ServiceAgentRef,
ctx?: ServiceAgentContext, ctx?: ServiceAgentContext,
@@ -311,6 +395,10 @@ export async function runServiceAgentProbe(
return ctx return ctx
? await runResumeTailor(ctx) ? await runResumeTailor(ctx)
: healthCheck(config.resumeServiceUrl, "Resume Agent / resume-service"); : healthCheck(config.resumeServiceUrl, "Resume Agent / resume-service");
case "matchmaking-service":
return ctx
? await runMatchmaking(ctx)
: healthCheck(config.matchmakingServiceUrl, "Scout / matchmaking-service");
default: default:
return { return {
status: "local", status: "local",