@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.
Files changed (237) hide show
  1. package/dist/index.js +15 -3
  2. package/dist/index.js.map +1 -1
  3. package/dist/lib/config/cors.d.ts.map +1 -1
  4. package/dist/lib/config/cors.js +2 -3
  5. package/dist/lib/config/cors.js.map +1 -1
  6. package/dist/lib/fileUpload.d.ts.map +1 -1
  7. package/dist/lib/fileUpload.js +4 -5
  8. package/dist/lib/fileUpload.js.map +1 -1
  9. package/dist/lib/pusher.d.ts +7 -1
  10. package/dist/lib/pusher.d.ts.map +1 -1
  11. package/dist/lib/pusher.js +9 -3
  12. package/dist/lib/pusher.js.map +1 -1
  13. package/dist/models/attendance.d.ts +16 -7
  14. package/dist/models/attendance.d.ts.map +1 -1
  15. package/dist/models/attendance.js +17 -2
  16. package/dist/models/attendance.js.map +1 -1
  17. package/dist/models/auth.d.ts +1 -0
  18. package/dist/models/auth.d.ts.map +1 -1
  19. package/dist/models/auth.js +3 -2
  20. package/dist/models/auth.js.map +1 -1
  21. package/dist/models/comment.d.ts +0 -5
  22. package/dist/models/comment.d.ts.map +1 -1
  23. package/dist/models/conversation.d.ts +17 -16
  24. package/dist/models/conversation.d.ts.map +1 -1
  25. package/dist/models/labChat.d.ts +2 -1
  26. package/dist/models/labChat.d.ts.map +1 -1
  27. package/dist/models/message.d.ts +6 -3
  28. package/dist/models/message.d.ts.map +1 -1
  29. package/dist/models/school.d.ts +0 -22
  30. package/dist/models/school.d.ts.map +1 -1
  31. package/dist/models/school.js +2 -14
  32. package/dist/models/school.js.map +1 -1
  33. package/dist/models/worksheet.d.ts +76 -19
  34. package/dist/models/worksheet.d.ts.map +1 -1
  35. package/dist/models/worksheet.js +2 -2
  36. package/dist/models/worksheet.js.map +1 -1
  37. package/dist/pipelines/aiLabChat.d.ts +20 -9
  38. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  39. package/dist/pipelines/aiLabChat.js +63 -75
  40. package/dist/pipelines/aiLabChat.js.map +1 -1
  41. package/dist/pipelines/aiNewtonChat.d.ts +13 -4
  42. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  43. package/dist/pipelines/aiNewtonChat.js +39 -27
  44. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  45. package/dist/pipelines/gradeWorksheet.d.ts +5 -3
  46. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  47. package/dist/pipelines/gradeWorksheet.js +60 -24
  48. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  49. package/dist/pipelines/pdfToWorksheet.d.ts +34 -2
  50. package/dist/pipelines/pdfToWorksheet.d.ts.map +1 -1
  51. package/dist/pipelines/pdfToWorksheet.js +205 -32
  52. package/dist/pipelines/pdfToWorksheet.js.map +1 -1
  53. package/dist/routers/_app.d.ts +106 -48
  54. package/dist/routers/_app.d.ts.map +1 -1
  55. package/dist/routers/announcement.d.ts +0 -4
  56. package/dist/routers/announcement.d.ts.map +1 -1
  57. package/dist/routers/assignment.d.ts +0 -8
  58. package/dist/routers/assignment.d.ts.map +1 -1
  59. package/dist/routers/assignment.js +64 -4
  60. package/dist/routers/assignment.js.map +1 -1
  61. package/dist/routers/auth.d.ts +1 -0
  62. package/dist/routers/auth.d.ts.map +1 -1
  63. package/dist/routers/comment.d.ts +0 -3
  64. package/dist/routers/comment.d.ts.map +1 -1
  65. package/dist/routers/conversation.d.ts +2 -1
  66. package/dist/routers/conversation.d.ts.map +1 -1
  67. package/dist/routers/labChat.d.ts +3 -1
  68. package/dist/routers/labChat.d.ts.map +1 -1
  69. package/dist/routers/message.d.ts +3 -0
  70. package/dist/routers/message.d.ts.map +1 -1
  71. package/dist/routers/newtonChat.d.ts +1 -0
  72. package/dist/routers/newtonChat.d.ts.map +1 -1
  73. package/dist/routers/school.d.ts +6 -0
  74. package/dist/routers/school.d.ts.map +1 -1
  75. package/dist/routers/school.js +7 -2
  76. package/dist/routers/school.js.map +1 -1
  77. package/dist/routers/worksheet.d.ts +37 -7
  78. package/dist/routers/worksheet.d.ts.map +1 -1
  79. package/dist/routers/worksheet.js +16 -6
  80. package/dist/routers/worksheet.js.map +1 -1
  81. package/dist/services/announcement.js +4 -4
  82. package/dist/services/announcement.js.map +1 -1
  83. package/dist/services/assignment.d.ts.map +1 -1
  84. package/dist/services/assignment.js +16 -6
  85. package/dist/services/assignment.js.map +1 -1
  86. package/dist/services/attendance.d.ts.map +1 -1
  87. package/dist/services/attendance.js +68 -4
  88. package/dist/services/attendance.js.map +1 -1
  89. package/dist/services/auth.d.ts +1 -0
  90. package/dist/services/auth.d.ts.map +1 -1
  91. package/dist/services/comment.d.ts +0 -3
  92. package/dist/services/comment.d.ts.map +1 -1
  93. package/dist/services/conversation.d.ts +2 -1
  94. package/dist/services/conversation.d.ts.map +1 -1
  95. package/dist/services/labChat.d.ts +10 -2
  96. package/dist/services/labChat.d.ts.map +1 -1
  97. package/dist/services/labChat.js +103 -44
  98. package/dist/services/labChat.js.map +1 -1
  99. package/dist/services/message.d.ts +3 -0
  100. package/dist/services/message.d.ts.map +1 -1
  101. package/dist/services/message.js +20 -2
  102. package/dist/services/message.js.map +1 -1
  103. package/dist/services/newtonChat.d.ts +1 -0
  104. package/dist/services/newtonChat.d.ts.map +1 -1
  105. package/dist/services/newtonChat.js +40 -17
  106. package/dist/services/newtonChat.js.map +1 -1
  107. package/dist/services/school.d.ts +6 -0
  108. package/dist/services/school.d.ts.map +1 -1
  109. package/dist/services/school.js +71 -6
  110. package/dist/services/school.js.map +1 -1
  111. package/dist/services/worksheet.d.ts +48 -11
  112. package/dist/services/worksheet.d.ts.map +1 -1
  113. package/dist/services/worksheet.js +73 -29
  114. package/dist/services/worksheet.js.map +1 -1
  115. package/dist/utils/generateUsername.d.ts +8 -0
  116. package/dist/utils/generateUsername.d.ts.map +1 -0
  117. package/dist/utils/generateUsername.js +103 -0
  118. package/dist/utils/generateUsername.js.map +1 -0
  119. package/dist/utils/inference.d.ts +50 -1
  120. package/dist/utils/inference.d.ts.map +1 -1
  121. package/dist/utils/inference.js +167 -2
  122. package/dist/utils/inference.js.map +1 -1
  123. package/dist/workers/connection.d.ts +3 -0
  124. package/dist/workers/connection.d.ts.map +1 -0
  125. package/dist/workers/connection.js +33 -0
  126. package/dist/workers/connection.js.map +1 -0
  127. package/dist/workers/index.d.ts +10 -0
  128. package/dist/workers/index.d.ts.map +1 -0
  129. package/dist/workers/index.js +63 -0
  130. package/dist/workers/index.js.map +1 -0
  131. package/dist/workers/processors/aiLabChat.d.ts +7 -0
  132. package/dist/workers/processors/aiLabChat.d.ts.map +1 -0
  133. package/dist/workers/processors/aiLabChat.js +52 -0
  134. package/dist/workers/processors/aiLabChat.js.map +1 -0
  135. package/dist/workers/processors/aiNewtonChat.d.ts +7 -0
  136. package/dist/workers/processors/aiNewtonChat.d.ts.map +1 -0
  137. package/dist/workers/processors/aiNewtonChat.js +57 -0
  138. package/dist/workers/processors/aiNewtonChat.js.map +1 -0
  139. package/dist/workers/processors/createRunJobWorker.d.ts +46 -0
  140. package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -0
  141. package/dist/workers/processors/createRunJobWorker.js +118 -0
  142. package/dist/workers/processors/createRunJobWorker.js.map +1 -0
  143. package/dist/workers/processors/gradeWorksheet.d.ts +6 -0
  144. package/dist/workers/processors/gradeWorksheet.d.ts.map +1 -0
  145. package/dist/workers/processors/gradeWorksheet.js +19 -0
  146. package/dist/workers/processors/gradeWorksheet.js.map +1 -0
  147. package/dist/workers/processors/pdfToWorksheet.d.ts +8 -0
  148. package/dist/workers/processors/pdfToWorksheet.d.ts.map +1 -0
  149. package/dist/workers/processors/pdfToWorksheet.js +20 -0
  150. package/dist/workers/processors/pdfToWorksheet.js.map +1 -0
  151. package/dist/workers/processors/studentProgressChat.d.ts +8 -0
  152. package/dist/workers/processors/studentProgressChat.d.ts.map +1 -0
  153. package/dist/workers/processors/studentProgressChat.js +27 -0
  154. package/dist/workers/processors/studentProgressChat.js.map +1 -0
  155. package/dist/workers/queues.d.ts +85 -0
  156. package/dist/workers/queues.d.ts.map +1 -0
  157. package/dist/workers/queues.js +119 -0
  158. package/dist/workers/queues.js.map +1 -0
  159. package/package.json +6 -3
  160. package/prisma/migrations/20260506162500_consolidate_run_job_and_unify_job_status/migration.sql +166 -0
  161. package/prisma/schema.prisma +465 -387
  162. package/src/index.ts +12 -1
  163. package/src/lib/config/cors.ts +0 -1
  164. package/src/lib/fileUpload.ts +2 -4
  165. package/src/lib/pusher.ts +7 -1
  166. package/src/models/attendance.ts +15 -0
  167. package/src/models/auth.ts +1 -0
  168. package/src/models/school.ts +0 -14
  169. package/src/models/worksheet.ts +2 -2
  170. package/src/pipelines/aiLabChat.ts +71 -79
  171. package/src/pipelines/aiNewtonChat.ts +47 -31
  172. package/src/pipelines/gradeWorksheet.ts +63 -25
  173. package/src/pipelines/pdfToWorksheet.ts +272 -34
  174. package/src/routers/assignment.ts +74 -3
  175. package/src/routers/school.ts +5 -0
  176. package/src/routers/worksheet.ts +25 -4
  177. package/src/services/announcement.ts +2 -2
  178. package/src/services/assignment.ts +18 -4
  179. package/src/services/attendance.ts +87 -2
  180. package/src/services/labChat.ts +108 -48
  181. package/src/services/message.ts +19 -0
  182. package/src/services/newtonChat.ts +46 -25
  183. package/src/services/school.ts +89 -6
  184. package/src/services/worksheet.ts +87 -28
  185. package/src/utils/generateUsername.ts +108 -0
  186. package/src/utils/inference.ts +209 -1
  187. package/src/workers/connection.ts +37 -0
  188. package/src/workers/index.ts +71 -0
  189. package/src/workers/processors/aiLabChat.ts +65 -0
  190. package/src/workers/processors/aiNewtonChat.ts +75 -0
  191. package/src/workers/processors/createRunJobWorker.ts +164 -0
  192. package/src/workers/processors/gradeWorksheet.ts +16 -0
  193. package/src/workers/processors/pdfToWorksheet.ts +17 -0
  194. package/src/workers/processors/studentProgressChat.ts +28 -0
  195. package/src/workers/queues.ts +174 -0
  196. package/tests/{globalSetup.ts → integration/globalSetup.ts} +4 -4
  197. package/tests/{helpers.ts → integration/helpers.ts} +2 -2
  198. package/tests/{pipelines → integration/pipelines}/pdfToWorksheet.test.ts +55 -43
  199. package/tests/{routers → integration/routers}/attendance.test.ts +61 -2
  200. package/tests/{routers → integration/routers}/auth.test.ts +3 -3
  201. package/tests/{routers → integration/routers}/class.test.ts +3 -17
  202. package/tests/{routers → integration/routers}/conversation.test.ts +1 -1
  203. package/tests/{routers → integration/routers}/event.test.ts +1 -1
  204. package/tests/{routers → integration/routers}/message.test.ts +1 -1
  205. package/tests/{routers → integration/routers}/newtonChat.test.ts +1 -1
  206. package/tests/{routers → integration/routers}/studentProgress.test.ts +1 -1
  207. package/tests/{user.test.ts → integration/routers/user.test.ts} +2 -2
  208. package/tests/integration/routers/worksheet.test.ts +448 -0
  209. package/tests/{setup.ts → integration/setup.ts} +4 -4
  210. package/tests/{lib → unit/lib}/cors.test.ts +1 -1
  211. package/tests/{middleware → unit/middleware}/security.test.ts +1 -1
  212. package/tests/{pipelines → unit/pipelines}/aiLabChat.test.ts +1 -1
  213. package/tests/{lib → unit/pipelines}/aiLabChatContract.test.ts +1 -1
  214. package/tests/{utils/aiLabChatPrompt.test.ts → unit/pipelines/labChatPrompt.test.ts} +2 -2
  215. package/tests/{utils → unit/services}/studentProgress.test.ts +5 -5
  216. package/tests/{utils → unit/utils}/aiUser.test.ts +1 -1
  217. package/tests/{utils → unit/utils}/generateInviteCode.test.ts +1 -1
  218. package/tests/{utils → unit/utils}/inference.test.ts +6 -6
  219. package/tests/{utils → unit/utils}/logger.test.ts +1 -1
  220. package/tests/{utils → unit/utils}/prismaErrorHandler.test.ts +1 -1
  221. package/tests/{utils → unit/utils}/prismaWrapper.test.ts +1 -1
  222. package/vitest.config.ts +11 -3
  223. package/vitest.unit.config.ts +10 -10
  224. package/tests/routers/worksheet.test.ts +0 -194
  225. package/tests/startup.test.ts +0 -5
  226. package/tests/worksheet.test.ts +0 -181
  227. /package/tests/{routers → integration/routers}/agenda.test.ts +0 -0
  228. /package/tests/{routers → integration/routers}/announcement.test.ts +0 -0
  229. /package/tests/{routers → integration/routers}/assignment.test.ts +0 -0
  230. /package/tests/{routers → integration/routers}/comment.test.ts +0 -0
  231. /package/tests/{routers → integration/routers}/folder.test.ts +0 -0
  232. /package/tests/{routers → integration/routers}/labChat.test.ts +0 -0
  233. /package/tests/{routers → integration/routers}/marketing.test.ts +0 -0
  234. /package/tests/{routers → integration/routers}/notification.test.ts +0 -0
  235. /package/tests/{routers → integration/routers}/search.test.ts +0 -0
  236. /package/tests/{routers → integration/routers}/section.test.ts +0 -0
  237. /package/tests/{server → unit/middleware}/rateLimit.test.ts +0 -0
@@ -1,13 +1,26 @@
1
1
  /**
2
2
  * PDF URL → worksheet: send a remote PDF URL to structured inference,
3
3
  * then persist a Worksheet and WorksheetQuestion rows.
4
+ *
5
+ * Normally driven by a BullMQ worker, so the producer (e.g.
6
+ * `createWorksheetFromUploadedPdf`) creates a placeholder Worksheet row up
7
+ * front and passes its id in via `worksheetId`. The pipeline broadcasts
8
+ * `Worksheet.status` transitions on the per-worksheet job Pusher channel
9
+ * (`set-pending` / `set-completed` / `set-failed` / `set-cancelled`) — same
10
+ * convention as `gradeWorksheet`.
11
+ *
12
+ * The legacy `pdfToWorksheet(gcsUrl, classId)` signature is preserved for
13
+ * direct/synchronous callers (such as tests): when no `worksheetId` is
14
+ * supplied the pipeline creates the row itself.
4
15
  */
5
16
 
6
- !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]="ea4c1689-45bc-59df-b76c-6c749341cffe")}catch(e){}}();
17
+ !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]="498d4f3f-e9a4-58fb-8e7a-ed49c5f116e7")}catch(e){}}();
7
18
  import { z } from "zod";
19
+ import { JobStatus, Prisma } from "@prisma/client";
8
20
  import { prisma } from "../lib/prisma.js";
9
21
  import { inference } from "../utils/inference.js";
10
22
  import { logger } from "../utils/logger.js";
23
+ import { pusher, worksheetJobChannel } from "../lib/pusher.js";
11
24
  /** Restrict remote fetches to Google-hosted object URLs. */
12
25
  function isProbablyGoogleCloudUrl(url) {
13
26
  try {
@@ -39,6 +52,36 @@ function sanitizeUrlForLogs(url) {
39
52
  return 'invalid-url';
40
53
  }
41
54
  }
55
+ /**
56
+ * Pusher event names mirror `gradeWorksheet`'s convention so clients can
57
+ * share the same JobStatus → event-name mapping.
58
+ */
59
+ const STATUS_EVENT_NAMES = {
60
+ NOT_STARTED: 'set-not-started',
61
+ PENDING: 'set-pending',
62
+ COMPLETED: 'set-completed',
63
+ FAILED: 'set-failed',
64
+ CANCELLED: 'set-cancelled',
65
+ };
66
+ /**
67
+ * Fire-and-forget broadcast of a `Worksheet.status` transition. We never let
68
+ * a Pusher outage break the actual pipeline: the persisted `Worksheet.status`
69
+ * is the source of truth, the channel just nudges clients.
70
+ */
71
+ async function emitStatus(worksheetId, status, extras = {}) {
72
+ if (!worksheetId)
73
+ return;
74
+ try {
75
+ await pusher.trigger(worksheetJobChannel(worksheetId), STATUS_EVENT_NAMES[status], { worksheetId, status, ...extras });
76
+ }
77
+ catch (error) {
78
+ logger.warn('pdfToWorksheet: failed to broadcast status update', {
79
+ worksheetId,
80
+ status,
81
+ error: error instanceof Error ? error.message : String(error),
82
+ });
83
+ }
84
+ }
42
85
  /** LLM output shape (validated by inference). */
43
86
  const worksheetDraftSchema = z.object({
44
87
  title: z.string(),
@@ -94,16 +137,48 @@ Rules: "questions" is an array ([] if none). Non-MCQ "options" can be []. Use "o
94
137
  If no answer is provided for a multiple-choice or true/false question, determine the answer from the options.`;
95
138
  /**
96
139
  * Sends a PDF URL to the model for remote retrieval, extracts a worksheet via
97
- * structured inference, and inserts worksheet + questions in one DB transaction.
140
+ * structured inference, and inserts (or back-fills) worksheet + questions.
141
+ *
142
+ * When called with `options.worksheetId` the pipeline:
143
+ * - keeps that row's id stable (the client already routed to it),
144
+ * - drives `status` PENDING → COMPLETED / FAILED,
145
+ * - fans out `set-*` Pusher events on `worksheetJobChannel`.
98
146
  */
99
- export async function pdfToWorksheet(gcsUrl, classId) {
100
- const sanitizedUrl = sanitizeUrlForLogs(gcsUrl);
147
+ export async function pdfToWorksheet(gcsUrl, classId, options = {}) {
148
+ const sourceUrl = sanitizeUrlForLogs(gcsUrl);
149
+ const placeholderWorksheetId = options.worksheetId;
150
+ await emitStatus(placeholderWorksheetId, JobStatus.PENDING);
101
151
  if (!isProbablyGoogleCloudUrl(gcsUrl)) {
102
152
  logger.warn("pdfToWorksheet: rejected unsupported PDF URL", {
103
- sanitizedUrl,
153
+ sanitizedUrl: sourceUrl,
104
154
  });
155
+ await markWorksheetFailed(placeholderWorksheetId, "Only Google Cloud Storage URLs are supported");
105
156
  throw new Error("Only Google Cloud Storage URLs are supported");
106
157
  }
158
+ // Look for a previously-completed worksheet pointing at the same source PDF.
159
+ // If there is one (and it's not the placeholder we were handed) we reuse it,
160
+ // mark the placeholder CANCELLED so the editor stops spinning, and return
161
+ // the canonical worksheet.
162
+ const existingForSource = await prisma.worksheet.findUnique({
163
+ where: { classId_sourceUrl: { classId, sourceUrl } },
164
+ });
165
+ if (existingForSource && existingForSource.id !== placeholderWorksheetId) {
166
+ logger.info("pdfToWorksheet: reusing existing worksheet for source PDF", {
167
+ worksheetId: existingForSource.id,
168
+ classId,
169
+ sourceUrl,
170
+ });
171
+ if (placeholderWorksheetId) {
172
+ await prisma.worksheet
173
+ .update({
174
+ where: { id: placeholderWorksheetId },
175
+ data: { status: JobStatus.CANCELLED },
176
+ })
177
+ .catch(() => undefined);
178
+ await emitStatus(placeholderWorksheetId, JobStatus.CANCELLED);
179
+ }
180
+ return existingForSource;
181
+ }
107
182
  const filename = getFilenameFromUrl(gcsUrl);
108
183
  const prompt = `Read the attached PDF and convert it into a single worksheet JSON object.\n\n${schemaInstructions}`;
109
184
  // Force Responses API input types so we can pass a remote PDF via `file_url`.
@@ -116,40 +191,138 @@ export async function pdfToWorksheet(gcsUrl, classId) {
116
191
  ],
117
192
  },
118
193
  ];
119
- const draft = await inference(input, worksheetDraftSchema, {
120
- reason: 'pdf_to_worksheet_draft',
121
- metadata: {
122
- classId,
123
- filename,
124
- },
125
- });
194
+ let draft;
195
+ try {
196
+ draft = await inference(input, worksheetDraftSchema, {
197
+ reason: 'pdf_to_worksheet_draft',
198
+ metadata: {
199
+ classId,
200
+ filename,
201
+ },
202
+ });
203
+ }
204
+ catch (error) {
205
+ const message = error instanceof Error ? error.message : String(error);
206
+ await markWorksheetFailed(placeholderWorksheetId, message);
207
+ throw error;
208
+ }
126
209
  if (!draft) {
127
210
  logger.warn("pdfToWorksheet: no model output", { filename });
211
+ await markWorksheetFailed(placeholderWorksheetId, "Model returned no parseable worksheet output");
128
212
  return null;
129
213
  }
130
- return prisma.$transaction(async (tx) => {
131
- const worksheet = await tx.worksheet.create({
132
- data: { classId, name: draft.title },
133
- });
134
- if (draft.questions.length > 0) {
135
- await tx.worksheetQuestion.createMany({
136
- data: draft.questions.map((question) => ({
137
- worksheetId: worksheet.id,
138
- type: question.type,
139
- question: question.question,
140
- answer: question.answer,
141
- points: Math.round(question.points ?? 0),
142
- options: question.options ?? [],
143
- markScheme: (question.markScheme ?? []).map((item) => ({
144
- ...item,
145
- points: Math.round(item.points),
214
+ try {
215
+ const worksheet = await prisma.$transaction(async (tx) => {
216
+ const created = placeholderWorksheetId
217
+ ? await tx.worksheet.update({
218
+ where: { id: placeholderWorksheetId },
219
+ data: {
220
+ classId,
221
+ name: draft.title,
222
+ sourceUrl,
223
+ status: JobStatus.PENDING,
224
+ },
225
+ })
226
+ : await tx.worksheet.create({
227
+ data: {
228
+ classId,
229
+ name: draft.title,
230
+ sourceUrl,
231
+ status: JobStatus.PENDING,
232
+ },
233
+ });
234
+ if (draft.questions.length > 0) {
235
+ // When re-running against an existing placeholder, drop any prior
236
+ // questions so we don't accumulate duplicates from retries.
237
+ if (placeholderWorksheetId) {
238
+ await tx.worksheetQuestion.deleteMany({
239
+ where: { worksheetId: created.id },
240
+ });
241
+ }
242
+ await tx.worksheetQuestion.createMany({
243
+ data: draft.questions.map((question) => ({
244
+ worksheetId: created.id,
245
+ type: question.type,
246
+ question: question.question,
247
+ answer: question.answer,
248
+ points: Math.round(question.points ?? 0),
249
+ options: question.options ?? [],
250
+ markScheme: (question.markScheme ?? []).map((item) => ({
251
+ ...item,
252
+ points: Math.round(item.points),
253
+ })),
254
+ order: Math.round(question.order),
146
255
  })),
147
- order: Math.round(question.order),
148
- })),
256
+ });
257
+ }
258
+ return tx.worksheet.update({
259
+ where: { id: created.id },
260
+ data: { status: JobStatus.COMPLETED },
149
261
  });
150
- }
262
+ });
263
+ await emitStatus(placeholderWorksheetId, JobStatus.COMPLETED, {
264
+ name: worksheet.name,
265
+ questionCount: draft.questions.length,
266
+ });
151
267
  return worksheet;
268
+ }
269
+ catch (error) {
270
+ if (error instanceof Prisma.PrismaClientKnownRequestError &&
271
+ error.code === "P2002") {
272
+ const worksheet = await prisma.worksheet.findUnique({
273
+ where: {
274
+ classId_sourceUrl: {
275
+ classId,
276
+ sourceUrl,
277
+ },
278
+ },
279
+ });
280
+ if (worksheet) {
281
+ logger.info("pdfToWorksheet: reused worksheet created by concurrent retry", {
282
+ worksheetId: worksheet.id,
283
+ classId,
284
+ sourceUrl,
285
+ });
286
+ if (placeholderWorksheetId && placeholderWorksheetId !== worksheet.id) {
287
+ await prisma.worksheet
288
+ .update({
289
+ where: { id: placeholderWorksheetId },
290
+ data: { status: JobStatus.CANCELLED },
291
+ })
292
+ .catch(() => undefined);
293
+ await emitStatus(placeholderWorksheetId, JobStatus.CANCELLED);
294
+ }
295
+ return worksheet;
296
+ }
297
+ }
298
+ const message = error instanceof Error ? error.message : String(error);
299
+ await markWorksheetFailed(placeholderWorksheetId, message);
300
+ throw error;
301
+ }
302
+ }
303
+ async function markWorksheetFailed(worksheetId, errorMessage) {
304
+ if (!worksheetId)
305
+ return;
306
+ await prisma.worksheet
307
+ .update({
308
+ where: { id: worksheetId },
309
+ data: { status: JobStatus.FAILED },
310
+ })
311
+ .catch((updateError) => {
312
+ logger.error('pdfToWorksheet: failed to mark worksheet FAILED', {
313
+ worksheetId,
314
+ error: updateError instanceof Error
315
+ ? updateError.message
316
+ : String(updateError),
317
+ });
318
+ });
319
+ // The full error is logged + persisted on `RunJob.error`; the broadcast
320
+ // intentionally carries no error payload to avoid leaking internals.
321
+ logger.error('pdfToWorksheet: marking worksheet FAILED', {
322
+ worksheetId,
323
+ error: errorMessage,
152
324
  });
325
+ await emitStatus(worksheetId, JobStatus.FAILED);
153
326
  }
154
327
  //# sourceMappingURL=pdfToWorksheet.js.map
155
- //# debugId=ea4c1689-45bc-59df-b76c-6c749341cffe
328
+ //# debugId=498d4f3f-e9a4-58fb-8e7a-ed49c5f116e7
@@ -1 +1 @@
1
- {"version":3,"file":"pdfToWorksheet.js","sources":["pipelines/pdfToWorksheet.ts"],"sourceRoot":"/","sourcesContent":["/**\n * PDF URL → worksheet: send a remote PDF URL to structured inference,\n * then persist a Worksheet and WorksheetQuestion rows.\n */\nimport { z } from \"zod\";\nimport type { ResponseInput } from \"openai/resources/responses/responses.js\";\nimport { prisma } from \"../lib/prisma.js\";\nimport { inference } from \"../utils/inference.js\";\nimport { logger } from \"../utils/logger.js\";\n\n/** Restrict remote fetches to Google-hosted object URLs. */\nfunction isProbablyGoogleCloudUrl(url: string): boolean {\n try {\n const host = new URL(url).hostname;\n return (\n host.endsWith(\"storage.googleapis.com\") ||\n host.endsWith(\"googleusercontent.com\") ||\n host.includes(\"storage.cloud.google.com\")\n );\n } catch {\n return false;\n }\n}\n\nfunction getFilenameFromUrl(url: string): string {\n try {\n const parsed = new URL(url);\n const lastSegment = parsed.pathname.split(\"/\").filter(Boolean).pop();\n return lastSegment && lastSegment.includes(\".\") ? lastSegment : \"document.pdf\";\n } catch {\n return \"document.pdf\";\n }\n}\n\nfunction sanitizeUrlForLogs(url: string): string {\n try {\n const parsed = new URL(url);\n return `${parsed.origin}${parsed.pathname}`;\n } catch {\n return 'invalid-url';\n }\n}\n\n/** LLM output shape (validated by inference). */\nconst worksheetDraftSchema = z.object({\n title: z.string(),\n questions: z\n .array(\n z.object({\n type: z.enum([\n \"MULTIPLE_CHOICE\",\n \"TRUE_FALSE\",\n \"SHORT_ANSWER\",\n \"LONG_ANSWER\",\n \"MATH_EXPRESSION\",\n \"ESSAY\",\n ]),\n question: z.string(),\n answer: z.string(),\n options: z\n .array(\n z.object({\n id: z.string(),\n text: z.string(),\n isCorrect: z.boolean(),\n })\n )\n .optional()\n .default([]),\n markScheme: z\n .array(\n z.object({\n id: z.string(),\n points: z.number().transform((value) => Math.round(value)),\n description: z.string(),\n })\n )\n .optional()\n .default([]),\n points: z.number().optional().default(0),\n order: z.number().transform((value) => Math.round(value)),\n })\n )\n .default([]),\n});\n\ntype WorksheetDraft = z.infer<typeof worksheetDraftSchema>;\n\nconst schemaInstructions = `Return ONLY valid JSON matching this schema:\n{\n \"title\": string,\n \"questions\": [\n {\n \"type\": \"MULTIPLE_CHOICE\" | \"TRUE_FALSE\" | \"SHORT_ANSWER\" | \"LONG_ANSWER\" | \"MATH_EXPRESSION\" | \"ESSAY\",\n \"question\": string,\n \"answer\": string,\n \"options\": [ { \"id\": string, \"text\": string, \"isCorrect\": boolean } ],\n \"markScheme\": [ { \"id\": string, \"points\": number, \"description\": string } ],\n \"points\": number,\n \"order\": number\n }\n ]\n}\nRules: \"questions\" is an array ([] if none). Non-MCQ \"options\" can be []. Use \"order\" 1,2,3,… in reading order. No markdown or extra keys.\nIf no answer is provided for a multiple-choice or true/false question, determine the answer from the options.`;\n\n/**\n * Sends a PDF URL to the model for remote retrieval, extracts a worksheet via\n * structured inference, and inserts worksheet + questions in one DB transaction.\n */\nexport async function pdfToWorksheet(gcsUrl: string, classId: string) {\n const sanitizedUrl = sanitizeUrlForLogs(gcsUrl);\n\n if (!isProbablyGoogleCloudUrl(gcsUrl)) {\n logger.warn(\"pdfToWorksheet: rejected unsupported PDF URL\", {\n sanitizedUrl,\n });\n throw new Error(\"Only Google Cloud Storage URLs are supported\");\n }\n\n const filename = getFilenameFromUrl(gcsUrl);\n const prompt = `Read the attached PDF and convert it into a single worksheet JSON object.\\n\\n${schemaInstructions}`;\n\n // Force Responses API input types so we can pass a remote PDF via `file_url`.\n const input: ResponseInput = [\n {\n role: \"user\",\n content: [\n { type: \"input_text\", text: prompt },\n { type: \"input_file\", file_url: gcsUrl },\n ],\n },\n ];\n\n const draft = await inference<WorksheetDraft>(\n input,\n worksheetDraftSchema,\n {\n reason: 'pdf_to_worksheet_draft',\n metadata: {\n classId,\n filename,\n },\n }\n );\n\n if (!draft) {\n logger.warn(\"pdfToWorksheet: no model output\", { filename });\n return null;\n }\n\n return prisma.$transaction(async (tx) => {\n const worksheet = await tx.worksheet.create({\n data: { classId, name: draft.title },\n });\n\n if (draft.questions.length > 0) {\n await tx.worksheetQuestion.createMany({\n data: draft.questions.map((question) => ({\n worksheetId: worksheet.id,\n type: question.type,\n question: question.question,\n answer: question.answer,\n points: Math.round(question.points ?? 0),\n options: question.options ?? [],\n markScheme: (question.markScheme ?? []).map((item) => ({\n ...item,\n points: Math.round(item.points),\n })),\n order: Math.round(question.order),\n })),\n });\n }\n\n return worksheet;\n });\n}\n"],"names":[],"mappings":"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","debug_id":"ea4c1689-45bc-59df-b76c-6c749341cffe"}
1
+ {"version":3,"file":"pdfToWorksheet.js","sources":["pipelines/pdfToWorksheet.ts"],"sourceRoot":"/","sourcesContent":["/**\n * PDF URL → worksheet: send a remote PDF URL to structured inference,\n * then persist a Worksheet and WorksheetQuestion rows.\n *\n * Normally driven by a BullMQ worker, so the producer (e.g.\n * `createWorksheetFromUploadedPdf`) creates a placeholder Worksheet row up\n * front and passes its id in via `worksheetId`. The pipeline broadcasts\n * `Worksheet.status` transitions on the per-worksheet job Pusher channel\n * (`set-pending` / `set-completed` / `set-failed` / `set-cancelled`) — same\n * convention as `gradeWorksheet`.\n *\n * The legacy `pdfToWorksheet(gcsUrl, classId)` signature is preserved for\n * direct/synchronous callers (such as tests): when no `worksheetId` is\n * supplied the pipeline creates the row itself.\n */\nimport { z } from \"zod\";\nimport type { ResponseInput } from \"openai/resources/responses/responses.js\";\nimport { JobStatus, Prisma } from \"@prisma/client\";\nimport { prisma } from \"../lib/prisma.js\";\nimport { inference } from \"../utils/inference.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { pusher, worksheetJobChannel } from \"../lib/pusher.js\";\n\n/** Restrict remote fetches to Google-hosted object URLs. */\nfunction isProbablyGoogleCloudUrl(url: string): boolean {\n try {\n const host = new URL(url).hostname;\n return (\n host.endsWith(\"storage.googleapis.com\") ||\n host.endsWith(\"googleusercontent.com\") ||\n host.includes(\"storage.cloud.google.com\")\n );\n } catch {\n return false;\n }\n}\n\nfunction getFilenameFromUrl(url: string): string {\n try {\n const parsed = new URL(url);\n const lastSegment = parsed.pathname.split(\"/\").filter(Boolean).pop();\n return lastSegment && lastSegment.includes(\".\") ? lastSegment : \"document.pdf\";\n } catch {\n return \"document.pdf\";\n }\n}\n\nfunction sanitizeUrlForLogs(url: string): string {\n try {\n const parsed = new URL(url);\n return `${parsed.origin}${parsed.pathname}`;\n } catch {\n return 'invalid-url';\n }\n}\n\n/**\n * Pusher event names mirror `gradeWorksheet`'s convention so clients can\n * share the same JobStatus → event-name mapping.\n */\nconst STATUS_EVENT_NAMES: Record<JobStatus, string> = {\n NOT_STARTED: 'set-not-started',\n PENDING: 'set-pending',\n COMPLETED: 'set-completed',\n FAILED: 'set-failed',\n CANCELLED: 'set-cancelled',\n};\n\n/**\n * Realtime payload for the per-worksheet job channel.\n *\n * On COMPLETED we ship the new title and question count so clients can\n * patch their card optimistically and side-step replica lag on the next\n * refetch. Error strings are intentionally NOT broadcast — they may carry\n * server internals; the full error stays on `RunJob.error` for ops only.\n */\nexport interface WorksheetJobStatusEvent {\n worksheetId: string;\n status: JobStatus;\n /** Filled on COMPLETED so clients can avoid a replica-lagged refetch. */\n name?: string;\n questionCount?: number;\n}\n\n/**\n * Fire-and-forget broadcast of a `Worksheet.status` transition. We never let\n * a Pusher outage break the actual pipeline: the persisted `Worksheet.status`\n * is the source of truth, the channel just nudges clients.\n */\nasync function emitStatus(\n worksheetId: string | undefined,\n status: JobStatus,\n extras: Pick<WorksheetJobStatusEvent, 'name' | 'questionCount'> = {},\n): Promise<void> {\n if (!worksheetId) return;\n\n try {\n await pusher.trigger(\n worksheetJobChannel(worksheetId),\n STATUS_EVENT_NAMES[status],\n { worksheetId, status, ...extras } satisfies WorksheetJobStatusEvent,\n );\n } catch (error) {\n logger.warn('pdfToWorksheet: failed to broadcast status update', {\n worksheetId,\n status,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n}\n\n/** LLM output shape (validated by inference). */\nconst worksheetDraftSchema = z.object({\n title: z.string(),\n questions: z\n .array(\n z.object({\n type: z.enum([\n \"MULTIPLE_CHOICE\",\n \"TRUE_FALSE\",\n \"SHORT_ANSWER\",\n \"LONG_ANSWER\",\n \"MATH_EXPRESSION\",\n \"ESSAY\",\n ]),\n question: z.string(),\n answer: z.string(),\n options: z\n .array(\n z.object({\n id: z.string(),\n text: z.string(),\n isCorrect: z.boolean(),\n })\n )\n .optional()\n .default([]),\n markScheme: z\n .array(\n z.object({\n id: z.string(),\n points: z.number().transform((value) => Math.round(value)),\n description: z.string(),\n })\n )\n .optional()\n .default([]),\n points: z.number().optional().default(0),\n order: z.number().transform((value) => Math.round(value)),\n })\n )\n .default([]),\n});\n\ntype WorksheetDraft = z.infer<typeof worksheetDraftSchema>;\n\nconst schemaInstructions = `Return ONLY valid JSON matching this schema:\n{\n \"title\": string,\n \"questions\": [\n {\n \"type\": \"MULTIPLE_CHOICE\" | \"TRUE_FALSE\" | \"SHORT_ANSWER\" | \"LONG_ANSWER\" | \"MATH_EXPRESSION\" | \"ESSAY\",\n \"question\": string,\n \"answer\": string,\n \"options\": [ { \"id\": string, \"text\": string, \"isCorrect\": boolean } ],\n \"markScheme\": [ { \"id\": string, \"points\": number, \"description\": string } ],\n \"points\": number,\n \"order\": number\n }\n ]\n}\nRules: \"questions\" is an array ([] if none). Non-MCQ \"options\" can be []. Use \"order\" 1,2,3,… in reading order. No markdown or extra keys.\nIf no answer is provided for a multiple-choice or true/false question, determine the answer from the options.`;\n\nexport interface PdfToWorksheetOptions {\n /**\n * Pre-existing Worksheet row id. When supplied, the pipeline fills in this\n * row instead of creating a new one and broadcasts progress on its job\n * channel. This is the path used by the BullMQ worker.\n */\n worksheetId?: string;\n}\n\n/**\n * Sends a PDF URL to the model for remote retrieval, extracts a worksheet via\n * structured inference, and inserts (or back-fills) worksheet + questions.\n *\n * When called with `options.worksheetId` the pipeline:\n * - keeps that row's id stable (the client already routed to it),\n * - drives `status` PENDING → COMPLETED / FAILED,\n * - fans out `set-*` Pusher events on `worksheetJobChannel`.\n */\nexport async function pdfToWorksheet(\n gcsUrl: string,\n classId: string,\n options: PdfToWorksheetOptions = {},\n) {\n const sourceUrl = sanitizeUrlForLogs(gcsUrl);\n const placeholderWorksheetId = options.worksheetId;\n\n await emitStatus(placeholderWorksheetId, JobStatus.PENDING);\n\n if (!isProbablyGoogleCloudUrl(gcsUrl)) {\n logger.warn(\"pdfToWorksheet: rejected unsupported PDF URL\", {\n sanitizedUrl: sourceUrl,\n });\n await markWorksheetFailed(\n placeholderWorksheetId,\n \"Only Google Cloud Storage URLs are supported\",\n );\n throw new Error(\"Only Google Cloud Storage URLs are supported\");\n }\n\n // Look for a previously-completed worksheet pointing at the same source PDF.\n // If there is one (and it's not the placeholder we were handed) we reuse it,\n // mark the placeholder CANCELLED so the editor stops spinning, and return\n // the canonical worksheet.\n const existingForSource = await prisma.worksheet.findUnique({\n where: { classId_sourceUrl: { classId, sourceUrl } },\n });\n\n if (existingForSource && existingForSource.id !== placeholderWorksheetId) {\n logger.info(\"pdfToWorksheet: reusing existing worksheet for source PDF\", {\n worksheetId: existingForSource.id,\n classId,\n sourceUrl,\n });\n\n if (placeholderWorksheetId) {\n await prisma.worksheet\n .update({\n where: { id: placeholderWorksheetId },\n data: { status: JobStatus.CANCELLED },\n })\n .catch(() => undefined);\n\n await emitStatus(placeholderWorksheetId, JobStatus.CANCELLED);\n }\n\n return existingForSource;\n }\n\n const filename = getFilenameFromUrl(gcsUrl);\n const prompt = `Read the attached PDF and convert it into a single worksheet JSON object.\\n\\n${schemaInstructions}`;\n\n // Force Responses API input types so we can pass a remote PDF via `file_url`.\n const input: ResponseInput = [\n {\n role: \"user\",\n content: [\n { type: \"input_text\", text: prompt },\n { type: \"input_file\", file_url: gcsUrl },\n ],\n },\n ];\n\n let draft: WorksheetDraft | null;\n try {\n draft = await inference<WorksheetDraft>(\n input,\n worksheetDraftSchema,\n {\n reason: 'pdf_to_worksheet_draft',\n metadata: {\n classId,\n filename,\n },\n }\n );\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n await markWorksheetFailed(placeholderWorksheetId, message);\n throw error;\n }\n\n if (!draft) {\n logger.warn(\"pdfToWorksheet: no model output\", { filename });\n await markWorksheetFailed(\n placeholderWorksheetId,\n \"Model returned no parseable worksheet output\",\n );\n return null;\n }\n\n try {\n const worksheet = await prisma.$transaction(async (tx) => {\n const created = placeholderWorksheetId\n ? await tx.worksheet.update({\n where: { id: placeholderWorksheetId },\n data: {\n classId,\n name: draft!.title,\n sourceUrl,\n status: JobStatus.PENDING,\n },\n })\n : await tx.worksheet.create({\n data: {\n classId,\n name: draft!.title,\n sourceUrl,\n status: JobStatus.PENDING,\n },\n });\n\n if (draft!.questions.length > 0) {\n // When re-running against an existing placeholder, drop any prior\n // questions so we don't accumulate duplicates from retries.\n if (placeholderWorksheetId) {\n await tx.worksheetQuestion.deleteMany({\n where: { worksheetId: created.id },\n });\n }\n\n await tx.worksheetQuestion.createMany({\n data: draft!.questions.map((question) => ({\n worksheetId: created.id,\n type: question.type,\n question: question.question,\n answer: question.answer,\n points: Math.round(question.points ?? 0),\n options: question.options ?? [],\n markScheme: (question.markScheme ?? []).map((item) => ({\n ...item,\n points: Math.round(item.points),\n })),\n order: Math.round(question.order),\n })),\n });\n }\n\n return tx.worksheet.update({\n where: { id: created.id },\n data: { status: JobStatus.COMPLETED },\n });\n });\n\n await emitStatus(placeholderWorksheetId, JobStatus.COMPLETED, {\n name: worksheet.name,\n questionCount: draft.questions.length,\n });\n\n return worksheet;\n } catch (error) {\n if (\n error instanceof Prisma.PrismaClientKnownRequestError &&\n error.code === \"P2002\"\n ) {\n const worksheet = await prisma.worksheet.findUnique({\n where: {\n classId_sourceUrl: {\n classId,\n sourceUrl,\n },\n },\n });\n\n if (worksheet) {\n logger.info(\"pdfToWorksheet: reused worksheet created by concurrent retry\", {\n worksheetId: worksheet.id,\n classId,\n sourceUrl,\n });\n\n if (placeholderWorksheetId && placeholderWorksheetId !== worksheet.id) {\n await prisma.worksheet\n .update({\n where: { id: placeholderWorksheetId },\n data: { status: JobStatus.CANCELLED },\n })\n .catch(() => undefined);\n\n await emitStatus(placeholderWorksheetId, JobStatus.CANCELLED);\n }\n\n return worksheet;\n }\n }\n\n const message = error instanceof Error ? error.message : String(error);\n await markWorksheetFailed(placeholderWorksheetId, message);\n throw error;\n }\n}\n\nasync function markWorksheetFailed(\n worksheetId: string | undefined,\n errorMessage: string,\n) {\n if (!worksheetId) return;\n\n await prisma.worksheet\n .update({\n where: { id: worksheetId },\n data: { status: JobStatus.FAILED },\n })\n .catch((updateError) => {\n logger.error('pdfToWorksheet: failed to mark worksheet FAILED', {\n worksheetId,\n error:\n updateError instanceof Error\n ? updateError.message\n : String(updateError),\n });\n });\n\n // The full error is logged + persisted on `RunJob.error`; the broadcast\n // intentionally carries no error payload to avoid leaking internals.\n logger.error('pdfToWorksheet: marking worksheet FAILED', {\n worksheetId,\n error: errorMessage,\n });\n\n await emitStatus(worksheetId, JobStatus.FAILED);\n}\n"],"names":[],"mappings":"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","debug_id":"498d4f3f-e9a4-58fb-8e7a-ed49c5f116e7"}