@studious-lms/server 1.4.9 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/config/cors.d.ts.map +1 -1
- package/dist/lib/config/cors.js +2 -3
- package/dist/lib/config/cors.js.map +1 -1
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +4 -5
- package/dist/lib/fileUpload.js.map +1 -1
- package/dist/lib/pusher.d.ts +7 -1
- package/dist/lib/pusher.d.ts.map +1 -1
- package/dist/lib/pusher.js +9 -3
- package/dist/lib/pusher.js.map +1 -1
- package/dist/models/attendance.d.ts +16 -7
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/attendance.js +17 -2
- package/dist/models/attendance.js.map +1 -1
- package/dist/models/auth.d.ts +1 -0
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +3 -2
- package/dist/models/auth.js.map +1 -1
- package/dist/models/comment.d.ts +0 -5
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/conversation.d.ts +17 -16
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +2 -1
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/message.d.ts +6 -3
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/school.d.ts +0 -22
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/school.js +2 -14
- package/dist/models/school.js.map +1 -1
- package/dist/models/worksheet.d.ts +76 -19
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +2 -2
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts +20 -9
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +63 -75
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiNewtonChat.d.ts +13 -4
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +39 -27
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +5 -3
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +60 -24
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +34 -2
- package/dist/pipelines/pdfToWorksheet.d.ts.map +1 -1
- package/dist/pipelines/pdfToWorksheet.js +205 -32
- package/dist/pipelines/pdfToWorksheet.js.map +1 -1
- package/dist/routers/_app.d.ts +106 -48
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts +0 -4
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +0 -8
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +64 -4
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/auth.d.ts +1 -0
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/comment.d.ts +0 -3
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/conversation.d.ts +2 -1
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/labChat.d.ts +3 -1
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/message.d.ts +3 -0
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/newtonChat.d.ts +1 -0
- package/dist/routers/newtonChat.d.ts.map +1 -1
- package/dist/routers/school.d.ts +6 -0
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +7 -2
- package/dist/routers/school.js.map +1 -1
- package/dist/routers/worksheet.d.ts +37 -7
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +16 -6
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/services/announcement.js +4 -4
- package/dist/services/announcement.js.map +1 -1
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +16 -6
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts.map +1 -1
- package/dist/services/attendance.js +68 -4
- package/dist/services/attendance.js.map +1 -1
- package/dist/services/auth.d.ts +1 -0
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/comment.d.ts +0 -3
- package/dist/services/comment.d.ts.map +1 -1
- package/dist/services/conversation.d.ts +2 -1
- package/dist/services/conversation.d.ts.map +1 -1
- package/dist/services/labChat.d.ts +10 -2
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +103 -44
- package/dist/services/labChat.js.map +1 -1
- package/dist/services/message.d.ts +3 -0
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +20 -2
- package/dist/services/message.js.map +1 -1
- package/dist/services/newtonChat.d.ts +1 -0
- package/dist/services/newtonChat.d.ts.map +1 -1
- package/dist/services/newtonChat.js +40 -17
- package/dist/services/newtonChat.js.map +1 -1
- package/dist/services/school.d.ts +6 -0
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +71 -6
- package/dist/services/school.js.map +1 -1
- package/dist/services/worksheet.d.ts +48 -11
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +73 -29
- package/dist/services/worksheet.js.map +1 -1
- package/dist/utils/generateUsername.d.ts +8 -0
- package/dist/utils/generateUsername.d.ts.map +1 -0
- package/dist/utils/generateUsername.js +103 -0
- package/dist/utils/generateUsername.js.map +1 -0
- package/dist/utils/inference.d.ts +50 -1
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +167 -2
- package/dist/utils/inference.js.map +1 -1
- package/dist/workers/connection.d.ts +3 -0
- package/dist/workers/connection.d.ts.map +1 -0
- package/dist/workers/connection.js +33 -0
- package/dist/workers/connection.js.map +1 -0
- package/dist/workers/index.d.ts +10 -0
- package/dist/workers/index.d.ts.map +1 -0
- package/dist/workers/index.js +63 -0
- package/dist/workers/index.js.map +1 -0
- package/dist/workers/processors/aiLabChat.d.ts +7 -0
- package/dist/workers/processors/aiLabChat.d.ts.map +1 -0
- package/dist/workers/processors/aiLabChat.js +52 -0
- package/dist/workers/processors/aiLabChat.js.map +1 -0
- package/dist/workers/processors/aiNewtonChat.d.ts +7 -0
- package/dist/workers/processors/aiNewtonChat.d.ts.map +1 -0
- package/dist/workers/processors/aiNewtonChat.js +57 -0
- package/dist/workers/processors/aiNewtonChat.js.map +1 -0
- package/dist/workers/processors/createRunJobWorker.d.ts +46 -0
- package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -0
- package/dist/workers/processors/createRunJobWorker.js +118 -0
- package/dist/workers/processors/createRunJobWorker.js.map +1 -0
- package/dist/workers/processors/gradeWorksheet.d.ts +6 -0
- package/dist/workers/processors/gradeWorksheet.d.ts.map +1 -0
- package/dist/workers/processors/gradeWorksheet.js +19 -0
- package/dist/workers/processors/gradeWorksheet.js.map +1 -0
- package/dist/workers/processors/pdfToWorksheet.d.ts +8 -0
- package/dist/workers/processors/pdfToWorksheet.d.ts.map +1 -0
- package/dist/workers/processors/pdfToWorksheet.js +20 -0
- package/dist/workers/processors/pdfToWorksheet.js.map +1 -0
- package/dist/workers/processors/studentProgressChat.d.ts +8 -0
- package/dist/workers/processors/studentProgressChat.d.ts.map +1 -0
- package/dist/workers/processors/studentProgressChat.js +27 -0
- package/dist/workers/processors/studentProgressChat.js.map +1 -0
- package/dist/workers/queues.d.ts +85 -0
- package/dist/workers/queues.d.ts.map +1 -0
- package/dist/workers/queues.js +119 -0
- package/dist/workers/queues.js.map +1 -0
- package/package.json +6 -3
- package/prisma/migrations/20260506162500_consolidate_run_job_and_unify_job_status/migration.sql +166 -0
- package/prisma/schema.prisma +465 -387
- package/src/index.ts +12 -1
- package/src/lib/config/cors.ts +0 -1
- package/src/lib/fileUpload.ts +2 -4
- package/src/lib/pusher.ts +7 -1
- package/src/models/attendance.ts +15 -0
- package/src/models/auth.ts +1 -0
- package/src/models/school.ts +0 -14
- package/src/models/worksheet.ts +2 -2
- package/src/pipelines/aiLabChat.ts +71 -79
- package/src/pipelines/aiNewtonChat.ts +47 -31
- package/src/pipelines/gradeWorksheet.ts +63 -25
- package/src/pipelines/pdfToWorksheet.ts +272 -34
- package/src/routers/assignment.ts +74 -3
- package/src/routers/school.ts +5 -0
- package/src/routers/worksheet.ts +25 -4
- package/src/services/announcement.ts +2 -2
- package/src/services/assignment.ts +18 -4
- package/src/services/attendance.ts +87 -2
- package/src/services/labChat.ts +108 -48
- package/src/services/message.ts +19 -0
- package/src/services/newtonChat.ts +46 -25
- package/src/services/school.ts +89 -6
- package/src/services/worksheet.ts +87 -28
- package/src/utils/generateUsername.ts +108 -0
- package/src/utils/inference.ts +209 -1
- package/src/workers/connection.ts +37 -0
- package/src/workers/index.ts +71 -0
- package/src/workers/processors/aiLabChat.ts +65 -0
- package/src/workers/processors/aiNewtonChat.ts +75 -0
- package/src/workers/processors/createRunJobWorker.ts +164 -0
- package/src/workers/processors/gradeWorksheet.ts +16 -0
- package/src/workers/processors/pdfToWorksheet.ts +17 -0
- package/src/workers/processors/studentProgressChat.ts +28 -0
- package/src/workers/queues.ts +174 -0
- package/tests/{globalSetup.ts → integration/globalSetup.ts} +4 -4
- package/tests/{helpers.ts → integration/helpers.ts} +2 -2
- package/tests/{pipelines → integration/pipelines}/pdfToWorksheet.test.ts +55 -43
- package/tests/{routers → integration/routers}/attendance.test.ts +61 -2
- package/tests/{routers → integration/routers}/auth.test.ts +3 -3
- package/tests/{routers → integration/routers}/class.test.ts +3 -17
- package/tests/{routers → integration/routers}/conversation.test.ts +1 -1
- package/tests/{routers → integration/routers}/event.test.ts +1 -1
- package/tests/{routers → integration/routers}/message.test.ts +1 -1
- package/tests/{routers → integration/routers}/newtonChat.test.ts +1 -1
- package/tests/{routers → integration/routers}/studentProgress.test.ts +1 -1
- package/tests/{user.test.ts → integration/routers/user.test.ts} +2 -2
- package/tests/integration/routers/worksheet.test.ts +448 -0
- package/tests/{setup.ts → integration/setup.ts} +4 -4
- package/tests/{lib → unit/lib}/cors.test.ts +1 -1
- package/tests/{middleware → unit/middleware}/security.test.ts +1 -1
- package/tests/{pipelines → unit/pipelines}/aiLabChat.test.ts +1 -1
- package/tests/{lib → unit/pipelines}/aiLabChatContract.test.ts +1 -1
- package/tests/{utils/aiLabChatPrompt.test.ts → unit/pipelines/labChatPrompt.test.ts} +2 -2
- package/tests/{utils → unit/services}/studentProgress.test.ts +5 -5
- package/tests/{utils → unit/utils}/aiUser.test.ts +1 -1
- package/tests/{utils → unit/utils}/generateInviteCode.test.ts +1 -1
- package/tests/{utils → unit/utils}/inference.test.ts +6 -6
- package/tests/{utils → unit/utils}/logger.test.ts +1 -1
- package/tests/{utils → unit/utils}/prismaErrorHandler.test.ts +1 -1
- package/tests/{utils → unit/utils}/prismaWrapper.test.ts +1 -1
- package/vitest.config.ts +11 -3
- package/vitest.unit.config.ts +10 -10
- package/tests/routers/worksheet.test.ts +0 -194
- package/tests/startup.test.ts +0 -5
- package/tests/worksheet.test.ts +0 -181
- /package/tests/{routers → integration/routers}/agenda.test.ts +0 -0
- /package/tests/{routers → integration/routers}/announcement.test.ts +0 -0
- /package/tests/{routers → integration/routers}/assignment.test.ts +0 -0
- /package/tests/{routers → integration/routers}/comment.test.ts +0 -0
- /package/tests/{routers → integration/routers}/folder.test.ts +0 -0
- /package/tests/{routers → integration/routers}/labChat.test.ts +0 -0
- /package/tests/{routers → integration/routers}/marketing.test.ts +0 -0
- /package/tests/{routers → integration/routers}/notification.test.ts +0 -0
- /package/tests/{routers → integration/routers}/search.test.ts +0 -0
- /package/tests/{routers → integration/routers}/section.test.ts +0 -0
- /package/tests/{server → unit/middleware}/rateLimit.test.ts +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["workers/index.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Worker process entrypoint.\n *\n * Run separately from the HTTP server (e.g. `node dist/workers/index.js` on\n * Render/Railway/Fly). Importing this file boots every BullMQ Worker\n * declared under `src/workers/processors/*`. Do NOT import this from the\n * HTTP server — that would spin up workers in your serverless functions.\n */\nimport '../instrument.js';\n\nimport { Worker } from 'bullmq';\nimport { prisma } from '../lib/prisma.js';\nimport { logger } from '../utils/logger.js';\nimport { bullConnection } from './connection.js';\nimport { aiLabChatWorker } from './processors/aiLabChat.js';\nimport { aiNewtonChatWorker } from './processors/aiNewtonChat.js';\nimport { gradeWorksheetWorker } from './processors/gradeWorksheet.js';\nimport { pdfToWorksheetWorker } from './processors/pdfToWorksheet.js';\nimport { studentProgressChatWorker } from './processors/studentProgressChat.js';\n\nconst workers: Worker<any>[] = [\n pdfToWorksheetWorker,\n gradeWorksheetWorker,\n aiLabChatWorker,\n aiNewtonChatWorker,\n studentProgressChatWorker,\n];\n\nlogger.info('Workers ready', {\n pid: process.pid,\n queues: workers.map((w) => w.name),\n});\n\nlet shuttingDown = false;\n\nasync function shutdown(signal: string): Promise<void> {\n if (shuttingDown) return;\n shuttingDown = true;\n\n logger.info(`Received ${signal}, draining workers...`);\n\n // `worker.close()` waits for in-flight jobs to finish (up to the worker's\n // settings) before resolving. Run them in parallel.\n await Promise.allSettled(workers.map((w) => w.close()));\n\n await Promise.allSettled([\n bullConnection.quit(),\n prisma.$disconnect(),\n ]);\n\n logger.info('Workers shut down cleanly');\n process.exit(0);\n}\n\nprocess.on('SIGTERM', () => void shutdown('SIGTERM'));\nprocess.on('SIGINT', () => void shutdown('SIGINT'));\n\nprocess.on('uncaughtException', (error) => {\n logger.error('Uncaught exception in worker process', {\n error: error.message,\n stack: error.stack,\n });\n process.exit(1);\n});\n\nprocess.on('unhandledRejection', (reason) => {\n logger.error('Unhandled rejection in worker process', {\n reason: reason instanceof Error ? reason.message : String(reason),\n });\n process.exit(1);\n});\n"],"names":[],"mappings":"AAAA;;;;;;;GAOG;;;AACH,OAAO,kBAAkB,CAAC;AAG1B,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAEhF,MAAM,OAAO,GAAkB;IAC7B,oBAAoB;IACpB,oBAAoB;IACpB,eAAe;IACf,kBAAkB;IAClB,yBAAyB;CAC1B,CAAC;AAEF,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE;IAC3B,GAAG,EAAE,OAAO,CAAC,GAAG;IAChB,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;CACnC,CAAC,CAAC;AAEH,IAAI,YAAY,GAAG,KAAK,CAAC;AAEzB,KAAK,UAAU,QAAQ,CAAC,MAAc;IACpC,IAAI,YAAY;QAAE,OAAO;IACzB,YAAY,GAAG,IAAI,CAAC;IAEpB,MAAM,CAAC,IAAI,CAAC,YAAY,MAAM,uBAAuB,CAAC,CAAC;IAEvD,0EAA0E;IAC1E,oDAAoD;IACpD,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAExD,MAAM,OAAO,CAAC,UAAU,CAAC;QACvB,cAAc,CAAC,IAAI,EAAE;QACrB,MAAM,CAAC,WAAW,EAAE;KACrB,CAAC,CAAC;IAEH,MAAM,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;AACtD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AAEpD,OAAO,CAAC,EAAE,CAAC,mBAAmB,EAAE,CAAC,KAAK,EAAE,EAAE;IACxC,MAAM,CAAC,KAAK,CAAC,sCAAsC,EAAE;QACnD,KAAK,EAAE,KAAK,CAAC,OAAO;QACpB,KAAK,EAAE,KAAK,CAAC,KAAK;KACnB,CAAC,CAAC;IACH,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,MAAM,EAAE,EAAE;IAC1C,MAAM,CAAC,KAAK,CAAC,uCAAuC,EAAE;QACpD,MAAM,EAAE,MAAM,YAAY,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC;KAClE,CAAC,CAAC;IACH,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC","debug_id":"07a4db46-6b47-5518-8343-2659c25cad54"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aiLabChat.d.ts","sourceRoot":"/","sources":["workers/processors/aiLabChat.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,eAAe;;;;;eAwD1B,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="cb84c0dd-1443-5364-811e-02e333b3e8c2")}catch(e){}}();
|
|
3
|
+
import { generateAndSendLabIntroduction, generateAndSendLabResponse, } from '../../pipelines/aiLabChat.js';
|
|
4
|
+
import { prisma } from '../../lib/prisma.js';
|
|
5
|
+
import { isAIUser } from '../../utils/aiUser.js';
|
|
6
|
+
import { createRunJobWorker } from './createRunJobWorker.js';
|
|
7
|
+
export const aiLabChatWorker = createRunJobWorker({
|
|
8
|
+
type: 'AI_LAB_CHAT',
|
|
9
|
+
workerOptions: {
|
|
10
|
+
// Lab responses can produce PDFs and large schemas; cap concurrency to
|
|
11
|
+
// keep memory under control and to honour upstream model rate limits.
|
|
12
|
+
concurrency: 1,
|
|
13
|
+
limiter: { max: 5, duration: 60000 },
|
|
14
|
+
},
|
|
15
|
+
run: async ({ conversationId, aiMessageId }) => {
|
|
16
|
+
const labChat = await prisma.labChat.findFirst({
|
|
17
|
+
where: { conversationId },
|
|
18
|
+
select: {
|
|
19
|
+
id: true,
|
|
20
|
+
classId: true,
|
|
21
|
+
context: true,
|
|
22
|
+
class: { select: { subject: true } },
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
if (!labChat) {
|
|
26
|
+
throw new Error(`LabChat for conversationId=${conversationId} not found`);
|
|
27
|
+
}
|
|
28
|
+
// The AI message is a placeholder we created before enqueue; the
|
|
29
|
+
// pipeline fills it in. Look up the most recent non-AI message in the
|
|
30
|
+
// conversation to feed the response generator. If none exists, this is
|
|
31
|
+
// the introductory message at the top of a brand-new lab chat.
|
|
32
|
+
const triggeringMessage = await prisma.message.findFirst({
|
|
33
|
+
where: {
|
|
34
|
+
conversationId,
|
|
35
|
+
senderId: { not: 'AI_ASSISTANT' },
|
|
36
|
+
},
|
|
37
|
+
orderBy: { createdAt: 'desc' },
|
|
38
|
+
select: { id: true, content: true, senderId: true },
|
|
39
|
+
});
|
|
40
|
+
if (!triggeringMessage || isAIUser(triggeringMessage.senderId)) {
|
|
41
|
+
await generateAndSendLabIntroduction(labChat.id, conversationId, labChat.context, labChat.class?.subject || 'Lab', aiMessageId);
|
|
42
|
+
return { labChatId: labChat.id, kind: 'introduction', aiMessageId };
|
|
43
|
+
}
|
|
44
|
+
await generateAndSendLabResponse(labChat.id, triggeringMessage.content, {
|
|
45
|
+
classId: labChat.classId,
|
|
46
|
+
aiMessageId,
|
|
47
|
+
});
|
|
48
|
+
return { labChatId: labChat.id, aiMessageId };
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
//# sourceMappingURL=aiLabChat.js.map
|
|
52
|
+
//# debugId=cb84c0dd-1443-5364-811e-02e333b3e8c2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aiLabChat.js","sources":["workers/processors/aiLabChat.ts"],"sourceRoot":"/","sourcesContent":["import {\n generateAndSendLabIntroduction,\n generateAndSendLabResponse,\n} from '../../pipelines/aiLabChat.js';\nimport { prisma } from '../../lib/prisma.js';\nimport { isAIUser } from '../../utils/aiUser.js';\nimport { createRunJobWorker } from './createRunJobWorker.js';\n\nexport const aiLabChatWorker = createRunJobWorker({\n type: 'AI_LAB_CHAT',\n workerOptions: {\n // Lab responses can produce PDFs and large schemas; cap concurrency to\n // keep memory under control and to honour upstream model rate limits.\n concurrency: 1,\n limiter: { max: 5, duration: 60_000 },\n },\n run: async ({ conversationId, aiMessageId }) => {\n const labChat = await prisma.labChat.findFirst({\n where: { conversationId },\n select: {\n id: true,\n classId: true,\n context: true,\n class: { select: { subject: true } },\n },\n });\n\n if (!labChat) {\n throw new Error(\n `LabChat for conversationId=${conversationId} not found`,\n );\n }\n\n // The AI message is a placeholder we created before enqueue; the\n // pipeline fills it in. Look up the most recent non-AI message in the\n // conversation to feed the response generator. If none exists, this is\n // the introductory message at the top of a brand-new lab chat.\n const triggeringMessage = await prisma.message.findFirst({\n where: {\n conversationId,\n senderId: { not: 'AI_ASSISTANT' },\n },\n orderBy: { createdAt: 'desc' },\n select: { id: true, content: true, senderId: true },\n });\n\n if (!triggeringMessage || isAIUser(triggeringMessage.senderId)) {\n await generateAndSendLabIntroduction(\n labChat.id,\n conversationId,\n labChat.context,\n labChat.class?.subject || 'Lab',\n aiMessageId,\n );\n return { labChatId: labChat.id, kind: 'introduction', aiMessageId };\n }\n\n await generateAndSendLabResponse(labChat.id, triggeringMessage.content, {\n classId: labChat.classId,\n aiMessageId,\n });\n\n return { labChatId: labChat.id, aiMessageId };\n },\n});\n"],"names":[],"mappings":";;AAAA,OAAO,EACL,8BAA8B,EAC9B,0BAA0B,GAC3B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,CAAC,MAAM,eAAe,GAAG,kBAAkB,CAAC;IAChD,IAAI,EAAE,aAAa;IACnB,aAAa,EAAE;QACb,uEAAuE;QACvE,sEAAsE;QACtE,WAAW,EAAE,CAAC;QACd,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAM,EAAE;KACtC;IACD,GAAG,EAAE,KAAK,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,EAAE,EAAE;QAC7C,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC;YAC7C,KAAK,EAAE,EAAE,cAAc,EAAE;YACzB,MAAM,EAAE;gBACN,EAAE,EAAE,IAAI;gBACR,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,IAAI;gBACb,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;aACrC;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CACb,8BAA8B,cAAc,YAAY,CACzD,CAAC;QACJ,CAAC;QAED,iEAAiE;QACjE,sEAAsE;QACtE,uEAAuE;QACvE,+DAA+D;QAC/D,MAAM,iBAAiB,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC;YACvD,KAAK,EAAE;gBACL,cAAc;gBACd,QAAQ,EAAE,EAAE,GAAG,EAAE,cAAc,EAAE;aAClC;YACD,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;YAC9B,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE;SACpD,CAAC,CAAC;QAEH,IAAI,CAAC,iBAAiB,IAAI,QAAQ,CAAC,iBAAiB,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/D,MAAM,8BAA8B,CAClC,OAAO,CAAC,EAAE,EACV,cAAc,EACd,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,KAAK,EAAE,OAAO,IAAI,KAAK,EAC/B,WAAW,CACZ,CAAC;YACF,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC;QACtE,CAAC;QAED,MAAM,0BAA0B,CAAC,OAAO,CAAC,EAAE,EAAE,iBAAiB,CAAC,OAAO,EAAE;YACtE,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,WAAW;SACZ,CAAC,CAAC;QAEH,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,WAAW,EAAE,CAAC;IAChD,CAAC;CACF,CAAC,CAAC","debug_id":"cb84c0dd-1443-5364-811e-02e333b3e8c2"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aiNewtonChat.d.ts","sourceRoot":"/","sources":["workers/processors/aiNewtonChat.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,kBAAkB;;;;;eAkE7B,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e55f6275-3148-5423-9eae-9460ac8c1ece")}catch(e){}}();
|
|
3
|
+
import { generateAndSendNewtonIntroduction, generateAndSendNewtonResponse, } from '../../pipelines/aiNewtonChat.js';
|
|
4
|
+
import { prisma } from '../../lib/prisma.js';
|
|
5
|
+
import { isAIUser } from '../../utils/aiUser.js';
|
|
6
|
+
import { createRunJobWorker } from './createRunJobWorker.js';
|
|
7
|
+
export const aiNewtonChatWorker = createRunJobWorker({
|
|
8
|
+
type: 'AI_NEWTON_CHAT',
|
|
9
|
+
workerOptions: {
|
|
10
|
+
concurrency: 2,
|
|
11
|
+
limiter: { max: 10, duration: 60000 },
|
|
12
|
+
},
|
|
13
|
+
run: async ({ conversationId, aiMessageId }) => {
|
|
14
|
+
const newtonChat = await prisma.newtonChat.findFirst({
|
|
15
|
+
where: { conversationId },
|
|
16
|
+
select: {
|
|
17
|
+
id: true,
|
|
18
|
+
submission: {
|
|
19
|
+
select: {
|
|
20
|
+
id: true,
|
|
21
|
+
assignment: {
|
|
22
|
+
select: {
|
|
23
|
+
id: true,
|
|
24
|
+
title: true,
|
|
25
|
+
instructions: true,
|
|
26
|
+
class: { select: { subject: true } },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
if (!newtonChat) {
|
|
34
|
+
throw new Error(`NewtonChat for conversationId=${conversationId} not found`);
|
|
35
|
+
}
|
|
36
|
+
// Resolve the triggering student message (if any). On a brand-new
|
|
37
|
+
// tutor session there are no student messages yet, so we run the
|
|
38
|
+
// introduction path; otherwise we run the response path against the
|
|
39
|
+
// most recent non-AI message.
|
|
40
|
+
const triggeringMessage = await prisma.message.findFirst({
|
|
41
|
+
where: {
|
|
42
|
+
conversationId,
|
|
43
|
+
senderId: { not: 'AI_ASSISTANT' },
|
|
44
|
+
},
|
|
45
|
+
orderBy: { createdAt: 'desc' },
|
|
46
|
+
select: { id: true, content: true, senderId: true },
|
|
47
|
+
});
|
|
48
|
+
if (!triggeringMessage || isAIUser(triggeringMessage.senderId)) {
|
|
49
|
+
await generateAndSendNewtonIntroduction(newtonChat.id, conversationId, newtonChat.submission.id, aiMessageId);
|
|
50
|
+
return { newtonChatId: newtonChat.id, kind: 'introduction', aiMessageId };
|
|
51
|
+
}
|
|
52
|
+
await generateAndSendNewtonResponse(newtonChat.id, triggeringMessage.content, conversationId, newtonChat.submission, aiMessageId);
|
|
53
|
+
return { newtonChatId: newtonChat.id, aiMessageId };
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
//# sourceMappingURL=aiNewtonChat.js.map
|
|
57
|
+
//# debugId=e55f6275-3148-5423-9eae-9460ac8c1ece
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aiNewtonChat.js","sources":["workers/processors/aiNewtonChat.ts"],"sourceRoot":"/","sourcesContent":["import {\n generateAndSendNewtonIntroduction,\n generateAndSendNewtonResponse,\n} from '../../pipelines/aiNewtonChat.js';\nimport { prisma } from '../../lib/prisma.js';\nimport { isAIUser } from '../../utils/aiUser.js';\nimport { createRunJobWorker } from './createRunJobWorker.js';\n\nexport const aiNewtonChatWorker = createRunJobWorker({\n type: 'AI_NEWTON_CHAT',\n workerOptions: {\n concurrency: 2,\n limiter: { max: 10, duration: 60_000 },\n },\n run: async ({ conversationId, aiMessageId }) => {\n const newtonChat = await prisma.newtonChat.findFirst({\n where: { conversationId },\n select: {\n id: true,\n submission: {\n select: {\n id: true,\n assignment: {\n select: {\n id: true,\n title: true,\n instructions: true,\n class: { select: { subject: true } },\n },\n },\n },\n },\n },\n });\n\n if (!newtonChat) {\n throw new Error(\n `NewtonChat for conversationId=${conversationId} not found`,\n );\n }\n\n // Resolve the triggering student message (if any). On a brand-new\n // tutor session there are no student messages yet, so we run the\n // introduction path; otherwise we run the response path against the\n // most recent non-AI message.\n const triggeringMessage = await prisma.message.findFirst({\n where: {\n conversationId,\n senderId: { not: 'AI_ASSISTANT' },\n },\n orderBy: { createdAt: 'desc' },\n select: { id: true, content: true, senderId: true },\n });\n\n if (!triggeringMessage || isAIUser(triggeringMessage.senderId)) {\n await generateAndSendNewtonIntroduction(\n newtonChat.id,\n conversationId,\n newtonChat.submission.id,\n aiMessageId,\n );\n return { newtonChatId: newtonChat.id, kind: 'introduction', aiMessageId };\n }\n\n await generateAndSendNewtonResponse(\n newtonChat.id,\n triggeringMessage.content,\n conversationId,\n newtonChat.submission,\n aiMessageId,\n );\n\n return { newtonChatId: newtonChat.id, aiMessageId };\n },\n});\n"],"names":[],"mappings":";;AAAA,OAAO,EACL,iCAAiC,EACjC,6BAA6B,GAC9B,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,CAAC,MAAM,kBAAkB,GAAG,kBAAkB,CAAC;IACnD,IAAI,EAAE,gBAAgB;IACtB,aAAa,EAAE;QACb,WAAW,EAAE,CAAC;QACd,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAM,EAAE;KACvC;IACD,GAAG,EAAE,KAAK,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,EAAE,EAAE;QAC7C,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC;YACnD,KAAK,EAAE,EAAE,cAAc,EAAE;YACzB,MAAM,EAAE;gBACN,EAAE,EAAE,IAAI;gBACR,UAAU,EAAE;oBACV,MAAM,EAAE;wBACN,EAAE,EAAE,IAAI;wBACR,UAAU,EAAE;4BACV,MAAM,EAAE;gCACN,EAAE,EAAE,IAAI;gCACR,KAAK,EAAE,IAAI;gCACX,YAAY,EAAE,IAAI;gCAClB,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;6BACrC;yBACF;qBACF;iBACF;aACF;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CACb,iCAAiC,cAAc,YAAY,CAC5D,CAAC;QACJ,CAAC;QAED,kEAAkE;QAClE,iEAAiE;QACjE,oEAAoE;QACpE,8BAA8B;QAC9B,MAAM,iBAAiB,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC;YACvD,KAAK,EAAE;gBACL,cAAc;gBACd,QAAQ,EAAE,EAAE,GAAG,EAAE,cAAc,EAAE;aAClC;YACD,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;YAC9B,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE;SACpD,CAAC,CAAC;QAEH,IAAI,CAAC,iBAAiB,IAAI,QAAQ,CAAC,iBAAiB,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/D,MAAM,iCAAiC,CACrC,UAAU,CAAC,EAAE,EACb,cAAc,EACd,UAAU,CAAC,UAAU,CAAC,EAAE,EACxB,WAAW,CACZ,CAAC;YACF,OAAO,EAAE,YAAY,EAAE,UAAU,CAAC,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC;QAC5E,CAAC;QAED,MAAM,6BAA6B,CACjC,UAAU,CAAC,EAAE,EACb,iBAAiB,CAAC,OAAO,EACzB,cAAc,EACd,UAAU,CAAC,UAAU,EACrB,WAAW,CACZ,CAAC;QAEF,OAAO,EAAE,YAAY,EAAE,UAAU,CAAC,EAAE,EAAE,WAAW,EAAE,CAAC;IACtD,CAAC;CACF,CAAC,CAAC","debug_id":"e55f6275-3148-5423-9eae-9460ac8c1ece"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createRunJobWorker` — shared factory for every BullMQ processor in this app.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the boilerplate that every RunJob-backed worker needs:
|
|
5
|
+
* - Idempotent skip if the RunJob already SUCCEEDED (recovery after a crash
|
|
6
|
+
* that lost the BullMQ ack).
|
|
7
|
+
* - Status transitions: PENDING/RUNNING → RUNNING (with attempts++) → SUCCEEDED.
|
|
8
|
+
* - Failure handling: record latest error every attempt, flip to FAILED only
|
|
9
|
+
* on the final retry.
|
|
10
|
+
* - Standardised structured logging on `error` / `failed` / `completed`.
|
|
11
|
+
*
|
|
12
|
+
* A processor implementation only has to provide its `RunJobType`, optional
|
|
13
|
+
* BullMQ options (concurrency, limiter, etc.), and a `run` callback that does
|
|
14
|
+
* the actual work. The callback receives the typed `JobInputs[type]` payload
|
|
15
|
+
* and a context object with `runJobId`, `bullJobId`, `attempt`, plus the
|
|
16
|
+
* `userId` / `classId` from the original `enqueueRunJob` call.
|
|
17
|
+
*/
|
|
18
|
+
import { Worker, type WorkerOptions } from 'bullmq';
|
|
19
|
+
import { Prisma, type RunJobType } from '@prisma/client';
|
|
20
|
+
import { type JobInputs } from '../queues.js';
|
|
21
|
+
export interface RunJobContext {
|
|
22
|
+
runJobId: string;
|
|
23
|
+
bullJobId: string | undefined;
|
|
24
|
+
/** 1-indexed attempt number of the current run. */
|
|
25
|
+
attempt: number;
|
|
26
|
+
/** From `EnqueueOptions.userId` on the producer side. */
|
|
27
|
+
userId: string | null;
|
|
28
|
+
/** From `EnqueueOptions.classId` on the producer side. */
|
|
29
|
+
classId: string | null;
|
|
30
|
+
}
|
|
31
|
+
export interface CreateRunJobWorkerOptions<T extends RunJobType> {
|
|
32
|
+
type: T;
|
|
33
|
+
/** BullMQ `WorkerOptions` minus `connection`, which is supplied by the helper. */
|
|
34
|
+
workerOptions?: Omit<WorkerOptions, 'connection'>;
|
|
35
|
+
/**
|
|
36
|
+
* The actual work. Return any JSON-serialisable value (or `null`) — it gets
|
|
37
|
+
* persisted to `RunJob.result` for the client to read back.
|
|
38
|
+
*/
|
|
39
|
+
run: (input: JobInputs[T], ctx: RunJobContext) => Promise<Prisma.InputJsonValue | null>;
|
|
40
|
+
}
|
|
41
|
+
type WorkerData<T extends RunJobType> = JobInputs[T] & {
|
|
42
|
+
runJobId: string;
|
|
43
|
+
};
|
|
44
|
+
export declare function createRunJobWorker<T extends RunJobType>({ type, workerOptions, run, }: CreateRunJobWorkerOptions<T>): Worker<WorkerData<T>>;
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=createRunJobWorker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createRunJobWorker.d.ts","sourceRoot":"/","sources":["workers/processors/createRunJobWorker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,MAAM,EAAY,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAIzD,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,0DAA0D;IAC1D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,yBAAyB,CAAC,CAAC,SAAS,UAAU;IAC7D,IAAI,EAAE,CAAC,CAAC;IACR,kFAAkF;IAClF,aAAa,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;IAClD;;;OAGG;IACH,GAAG,EAAE,CACH,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EACnB,GAAG,EAAE,aAAa,KACf,OAAO,CAAC,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CAC5C;AAED,KAAK,UAAU,CAAC,CAAC,SAAS,UAAU,IAAI,SAAS,CAAC,CAAC,CAAC,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5E,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,UAAU,EAAE,EACvD,IAAI,EACJ,aAAa,EACb,GAAG,GACJ,EAAE,yBAAyB,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CA4GtD"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createRunJobWorker` — shared factory for every BullMQ processor in this app.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the boilerplate that every RunJob-backed worker needs:
|
|
5
|
+
* - Idempotent skip if the RunJob already SUCCEEDED (recovery after a crash
|
|
6
|
+
* that lost the BullMQ ack).
|
|
7
|
+
* - Status transitions: PENDING/RUNNING → RUNNING (with attempts++) → SUCCEEDED.
|
|
8
|
+
* - Failure handling: record latest error every attempt, flip to FAILED only
|
|
9
|
+
* on the final retry.
|
|
10
|
+
* - Standardised structured logging on `error` / `failed` / `completed`.
|
|
11
|
+
*
|
|
12
|
+
* A processor implementation only has to provide its `RunJobType`, optional
|
|
13
|
+
* BullMQ options (concurrency, limiter, etc.), and a `run` callback that does
|
|
14
|
+
* the actual work. The callback receives the typed `JobInputs[type]` payload
|
|
15
|
+
* and a context object with `runJobId`, `bullJobId`, `attempt`, plus the
|
|
16
|
+
* `userId` / `classId` from the original `enqueueRunJob` call.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="55ff9081-193c-5053-9606-34eeebf74f10")}catch(e){}}();
|
|
20
|
+
import { Worker } from 'bullmq';
|
|
21
|
+
import { Prisma } from '@prisma/client';
|
|
22
|
+
import { bullConnection } from '../connection.js';
|
|
23
|
+
import { prisma } from '../../lib/prisma.js';
|
|
24
|
+
import { logger } from '../../utils/logger.js';
|
|
25
|
+
import { QUEUE_NAMES } from '../queues.js';
|
|
26
|
+
export function createRunJobWorker({ type, workerOptions, run, }) {
|
|
27
|
+
const name = QUEUE_NAMES[type];
|
|
28
|
+
const worker = new Worker(name, async (job) => {
|
|
29
|
+
const { runJobId, ...rest } = job.data;
|
|
30
|
+
// The destructured rest is `Omit<JobInputs[T] & { runJobId }, 'runJobId'>`,
|
|
31
|
+
// which TS can't simplify back to `JobInputs[T]` while `T` is generic.
|
|
32
|
+
const input = rest;
|
|
33
|
+
const existing = await prisma.runJob.findUnique({
|
|
34
|
+
where: { id: runJobId },
|
|
35
|
+
select: { status: true, result: true, userId: true, classId: true },
|
|
36
|
+
});
|
|
37
|
+
if (existing?.status === 'SUCCEEDED') {
|
|
38
|
+
logger.info(`${name} RunJob already succeeded; skipping re-run`, {
|
|
39
|
+
runJobId,
|
|
40
|
+
bullJobId: job.id,
|
|
41
|
+
});
|
|
42
|
+
return existing.result;
|
|
43
|
+
}
|
|
44
|
+
await prisma.runJob.update({
|
|
45
|
+
where: { id: runJobId },
|
|
46
|
+
data: {
|
|
47
|
+
status: 'RUNNING',
|
|
48
|
+
startedAt: existing?.status === 'RUNNING' ? undefined : new Date(),
|
|
49
|
+
attempts: { increment: 1 },
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const result = await run(input, {
|
|
53
|
+
runJobId,
|
|
54
|
+
bullJobId: job.id,
|
|
55
|
+
attempt: job.attemptsMade + 1,
|
|
56
|
+
userId: existing?.userId ?? null,
|
|
57
|
+
classId: existing?.classId ?? null,
|
|
58
|
+
});
|
|
59
|
+
await prisma.runJob.update({
|
|
60
|
+
where: { id: runJobId },
|
|
61
|
+
data: {
|
|
62
|
+
status: 'SUCCEEDED',
|
|
63
|
+
finishedAt: new Date(),
|
|
64
|
+
error: null,
|
|
65
|
+
result: result === null ? Prisma.JsonNull : result,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
return result;
|
|
69
|
+
}, { connection: bullConnection, ...workerOptions });
|
|
70
|
+
worker.on('error', (err) => {
|
|
71
|
+
logger.error(`${name} worker error`, {
|
|
72
|
+
error: err.message,
|
|
73
|
+
stack: err.stack,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
worker.on('failed', async (job, err) => {
|
|
77
|
+
if (!job)
|
|
78
|
+
return;
|
|
79
|
+
const runJobId = job.data?.runJobId;
|
|
80
|
+
const isFinalAttempt = job.attemptsMade >= (job.opts.attempts ?? 1);
|
|
81
|
+
logger.error(`${name} job failed`, {
|
|
82
|
+
bullJobId: job.id,
|
|
83
|
+
runJobId,
|
|
84
|
+
attempt: job.attemptsMade,
|
|
85
|
+
finalAttempt: isFinalAttempt,
|
|
86
|
+
error: err.message,
|
|
87
|
+
});
|
|
88
|
+
if (!runJobId)
|
|
89
|
+
return;
|
|
90
|
+
await prisma.runJob
|
|
91
|
+
.update({
|
|
92
|
+
where: { id: runJobId },
|
|
93
|
+
data: {
|
|
94
|
+
error: err.message,
|
|
95
|
+
...(isFinalAttempt
|
|
96
|
+
? { status: 'FAILED', finishedAt: new Date() }
|
|
97
|
+
: {}),
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
.catch((updateError) => {
|
|
101
|
+
logger.error(`Failed to record ${name} failure on RunJob`, {
|
|
102
|
+
runJobId,
|
|
103
|
+
error: updateError instanceof Error
|
|
104
|
+
? updateError.message
|
|
105
|
+
: String(updateError),
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
worker.on('completed', (job) => {
|
|
110
|
+
logger.info(`${name} job completed`, {
|
|
111
|
+
bullJobId: job.id,
|
|
112
|
+
runJobId: job.data?.runJobId,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
return worker;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=createRunJobWorker.js.map
|
|
118
|
+
//# debugId=55ff9081-193c-5053-9606-34eeebf74f10
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createRunJobWorker.js","sources":["workers/processors/createRunJobWorker.ts"],"sourceRoot":"/","sourcesContent":["/**\n * `createRunJobWorker` — shared factory for every BullMQ processor in this app.\n *\n * Wraps the boilerplate that every RunJob-backed worker needs:\n * - Idempotent skip if the RunJob already SUCCEEDED (recovery after a crash\n * that lost the BullMQ ack).\n * - Status transitions: PENDING/RUNNING → RUNNING (with attempts++) → SUCCEEDED.\n * - Failure handling: record latest error every attempt, flip to FAILED only\n * on the final retry.\n * - Standardised structured logging on `error` / `failed` / `completed`.\n *\n * A processor implementation only has to provide its `RunJobType`, optional\n * BullMQ options (concurrency, limiter, etc.), and a `run` callback that does\n * the actual work. The callback receives the typed `JobInputs[type]` payload\n * and a context object with `runJobId`, `bullJobId`, `attempt`, plus the\n * `userId` / `classId` from the original `enqueueRunJob` call.\n */\nimport { Worker, type Job, type WorkerOptions } from 'bullmq';\nimport { Prisma, type RunJobType } from '@prisma/client';\nimport { bullConnection } from '../connection.js';\nimport { prisma } from '../../lib/prisma.js';\nimport { logger } from '../../utils/logger.js';\nimport { QUEUE_NAMES, type JobInputs } from '../queues.js';\n\nexport interface RunJobContext {\n runJobId: string;\n bullJobId: string | undefined;\n /** 1-indexed attempt number of the current run. */\n attempt: number;\n /** From `EnqueueOptions.userId` on the producer side. */\n userId: string | null;\n /** From `EnqueueOptions.classId` on the producer side. */\n classId: string | null;\n}\n\nexport interface CreateRunJobWorkerOptions<T extends RunJobType> {\n type: T;\n /** BullMQ `WorkerOptions` minus `connection`, which is supplied by the helper. */\n workerOptions?: Omit<WorkerOptions, 'connection'>;\n /**\n * The actual work. Return any JSON-serialisable value (or `null`) — it gets\n * persisted to `RunJob.result` for the client to read back.\n */\n run: (\n input: JobInputs[T],\n ctx: RunJobContext,\n ) => Promise<Prisma.InputJsonValue | null>;\n}\n\ntype WorkerData<T extends RunJobType> = JobInputs[T] & { runJobId: string };\n\nexport function createRunJobWorker<T extends RunJobType>({\n type,\n workerOptions,\n run,\n}: CreateRunJobWorkerOptions<T>): Worker<WorkerData<T>> {\n const name = QUEUE_NAMES[type];\n\n const worker = new Worker<WorkerData<T>>(\n name,\n async (job: Job<WorkerData<T>>) => {\n const { runJobId, ...rest } = job.data;\n // The destructured rest is `Omit<JobInputs[T] & { runJobId }, 'runJobId'>`,\n // which TS can't simplify back to `JobInputs[T]` while `T` is generic.\n const input = rest as unknown as JobInputs[T];\n\n const existing = await prisma.runJob.findUnique({\n where: { id: runJobId },\n select: { status: true, result: true, userId: true, classId: true },\n });\n\n if (existing?.status === 'SUCCEEDED') {\n logger.info(`${name} RunJob already succeeded; skipping re-run`, {\n runJobId,\n bullJobId: job.id,\n });\n return existing.result;\n }\n\n await prisma.runJob.update({\n where: { id: runJobId },\n data: {\n status: 'RUNNING',\n startedAt: existing?.status === 'RUNNING' ? undefined : new Date(),\n attempts: { increment: 1 },\n },\n });\n\n const result = await run(input, {\n runJobId,\n bullJobId: job.id,\n attempt: job.attemptsMade + 1,\n userId: existing?.userId ?? null,\n classId: existing?.classId ?? null,\n });\n\n await prisma.runJob.update({\n where: { id: runJobId },\n data: {\n status: 'SUCCEEDED',\n finishedAt: new Date(),\n error: null,\n result: result === null ? Prisma.JsonNull : result,\n },\n });\n\n return result;\n },\n { connection: bullConnection, ...workerOptions },\n );\n\n worker.on('error', (err) => {\n logger.error(`${name} worker error`, {\n error: err.message,\n stack: err.stack,\n });\n });\n\n worker.on('failed', async (job, err) => {\n if (!job) return;\n\n const runJobId = job.data?.runJobId;\n const isFinalAttempt = job.attemptsMade >= (job.opts.attempts ?? 1);\n\n logger.error(`${name} job failed`, {\n bullJobId: job.id,\n runJobId,\n attempt: job.attemptsMade,\n finalAttempt: isFinalAttempt,\n error: err.message,\n });\n\n if (!runJobId) return;\n\n await prisma.runJob\n .update({\n where: { id: runJobId },\n data: {\n error: err.message,\n ...(isFinalAttempt\n ? { status: 'FAILED', finishedAt: new Date() }\n : {}),\n },\n })\n .catch((updateError) => {\n logger.error(`Failed to record ${name} failure on RunJob`, {\n runJobId,\n error:\n updateError instanceof Error\n ? updateError.message\n : String(updateError),\n });\n });\n });\n\n worker.on('completed', (job) => {\n logger.info(`${name} job completed`, {\n bullJobId: job.id,\n runJobId: job.data?.runJobId,\n });\n });\n\n return worker;\n}\n"],"names":[],"mappings":"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","debug_id":"55ff9081-193c-5053-9606-34eeebf74f10"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gradeWorksheet.d.ts","sourceRoot":"/","sources":["workers/processors/gradeWorksheet.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB;;;;eAY/B,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="1acdee19-3e17-56ee-ab44-55001b9460f2")}catch(e){}}();
|
|
3
|
+
import { gradeWorksheetPipeline } from '../../pipelines/gradeWorksheet.js';
|
|
4
|
+
import { createRunJobWorker } from './createRunJobWorker.js';
|
|
5
|
+
export const gradeWorksheetWorker = createRunJobWorker({
|
|
6
|
+
type: 'GRADE_WORKSHEET',
|
|
7
|
+
workerOptions: {
|
|
8
|
+
concurrency: 2,
|
|
9
|
+
// Each pipeline run can fan out to many per-question inferences; keep
|
|
10
|
+
// throughput modest until we measure real-world usage.
|
|
11
|
+
limiter: { max: 5, duration: 60000 },
|
|
12
|
+
},
|
|
13
|
+
run: async ({ worksheetResponseId }) => {
|
|
14
|
+
await gradeWorksheetPipeline(worksheetResponseId);
|
|
15
|
+
return { worksheetResponseId };
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=gradeWorksheet.js.map
|
|
19
|
+
//# debugId=1acdee19-3e17-56ee-ab44-55001b9460f2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gradeWorksheet.js","sources":["workers/processors/gradeWorksheet.ts"],"sourceRoot":"/","sourcesContent":["import { gradeWorksheetPipeline } from '../../pipelines/gradeWorksheet.js';\nimport { createRunJobWorker } from './createRunJobWorker.js';\n\nexport const gradeWorksheetWorker = createRunJobWorker({\n type: 'GRADE_WORKSHEET',\n workerOptions: {\n concurrency: 2,\n // Each pipeline run can fan out to many per-question inferences; keep\n // throughput modest until we measure real-world usage.\n limiter: { max: 5, duration: 60_000 },\n },\n run: async ({ worksheetResponseId }) => {\n await gradeWorksheetPipeline(worksheetResponseId);\n return { worksheetResponseId };\n },\n});\n"],"names":[],"mappings":";;AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,CAAC,MAAM,oBAAoB,GAAG,kBAAkB,CAAC;IACrD,IAAI,EAAE,iBAAiB;IACvB,aAAa,EAAE;QACb,WAAW,EAAE,CAAC;QACd,sEAAsE;QACtE,uDAAuD;QACvD,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAM,EAAE;KACtC;IACD,GAAG,EAAE,KAAK,EAAE,EAAE,mBAAmB,EAAE,EAAE,EAAE;QACrC,MAAM,sBAAsB,CAAC,mBAAmB,CAAC,CAAC;QAClD,OAAO,EAAE,mBAAmB,EAAE,CAAC;IACjC,CAAC;CACF,CAAC,CAAC","debug_id":"1acdee19-3e17-56ee-ab44-55001b9460f2"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pdfToWorksheet.d.ts","sourceRoot":"/","sources":["workers/processors/pdfToWorksheet.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB;;;;;;eAa/B,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3beb4788-6c75-5aae-b6c1-3195cbdeffa3")}catch(e){}}();
|
|
3
|
+
import { pdfToWorksheet } from '../../pipelines/pdfToWorksheet.js';
|
|
4
|
+
import { createRunJobWorker } from './createRunJobWorker.js';
|
|
5
|
+
export const pdfToWorksheetWorker = createRunJobWorker({
|
|
6
|
+
type: 'PDF_TO_WORKSHEET',
|
|
7
|
+
workerOptions: {
|
|
8
|
+
concurrency: 2,
|
|
9
|
+
// 5 jobs / minute, e.g. to stay under OpenAI rate limits.
|
|
10
|
+
limiter: { max: 5, duration: 60000 },
|
|
11
|
+
},
|
|
12
|
+
run: async ({ gcsUrl, classId, worksheetId }) => {
|
|
13
|
+
const worksheet = await pdfToWorksheet(gcsUrl, classId, { worksheetId });
|
|
14
|
+
return worksheet
|
|
15
|
+
? { worksheetId: worksheet.id, name: worksheet.name }
|
|
16
|
+
: null;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
//# sourceMappingURL=pdfToWorksheet.js.map
|
|
20
|
+
//# debugId=3beb4788-6c75-5aae-b6c1-3195cbdeffa3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pdfToWorksheet.js","sources":["workers/processors/pdfToWorksheet.ts"],"sourceRoot":"/","sourcesContent":["import { pdfToWorksheet } from '../../pipelines/pdfToWorksheet.js';\nimport { createRunJobWorker } from './createRunJobWorker.js';\n\nexport const pdfToWorksheetWorker = createRunJobWorker({\n type: 'PDF_TO_WORKSHEET',\n workerOptions: {\n concurrency: 2,\n // 5 jobs / minute, e.g. to stay under OpenAI rate limits.\n limiter: { max: 5, duration: 60_000 },\n },\n run: async ({ gcsUrl, classId, worksheetId }) => {\n const worksheet = await pdfToWorksheet(gcsUrl, classId, { worksheetId });\n return worksheet\n ? { worksheetId: worksheet.id, name: worksheet.name }\n : null;\n },\n});\n"],"names":[],"mappings":";;AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,CAAC,MAAM,oBAAoB,GAAG,kBAAkB,CAAC;IACrD,IAAI,EAAE,kBAAkB;IACxB,aAAa,EAAE;QACb,WAAW,EAAE,CAAC;QACd,0DAA0D;QAC1D,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAM,EAAE;KACtC;IACD,GAAG,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE;QAC9C,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC;QACzE,OAAO,SAAS;YACd,CAAC,CAAC,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE;YACrD,CAAC,CAAC,IAAI,CAAC;IACX,CAAC;CACF,CAAC,CAAC","debug_id":"3beb4788-6c75-5aae-b6c1-3195cbdeffa3"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"studentProgressChat.d.ts","sourceRoot":"/","sources":["workers/processors/studentProgressChat.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,yBAAyB;;;;;;eAwBpC,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="13740d05-3496-50cc-a32d-baa98cef5e0b")}catch(e){}}();
|
|
3
|
+
import { chatAboutStudentProgress } from '../../services/studentProgress.js';
|
|
4
|
+
import { createRunJobWorker } from './createRunJobWorker.js';
|
|
5
|
+
export const studentProgressChatWorker = createRunJobWorker({
|
|
6
|
+
type: 'STUDENT_PROGRESS_CHAT',
|
|
7
|
+
workerOptions: {
|
|
8
|
+
concurrency: 4,
|
|
9
|
+
limiter: { max: 20, duration: 60000 },
|
|
10
|
+
},
|
|
11
|
+
run: async ({ classId, studentId, message }, ctx) => {
|
|
12
|
+
// `chatAboutStudentProgress` runs an authorization check using the viewer
|
|
13
|
+
// id, so callers MUST pass `userId` via `enqueueRunJob` options for the
|
|
14
|
+
// service to receive it here.
|
|
15
|
+
if (!ctx.userId) {
|
|
16
|
+
throw new Error('STUDENT_PROGRESS_CHAT requires `userId` on the RunJob (set via enqueueRunJob options)');
|
|
17
|
+
}
|
|
18
|
+
const response = await chatAboutStudentProgress(ctx.userId, {
|
|
19
|
+
classId,
|
|
20
|
+
studentId,
|
|
21
|
+
message,
|
|
22
|
+
});
|
|
23
|
+
return response;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
//# sourceMappingURL=studentProgressChat.js.map
|
|
27
|
+
//# debugId=13740d05-3496-50cc-a32d-baa98cef5e0b
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"studentProgressChat.js","sources":["workers/processors/studentProgressChat.ts"],"sourceRoot":"/","sourcesContent":["import { chatAboutStudentProgress } from '../../services/studentProgress.js';\nimport { createRunJobWorker } from './createRunJobWorker.js';\n\nexport const studentProgressChatWorker = createRunJobWorker({\n type: 'STUDENT_PROGRESS_CHAT',\n workerOptions: {\n concurrency: 4,\n limiter: { max: 20, duration: 60_000 },\n },\n run: async ({ classId, studentId, message }, ctx) => {\n // `chatAboutStudentProgress` runs an authorization check using the viewer\n // id, so callers MUST pass `userId` via `enqueueRunJob` options for the\n // service to receive it here.\n if (!ctx.userId) {\n throw new Error(\n 'STUDENT_PROGRESS_CHAT requires `userId` on the RunJob (set via enqueueRunJob options)',\n );\n }\n\n const response = await chatAboutStudentProgress(ctx.userId, {\n classId,\n studentId,\n message,\n });\n\n return response;\n },\n});\n"],"names":[],"mappings":";;AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,CAAC,MAAM,yBAAyB,GAAG,kBAAkB,CAAC;IAC1D,IAAI,EAAE,uBAAuB;IAC7B,aAAa,EAAE;QACb,WAAW,EAAE,CAAC;QACd,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAM,EAAE;KACvC;IACD,GAAG,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE;QAClD,0EAA0E;QAC1E,wEAAwE;QACxE,8BAA8B;QAC9B,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CACb,uFAAuF,CACxF,CAAC;QACJ,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,wBAAwB,CAAC,GAAG,CAAC,MAAM,EAAE;YAC1D,OAAO;YACP,SAAS;YACT,OAAO;SACR,CAAC,CAAC;QAEH,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC,CAAC","debug_id":"13740d05-3496-50cc-a32d-baa98cef5e0b"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BullMQ queue producers and the `enqueueRunJob` helper.
|
|
3
|
+
*
|
|
4
|
+
* Producer side only — safe to import from HTTP routes / services running on
|
|
5
|
+
* Vercel. Importing this file does NOT start any workers; workers live in
|
|
6
|
+
* `src/workers/processors/*` and are wired up by `src/workers/index.ts`,
|
|
7
|
+
* which is the entrypoint of the separate long-running worker process.
|
|
8
|
+
*/
|
|
9
|
+
import { type JobsOptions } from 'bullmq';
|
|
10
|
+
import { type RunJobType } from '@prisma/client';
|
|
11
|
+
/**
|
|
12
|
+
* Map RunJobType → (queue name, input payload shape).
|
|
13
|
+
*
|
|
14
|
+
* Add a new entry here whenever you introduce a new background job type.
|
|
15
|
+
* The queue name string must match what the corresponding processor passes
|
|
16
|
+
* to `new Worker(<name>, ...)`.
|
|
17
|
+
*/
|
|
18
|
+
export type JobInputs = {
|
|
19
|
+
/**
|
|
20
|
+
* `worksheetId` is the placeholder Worksheet row created synchronously by
|
|
21
|
+
* the producer (so the client has an id to navigate to). The pipeline fills
|
|
22
|
+
* that row in asynchronously and broadcasts progress on the per-worksheet
|
|
23
|
+
* job channel.
|
|
24
|
+
*/
|
|
25
|
+
PDF_TO_WORKSHEET: {
|
|
26
|
+
gcsUrl: string;
|
|
27
|
+
classId: string;
|
|
28
|
+
worksheetId: string;
|
|
29
|
+
};
|
|
30
|
+
GRADE_WORKSHEET: {
|
|
31
|
+
worksheetResponseId: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* `aiMessageId` is the placeholder AI Message row created synchronously by
|
|
35
|
+
* the producer before enqueue (so the client renders a PENDING bubble in
|
|
36
|
+
* the timeline). The pipeline fills in the content and flips the row to
|
|
37
|
+
* COMPLETED / FAILED. Used for both intros and replies — single shape.
|
|
38
|
+
*/
|
|
39
|
+
AI_LAB_CHAT: {
|
|
40
|
+
conversationId: string;
|
|
41
|
+
aiMessageId: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* `aiMessageId` placeholder pattern, same as AI_LAB_CHAT — see above.
|
|
45
|
+
*/
|
|
46
|
+
AI_NEWTON_CHAT: {
|
|
47
|
+
conversationId: string;
|
|
48
|
+
aiMessageId: string;
|
|
49
|
+
};
|
|
50
|
+
STUDENT_PROGRESS_CHAT: {
|
|
51
|
+
classId: string;
|
|
52
|
+
studentId: string;
|
|
53
|
+
message: string;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
export declare const QUEUE_NAMES: Record<RunJobType, string>;
|
|
57
|
+
export interface EnqueueOptions {
|
|
58
|
+
classId?: string | null;
|
|
59
|
+
userId?: string | null;
|
|
60
|
+
/** Override BullMQ job options (attempts, delay, priority, etc.). */
|
|
61
|
+
jobOptions?: JobsOptions;
|
|
62
|
+
}
|
|
63
|
+
export interface EnqueueResult {
|
|
64
|
+
runJobId: string;
|
|
65
|
+
bullJobId: string | null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create a `RunJob` audit row, enqueue the BullMQ job, and link the two.
|
|
69
|
+
*
|
|
70
|
+
* Returns immediately — the actual work happens on the worker process. The
|
|
71
|
+
* caller can return `runJobId` to the client and have them poll `getRunJob`
|
|
72
|
+
* (or subscribe over Pusher) to track progress.
|
|
73
|
+
*
|
|
74
|
+
* IMPORTANT: do not call this inside a Prisma `$transaction`. If the outer
|
|
75
|
+
* transaction rolls back after the BullMQ job is added, the worker will pick
|
|
76
|
+
* up an orphaned job pointing at a non-existent RunJob row. Enqueue *after*
|
|
77
|
+
* the transaction commits.
|
|
78
|
+
*/
|
|
79
|
+
export declare function enqueueRunJob<T extends RunJobType>(type: T, input: JobInputs[T], options?: EnqueueOptions): Promise<EnqueueResult>;
|
|
80
|
+
/**
|
|
81
|
+
* Close every cached queue. Call from the HTTP server's shutdown handler so
|
|
82
|
+
* the producer side disconnects from Redis cleanly.
|
|
83
|
+
*/
|
|
84
|
+
export declare function closeAllQueues(): Promise<void>;
|
|
85
|
+
//# sourceMappingURL=queues.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queues.d.ts","sourceRoot":"/","sources":["workers/queues.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAS,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,EAAU,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAYzD;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB;;;;;OAKG;IACH,gBAAgB,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3E,eAAe,EAAE;QAAE,mBAAmB,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD;;;;;OAKG;IACH,WAAW,EAAE;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7D;;OAEG;IACH,cAAc,EAAE;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAChE,qBAAqB,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAChF,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAMlD,CAAC;AAoBF,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,WAAW,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,CAAC,SAAS,UAAU,EACtD,IAAI,EAAE,CAAC,EACP,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EACnB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,aAAa,CAAC,CA2DxB;AAED;;;GAGG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAGpD"}
|