feat(ai): implement an AI chat window for agentic resume building (#3022)

This commit is contained in:
Amruth Pillai
2026-05-10 13:23:32 +02:00
committed by GitHub
parent 42e83cc676
commit 6787175a8a
91 changed files with 11894 additions and 135 deletions
+3 -1
View File
@@ -58,6 +58,7 @@ export const aiRouter = {
try {
return await aiService.testConnection(input);
} catch (error) {
console.error(error);
if (isInvalidAiBaseUrlError(error)) throwAiProviderConfigError();
if (isAiProviderGatewayError(error)) throwAiProviderGatewayError();
throw error;
@@ -137,7 +138,7 @@ export const aiRouter = {
operationId: "aiChat",
summary: "Chat with AI to modify resume",
description:
"Streams a chat response from the configured AI provider. The LLM can call the patch_resume tool to generate JSON Patch operations that modify the resume. Requires authentication and AI provider credentials.",
"Streams a chat response from the configured AI provider. The LLM can call the propose_resume_patches tool to generate JSON Patch proposals for explicit user approval. Requires authentication and AI provider credentials.",
})
.input(
type<{
@@ -147,6 +148,7 @@ export const aiRouter = {
baseURL: string;
messages: UIMessage[];
resumeData: ResumeData;
resumeUpdatedAt: Date;
}>(),
)
.use(aiRequestRateLimit)
+39
View File
@@ -6,6 +6,7 @@ import { protectedProcedure, publicProcedure } from "../context";
import { resumeDto } from "../dto/resume";
import { resumeMutationRateLimit, resumePasswordRateLimit } from "../middleware/rate-limit";
import { resumeService } from "../services/resume";
import { subscribeResumeUpdated } from "../services/resume-events";
const tagsRouter = {
list: protectedProcedure
@@ -71,10 +72,43 @@ const analysisRouter = {
}),
};
const updatesRouter = {
subscribe: protectedProcedure
.route({
method: "GET",
path: "/resumes/{id}/updates",
tags: ["Resumes"],
operationId: "subscribeResumeUpdates",
summary: "Subscribe to resume updates",
description:
"Streams lightweight invalidation events when the specified resume changes. The event payload contains metadata only; clients should refetch the resume for canonical data.",
successDescription: "A stream of resume update invalidation events.",
})
.input(z.object({ id: z.string().describe("The unique identifier of the resume.") }))
.handler(async function* ({ context, input, signal }) {
const resume = await resumeService.getById({ id: input.id, userId: context.user.id });
yield {
type: "resume.updated" as const,
resumeId: input.id,
userId: context.user.id,
updatedAt: resume.updatedAt.toISOString(),
mutation: "sync" as const,
};
yield* subscribeResumeUpdated({
resumeId: input.id,
userId: context.user.id,
...(signal ? { signal } : {}),
});
}),
};
export const resumeRouter = {
tags: tagsRouter,
statistics: statisticsRouter,
analysis: analysisRouter,
updates: updatesRouter,
list: protectedProcedure
.route({
@@ -250,12 +284,17 @@ export const resumeRouter = {
message: "The patch operations are invalid or produced an invalid resume.",
status: 400,
},
RESUME_VERSION_CONFLICT: {
message: "The resume changed after this patch was generated.",
status: 409,
},
})
.handler(async ({ context, input }) => {
return resumeService.patch({
id: input.id,
userId: context.user.id,
operations: input.operations,
...(input.expectedUpdatedAt ? { expectedUpdatedAt: input.expectedUpdatedAt } : {}),
});
}),