@studious-lms/server 1.13.0 → 1.15.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 (375) hide show
  1. package/.dockerignore +28 -0
  2. package/.env.example +5 -0
  3. package/.github/workflows/deploy.yml +123 -0
  4. package/Dockerfile +54 -0
  5. package/dist/index.js +5 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/instrument.js +3 -3
  8. package/dist/instrument.js.map +1 -1
  9. package/dist/lib/config/cors.d.ts.map +1 -1
  10. package/dist/lib/config/cors.js +31 -11
  11. package/dist/lib/config/cors.js.map +1 -1
  12. package/dist/lib/config/env.d.ts +17 -0
  13. package/dist/lib/config/env.d.ts.map +1 -1
  14. package/dist/lib/config/env.js +8 -2
  15. package/dist/lib/config/env.js.map +1 -1
  16. package/dist/lib/googleCloudStorage.d.ts +10 -0
  17. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  18. package/dist/lib/googleCloudStorage.js +22 -2
  19. package/dist/lib/googleCloudStorage.js.map +1 -1
  20. package/dist/lib/inferDocumentFormat.d.ts +10 -0
  21. package/dist/lib/inferDocumentFormat.d.ts.map +1 -0
  22. package/dist/lib/inferDocumentFormat.js +48 -0
  23. package/dist/lib/inferDocumentFormat.js.map +1 -0
  24. package/dist/lib/legal.d.ts +3 -0
  25. package/dist/lib/legal.d.ts.map +1 -0
  26. package/dist/lib/legal.js +6 -0
  27. package/dist/lib/legal.js.map +1 -0
  28. package/dist/lib/prisma.d.ts +104 -5
  29. package/dist/lib/prisma.d.ts.map +1 -1
  30. package/dist/lib/prisma.js +33 -2
  31. package/dist/lib/prisma.js.map +1 -1
  32. package/dist/models/agenda.d.ts +20 -20
  33. package/dist/models/announcement.d.ts +211 -101
  34. package/dist/models/announcement.d.ts.map +1 -1
  35. package/dist/models/assignment.d.ts +560 -362
  36. package/dist/models/assignment.d.ts.map +1 -1
  37. package/dist/models/attendance.d.ts +197 -71
  38. package/dist/models/attendance.d.ts.map +1 -1
  39. package/dist/models/auth.d.ts +356 -114
  40. package/dist/models/auth.d.ts.map +1 -1
  41. package/dist/models/auth.js +4 -2
  42. package/dist/models/auth.js.map +1 -1
  43. package/dist/models/class.d.ts +327 -201
  44. package/dist/models/class.d.ts.map +1 -1
  45. package/dist/models/class.js +2 -2
  46. package/dist/models/class.js.map +1 -1
  47. package/dist/models/cloudStorage.d.ts +146 -56
  48. package/dist/models/cloudStorage.d.ts.map +1 -1
  49. package/dist/models/comment.d.ts +262 -110
  50. package/dist/models/comment.d.ts.map +1 -1
  51. package/dist/models/conversation.d.ts +206 -80
  52. package/dist/models/conversation.d.ts.map +1 -1
  53. package/dist/models/department.d.ts +196 -70
  54. package/dist/models/department.d.ts.map +1 -1
  55. package/dist/models/event.d.ts +305 -141
  56. package/dist/models/event.d.ts.map +1 -1
  57. package/dist/models/file.d.ts +422 -272
  58. package/dist/models/file.d.ts.map +1 -1
  59. package/dist/models/folder.d.ts +433 -181
  60. package/dist/models/folder.d.ts.map +1 -1
  61. package/dist/models/labChat.d.ts +205 -79
  62. package/dist/models/labChat.d.ts.map +1 -1
  63. package/dist/models/marketing.d.ts +79 -38
  64. package/dist/models/marketing.d.ts.map +1 -1
  65. package/dist/models/marketing.js +12 -4
  66. package/dist/models/marketing.js.map +1 -1
  67. package/dist/models/message.d.ts +97 -43
  68. package/dist/models/message.d.ts.map +1 -1
  69. package/dist/models/newtonChat.d.ts +84 -30
  70. package/dist/models/newtonChat.d.ts.map +1 -1
  71. package/dist/models/notification.d.ts +57 -21
  72. package/dist/models/notification.d.ts.map +1 -1
  73. package/dist/models/school.d.ts +349 -133
  74. package/dist/models/school.d.ts.map +1 -1
  75. package/dist/models/section.d.ts +216 -72
  76. package/dist/models/section.d.ts.map +1 -1
  77. package/dist/models/tag.d.ts +156 -48
  78. package/dist/models/tag.d.ts.map +1 -1
  79. package/dist/models/user.d.ts +82 -28
  80. package/dist/models/user.d.ts.map +1 -1
  81. package/dist/models/worksheet.d.ts +1032 -423
  82. package/dist/models/worksheet.d.ts.map +1 -1
  83. package/dist/models/worksheet.js +8 -2
  84. package/dist/models/worksheet.js.map +1 -1
  85. package/dist/pipelines/aiLabChat.d.ts +7 -9
  86. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  87. package/dist/pipelines/aiLabChat.js +194 -125
  88. package/dist/pipelines/aiLabChat.js.map +1 -1
  89. package/dist/pipelines/aiLabChatContract.d.ts +233 -389
  90. package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
  91. package/dist/pipelines/aiLabChatContract.js +54 -70
  92. package/dist/pipelines/aiLabChatContract.js.map +1 -1
  93. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  94. package/dist/pipelines/aiNewtonChat.js +41 -41
  95. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  96. package/dist/pipelines/gradeWorksheet.d.ts +8 -8
  97. package/dist/pipelines/labChatPrompt.d.ts +18 -11
  98. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  99. package/dist/pipelines/labChatPrompt.js +93 -75
  100. package/dist/pipelines/labChatPrompt.js.map +1 -1
  101. package/dist/pipelines/pdfToWorksheet.d.ts +1 -1
  102. package/dist/routers/_app.d.ts +2601 -2151
  103. package/dist/routers/_app.d.ts.map +1 -1
  104. package/dist/routers/_app.js +4 -2
  105. package/dist/routers/_app.js.map +1 -1
  106. package/dist/routers/admin.d.ts +13 -13
  107. package/dist/routers/agenda.d.ts +18 -18
  108. package/dist/routers/announcement.d.ts +60 -60
  109. package/dist/routers/assignment.d.ts +393 -383
  110. package/dist/routers/assignment.d.ts.map +1 -1
  111. package/dist/routers/attendance.d.ts +10 -10
  112. package/dist/routers/auth.d.ts +4 -2
  113. package/dist/routers/auth.d.ts.map +1 -1
  114. package/dist/routers/auth.js +15 -2
  115. package/dist/routers/auth.js.map +1 -1
  116. package/dist/routers/class.d.ts +157 -153
  117. package/dist/routers/class.d.ts.map +1 -1
  118. package/dist/routers/comment.d.ts +18 -18
  119. package/dist/routers/conversation.d.ts +11 -11
  120. package/dist/routers/department.d.ts +34 -34
  121. package/dist/routers/discussion.d.ts +68 -56
  122. package/dist/routers/discussion.d.ts.map +1 -1
  123. package/dist/routers/discussion.js +6 -2
  124. package/dist/routers/discussion.js.map +1 -1
  125. package/dist/routers/embedding.d.ts +73 -0
  126. package/dist/routers/embedding.d.ts.map +1 -0
  127. package/dist/routers/embedding.js +151 -0
  128. package/dist/routers/embedding.js.map +1 -0
  129. package/dist/routers/event.d.ts +57 -55
  130. package/dist/routers/event.d.ts.map +1 -1
  131. package/dist/routers/file.d.ts +14 -14
  132. package/dist/routers/folder.d.ts +38 -38
  133. package/dist/routers/labChat.d.ts +16 -16
  134. package/dist/routers/marketing.d.ts +1 -14
  135. package/dist/routers/marketing.d.ts.map +1 -1
  136. package/dist/routers/message.d.ts +13 -13
  137. package/dist/routers/newtonChat.d.ts +4 -4
  138. package/dist/routers/notifications.d.ts +5 -5
  139. package/dist/routers/permissions.d.ts +14 -14
  140. package/dist/routers/school.d.ts +59 -55
  141. package/dist/routers/school.d.ts.map +1 -1
  142. package/dist/routers/search.d.ts +22 -1
  143. package/dist/routers/search.d.ts.map +1 -1
  144. package/dist/routers/search.js +9 -3
  145. package/dist/routers/search.js.map +1 -1
  146. package/dist/routers/section.d.ts +4 -4
  147. package/dist/routers/studentProgress.d.ts +1 -1
  148. package/dist/routers/tag.d.ts +7 -7
  149. package/dist/routers/user.d.ts +99 -10
  150. package/dist/routers/user.d.ts.map +1 -1
  151. package/dist/routers/user.js +19 -2
  152. package/dist/routers/user.js.map +1 -1
  153. package/dist/routers/worksheet.d.ts +93 -55
  154. package/dist/routers/worksheet.d.ts.map +1 -1
  155. package/dist/routers/worksheet.js +26 -2
  156. package/dist/routers/worksheet.js.map +1 -1
  157. package/dist/routes/aiStream.d.ts +2 -0
  158. package/dist/routes/aiStream.d.ts.map +1 -0
  159. package/dist/routes/aiStream.js +726 -0
  160. package/dist/routes/aiStream.js.map +1 -0
  161. package/dist/seedDatabase.d.ts +3 -1
  162. package/dist/seedDatabase.d.ts.map +1 -1
  163. package/dist/seedDatabase.js +6 -6
  164. package/dist/seedDatabase.js.map +1 -1
  165. package/dist/services/adminInstitution.d.ts +8 -8
  166. package/dist/services/adminRunJob.d.ts +4 -4
  167. package/dist/services/agenda.d.ts +18 -18
  168. package/dist/services/aiChatStream.d.ts +88 -0
  169. package/dist/services/aiChatStream.d.ts.map +1 -0
  170. package/dist/services/aiChatStream.js +254 -0
  171. package/dist/services/aiChatStream.js.map +1 -0
  172. package/dist/services/aiModifierTools.d.ts +16 -0
  173. package/dist/services/aiModifierTools.d.ts.map +1 -0
  174. package/dist/services/aiModifierTools.js +274 -0
  175. package/dist/services/aiModifierTools.js.map +1 -0
  176. package/dist/services/aiProposalTools.d.ts +32 -0
  177. package/dist/services/aiProposalTools.d.ts.map +1 -0
  178. package/dist/services/aiProposalTools.js +207 -0
  179. package/dist/services/aiProposalTools.js.map +1 -0
  180. package/dist/services/aiStream.d.ts +22 -0
  181. package/dist/services/aiStream.d.ts.map +1 -0
  182. package/dist/services/aiStream.js +142 -0
  183. package/dist/services/aiStream.js.map +1 -0
  184. package/dist/services/aiTools.d.ts +16 -0
  185. package/dist/services/aiTools.d.ts.map +1 -0
  186. package/dist/services/aiTools.js +271 -0
  187. package/dist/services/aiTools.js.map +1 -0
  188. package/dist/services/announcement.d.ts +42 -42
  189. package/dist/services/announcement.d.ts.map +1 -1
  190. package/dist/services/announcement.js +7 -2
  191. package/dist/services/announcement.js.map +1 -1
  192. package/dist/services/assignment.d.ts +355 -345
  193. package/dist/services/assignment.d.ts.map +1 -1
  194. package/dist/services/assignment.js +30 -9
  195. package/dist/services/assignment.js.map +1 -1
  196. package/dist/services/attendance.d.ts +10 -10
  197. package/dist/services/auth.d.ts +4 -2
  198. package/dist/services/auth.d.ts.map +1 -1
  199. package/dist/services/auth.js +13 -4
  200. package/dist/services/auth.js.map +1 -1
  201. package/dist/services/class.d.ts +146 -142
  202. package/dist/services/class.d.ts.map +1 -1
  203. package/dist/services/class.js +24 -4
  204. package/dist/services/class.js.map +1 -1
  205. package/dist/services/comment.d.ts +18 -18
  206. package/dist/services/conversation.d.ts +11 -11
  207. package/dist/services/department.d.ts +33 -33
  208. package/dist/services/discussion.d.ts +58 -58
  209. package/dist/services/discussion.d.ts.map +1 -1
  210. package/dist/services/discussion.js +15 -8
  211. package/dist/services/discussion.js.map +1 -1
  212. package/dist/services/discussionSummary.d.ts +21 -0
  213. package/dist/services/discussionSummary.d.ts.map +1 -0
  214. package/dist/services/discussionSummary.js +179 -0
  215. package/dist/services/discussionSummary.js.map +1 -0
  216. package/dist/services/embedding.d.ts +85 -0
  217. package/dist/services/embedding.d.ts.map +1 -0
  218. package/dist/services/embedding.js +372 -0
  219. package/dist/services/embedding.js.map +1 -0
  220. package/dist/services/event.d.ts +55 -53
  221. package/dist/services/event.d.ts.map +1 -1
  222. package/dist/services/file.d.ts +28 -28
  223. package/dist/services/folder.d.ts +36 -36
  224. package/dist/services/folder.d.ts.map +1 -1
  225. package/dist/services/folder.js +32 -2
  226. package/dist/services/folder.js.map +1 -1
  227. package/dist/services/labChat.d.ts +16 -16
  228. package/dist/services/marketing.d.ts +2 -15
  229. package/dist/services/marketing.d.ts.map +1 -1
  230. package/dist/services/marketing.js +5 -5
  231. package/dist/services/marketing.js.map +1 -1
  232. package/dist/services/message.d.ts +11 -11
  233. package/dist/services/message.d.ts.map +1 -1
  234. package/dist/services/message.js +2 -2
  235. package/dist/services/message.js.map +1 -1
  236. package/dist/services/newtonChat.d.ts +4 -4
  237. package/dist/services/notification.d.ts +5 -5
  238. package/dist/services/notification.d.ts.map +1 -1
  239. package/dist/services/openaiDocumentGen.d.ts +37 -0
  240. package/dist/services/openaiDocumentGen.d.ts.map +1 -0
  241. package/dist/services/openaiDocumentGen.js +262 -0
  242. package/dist/services/openaiDocumentGen.js.map +1 -0
  243. package/dist/services/permissions.d.ts +14 -14
  244. package/dist/services/school.d.ts +54 -50
  245. package/dist/services/school.d.ts.map +1 -1
  246. package/dist/services/search.d.ts +13 -1
  247. package/dist/services/search.d.ts.map +1 -1
  248. package/dist/services/search.js +145 -2
  249. package/dist/services/search.js.map +1 -1
  250. package/dist/services/section.d.ts +3 -3
  251. package/dist/services/tag.d.ts +4 -4
  252. package/dist/services/user.d.ts +4 -4
  253. package/dist/services/user.d.ts.map +1 -1
  254. package/dist/services/user.js +10 -15
  255. package/dist/services/user.js.map +1 -1
  256. package/dist/services/userPrivacy.d.ts +86 -0
  257. package/dist/services/userPrivacy.d.ts.map +1 -0
  258. package/dist/services/userPrivacy.js +184 -0
  259. package/dist/services/userPrivacy.js.map +1 -0
  260. package/dist/services/worksheet.d.ts +86 -57
  261. package/dist/services/worksheet.d.ts.map +1 -1
  262. package/dist/services/worksheet.js +57 -5
  263. package/dist/services/worksheet.js.map +1 -1
  264. package/dist/utils/email.d.ts.map +1 -1
  265. package/dist/utils/email.js +7 -3
  266. package/dist/utils/email.js.map +1 -1
  267. package/dist/utils/streamBroadcast.d.ts +11 -0
  268. package/dist/utils/streamBroadcast.d.ts.map +1 -0
  269. package/dist/utils/streamBroadcast.js +32 -0
  270. package/dist/utils/streamBroadcast.js.map +1 -0
  271. package/dist/workers/connection.d.ts +15 -0
  272. package/dist/workers/connection.d.ts.map +1 -1
  273. package/dist/workers/connection.js +20 -2
  274. package/dist/workers/connection.js.map +1 -1
  275. package/dist/workers/index.js +4 -2
  276. package/dist/workers/index.js.map +1 -1
  277. package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -1
  278. package/dist/workers/processors/createRunJobWorker.js +31 -8
  279. package/dist/workers/processors/createRunJobWorker.js.map +1 -1
  280. package/dist/workers/processors/embedContent.d.ts +8 -0
  281. package/dist/workers/processors/embedContent.d.ts.map +1 -0
  282. package/dist/workers/processors/embedContent.js +67 -0
  283. package/dist/workers/processors/embedContent.js.map +1 -0
  284. package/dist/workers/queues.d.ts +6 -0
  285. package/dist/workers/queues.d.ts.map +1 -1
  286. package/dist/workers/queues.js +5 -3
  287. package/dist/workers/queues.js.map +1 -1
  288. package/package.json +5 -1
  289. package/prisma/migrations/20260609215135_init/migration.sql +1412 -0
  290. package/prisma/migrations/20260610130000_add_worksheet_tags/migration.sql +13 -0
  291. package/prisma/migrations/20260610140000_expand_embedding_source_types/migration.sql +4 -0
  292. package/prisma/migrations/20260611120000_add_user_terms_acceptance/migration.sql +3 -0
  293. package/prisma/schema.prisma +44 -7
  294. package/src/index.ts +4 -0
  295. package/src/instrument.ts +1 -1
  296. package/src/lib/config/cors.ts +32 -9
  297. package/src/lib/config/env.ts +6 -0
  298. package/src/lib/googleCloudStorage.ts +21 -0
  299. package/src/lib/inferDocumentFormat.ts +58 -0
  300. package/src/lib/legal.ts +2 -0
  301. package/src/lib/prisma.ts +40 -1
  302. package/src/models/auth.ts +4 -0
  303. package/src/models/class.ts +3 -2
  304. package/src/models/marketing.ts +10 -2
  305. package/src/models/worksheet.ts +5 -2
  306. package/src/pipelines/aiLabChat.ts +245 -146
  307. package/src/pipelines/aiLabChatContract.ts +59 -69
  308. package/src/pipelines/aiNewtonChat.ts +46 -43
  309. package/src/pipelines/labChatPrompt.ts +115 -90
  310. package/src/routers/_app.ts +2 -0
  311. package/src/routers/auth.ts +13 -0
  312. package/src/routers/discussion.ts +7 -0
  313. package/src/routers/embedding.ts +164 -0
  314. package/src/routers/search.ts +10 -1
  315. package/src/routers/user.ts +26 -0
  316. package/src/routers/worksheet.ts +27 -0
  317. package/src/routes/aiStream.ts +827 -0
  318. package/src/seedDatabase.ts +4 -4
  319. package/src/services/aiChatStream.ts +341 -0
  320. package/src/services/aiModifierTools.ts +341 -0
  321. package/src/services/aiProposalTools.ts +274 -0
  322. package/src/services/aiStream.ts +175 -0
  323. package/src/services/aiTools.ts +290 -0
  324. package/src/services/announcement.ts +7 -0
  325. package/src/services/assignment.ts +31 -7
  326. package/src/services/auth.ts +14 -2
  327. package/src/services/class.ts +27 -2
  328. package/src/services/discussion.ts +22 -8
  329. package/src/services/discussionSummary.ts +226 -0
  330. package/src/services/embedding.ts +446 -0
  331. package/src/services/folder.ts +31 -0
  332. package/src/services/marketing.ts +3 -3
  333. package/src/services/message.ts +2 -2
  334. package/src/services/openaiDocumentGen.ts +319 -0
  335. package/src/services/search.ts +163 -1
  336. package/src/services/user.ts +9 -17
  337. package/src/services/userPrivacy.ts +196 -0
  338. package/src/services/worksheet.ts +77 -3
  339. package/src/utils/email.ts +5 -1
  340. package/src/utils/streamBroadcast.ts +48 -0
  341. package/src/workers/connection.ts +19 -0
  342. package/src/workers/index.ts +2 -0
  343. package/src/workers/processors/createRunJobWorker.ts +32 -6
  344. package/src/workers/processors/embedContent.ts +86 -0
  345. package/src/workers/queues.ts +5 -1
  346. package/tests/helpers/registerInput.ts +20 -0
  347. package/tests/integration/globalSetup.ts +10 -18
  348. package/tests/integration/routers/auth.test.ts +13 -18
  349. package/tests/unit/lib/inferDocumentFormat.test.ts +33 -0
  350. package/tests/unit/pipelines/aiLabChatContract.test.ts +38 -21
  351. package/tests/unit/pipelines/labChatPrompt.test.ts +23 -36
  352. package/tests/unit/services/aiChatStream.test.ts +92 -0
  353. package/tests/unit/services/openaiDocumentGen.test.ts +65 -0
  354. package/prisma/migrations/20250807062924_init/migration.sql +0 -436
  355. package/prisma/migrations/20250919063704_init2/migration.sql +0 -24
  356. package/prisma/migrations/20250920143543_add_profile_fields/migration.sql +0 -15
  357. package/prisma/migrations/20250922165147_add_chat_models/migration.sql +0 -68
  358. package/prisma/migrations/20250922165314_add_chat_roles/migration.sql +0 -5
  359. package/prisma/migrations/20250922171242_refactor_notification_system/migration.sql +0 -20
  360. package/prisma/migrations/20250922172208_add_mentions_system/migration.sql +0 -21
  361. package/prisma/migrations/20250925072732_add_lab_chat_system/migration.sql +0 -25
  362. package/prisma/migrations/20251020151505_add_upload_tracking_fields/migration.sql +0 -57
  363. package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +0 -30
  364. package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +0 -35
  365. package/prisma/migrations/20260410124000_add_submission_recommendation_state/migration.sql +0 -14
  366. package/prisma/migrations/20260415103500_add_session_type/migration.sql +0 -33
  367. package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +0 -19
  368. package/prisma/migrations/20260417083000_add_conversation_member_hidden_at/migration.sql +0 -2
  369. package/prisma/migrations/20260502230500_assignment_submission_comments/migration.sql +0 -60
  370. package/prisma/migrations/20260504071500_school_memberships/migration.sql +0 -81
  371. package/prisma/migrations/20260506162500_consolidate_run_job_and_unify_job_status/migration.sql +0 -166
  372. package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +0 -184
  373. package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +0 -93
  374. package/prisma/migrations/20260510183000_add_discussions/migration.sql +0 -151
  375. /package/.github/workflows/{npm.yml → npm-publish.yml} +0 -0
@@ -1,23 +1,36 @@
1
1
  /**
2
2
  * AI lab chat pipeline – generates lab introductions and responses.
3
- * Can create worksheets, sections, assignments, and PDF docs from AI output.
3
+ * Can create worksheets, sections, assignments, and generated files from AI output.
4
4
  */
5
5
  import { isAIUser } from "../utils/aiUser.js";
6
6
  import { prisma } from "../lib/prisma.js";
7
- import { pusher, teacherChannel } from "../lib/pusher.js";
7
+ import { pusher, chatChannel, teacherChannel } from "../lib/pusher.js";
8
8
  import type { Assignment, Class, File, Section, User } from "@prisma/client";
9
9
  import {
10
- inference,
11
10
  completePendingAIMessage,
12
11
  failPendingAIMessage,
13
12
  } from "../utils/inference.js";
14
13
  import { logger } from "../utils/logger.js";
15
- import { createPdf } from "../lib/jsonConversion.js";
16
14
  import { v4 } from "uuid";
17
- import { bucket } from "../lib/googleCloudStorage.js";
18
- import { DocumentBlock } from "../lib/jsonStyles.js";
19
- import { type LabChatResponse, labChatResponseSchema } from "./aiLabChatContract.js";
20
- import { buildLabChatResponseMessages } from "./labChatPrompt.js";
15
+ import { bucket, getSignedUrl } from "../lib/googleCloudStorage.js";
16
+ import {
17
+ generateDocumentWithOpenAI,
18
+ normalizeDocumentFormat,
19
+ } from "../services/openaiDocumentGen.js";
20
+ import { inferDocumentFormat } from "../lib/inferDocumentFormat.js";
21
+ import { broadcastTextChunks, broadcastStreamComplete } from "../utils/streamBroadcast.js";
22
+ import { buildLabChatAgentMessages } from "./labChatPrompt.js";
23
+ import type { ConversationMessage } from "./labChatPrompt.js";
24
+ import {
25
+ applyResolvedFormatsToStreamParts,
26
+ enrichStreamPartsWithGeneratedFiles,
27
+ finalizeDeferredToolParts,
28
+ streamChatResponse,
29
+ type GeneratedDocFileLink,
30
+ type StreamPart,
31
+ } from "../services/aiChatStream.js";
32
+ import { aiTools } from "../services/aiTools.js";
33
+ import { labProposalTools, createLabProposals } from "../services/aiProposalTools.js";
21
34
 
22
35
  /** Cap model wait time; leave headroom after inference for PDF/GCS/DB within Vercel maxDuration (500s). */
23
36
  const LAB_CHAT_RESPONSE_TIMEOUT_MS = 400_000;
@@ -72,6 +85,12 @@ type RecentLabChatMessage = {
72
85
  displayName: string | null;
73
86
  } | null;
74
87
  } | null;
88
+ attachments: {
89
+ id: string;
90
+ name: string;
91
+ type: string;
92
+ path: string;
93
+ }[];
75
94
  };
76
95
 
77
96
  /**
@@ -116,6 +135,14 @@ const loadRecentLabChatMessages = async (
116
135
  },
117
136
  },
118
137
  },
138
+ attachments: {
139
+ select: {
140
+ id: true,
141
+ name: true,
142
+ type: true,
143
+ path: true,
144
+ },
145
+ },
119
146
  };
120
147
 
121
148
  const newestMessages = await prisma.message.findMany({
@@ -186,6 +213,42 @@ const loadRecentLabChatMessages = async (
186
213
  .slice(0, limit);
187
214
  };
188
215
 
216
+ /**
217
+ * Resolves signed URLs for image attachments in messages so the LLM can see them.
218
+ * Non-image files are passed as text descriptions only.
219
+ */
220
+ async function resolveAttachmentUrls(
221
+ messages: RecentLabChatMessage[],
222
+ ): Promise<ConversationMessage[]> {
223
+ const resolved: ConversationMessage[] = [];
224
+
225
+ for (const msg of messages) {
226
+ const attachments = msg.attachments ?? [];
227
+ if (attachments.length === 0) {
228
+ resolved.push({ ...msg, attachments: [] });
229
+ continue;
230
+ }
231
+
232
+ const withUrls = await Promise.all(
233
+ attachments.map(async (a) => {
234
+ if (a.type.startsWith('image/')) {
235
+ try {
236
+ const url = await getSignedUrl(a.path);
237
+ return { id: a.id, name: a.name, type: a.type, url };
238
+ } catch {
239
+ return { id: a.id, name: a.name, type: a.type };
240
+ }
241
+ }
242
+ return { id: a.id, name: a.name, type: a.type };
243
+ }),
244
+ );
245
+
246
+ resolved.push({ ...msg, attachments: withUrls });
247
+ }
248
+
249
+ return resolved;
250
+ }
251
+
189
252
  /**
190
253
  * Builds schema-aware context for the AI from class data.
191
254
  * Formats entities with IDs so the model can reference them when creating assignments.
@@ -303,33 +366,6 @@ export const getBaseSystemPrompt = (
303
366
 
304
367
 
305
368
 
306
- /**
307
- * Generate labchat responses
308
- * Allow for the generation of the following:
309
- * - Assignment(s) either individual or bulk as an lesson / course plan.
310
- * - Worksheet(s) either individual or bulk as an lesson / course plan.
311
- * - Files (PDFs)
312
- * @param labChatId
313
- */
314
- // export const sendAiLabChatResponsePipeline = async (labChatId: string) => {
315
- // const message = await prisma?.message.create({
316
- // data: {
317
- // content: "GENERATING_CONTENT",
318
- // senderId: getAIUserId(),
319
- // conversationId: labChatId,
320
- // status: JobStatus.PENDING,
321
- // },
322
- // });
323
-
324
- // try {
325
-
326
- // inference(`
327
- // `)
328
- // }
329
-
330
- // };
331
-
332
-
333
369
  /**
334
370
  * Fill the AI introduction placeholder for a brand-new lab chat.
335
371
  *
@@ -345,9 +381,10 @@ export const generateAndSendLabIntroduction = async (
345
381
  aiMessageId: string,
346
382
  ): Promise<void> => {
347
383
  const introMessage =
348
- "Hello teaching team! I'm your AI assistant for course material development. I'll help you create educational content - when you have a clear request, I'll produce it directly. I only ask questions when I need to clarify the topic or scope. What would you like to work on?";
384
+ "Hello teaching team! I'm your AI assistant for course material development. I can create worksheets, assignments, and downloadable files — PDFs, Word docs, PowerPoints, spreadsheets, and more. When you have a clear request, I'll produce it directly. I only ask questions when I need to clarify the topic or scope. What would you like to work on?";
349
385
 
350
386
  try {
387
+ await broadcastTextChunks(conversationId, aiMessageId, introMessage);
351
388
  await completePendingAIMessage(aiMessageId, introMessage);
352
389
  logger.info('AI Introduction sent', { labChatId, conversationId, aiMessageId });
353
390
  } catch (error) {
@@ -376,7 +413,7 @@ export const generateAndSendLabIntroduction = async (
376
413
  */
377
414
  export const generateAndSendLabResponse = async (
378
415
  labChatId: string,
379
- _teacherMessage: string,
416
+ teacherMessage: string,
380
417
  emitOptions: { classId: string; aiMessageId: string }
381
418
  ): Promise<void> => {
382
419
  try {
@@ -474,133 +511,195 @@ export const generateAndSendLabIntroduction = async (
474
511
  assignments: classData.assignments,
475
512
  });
476
513
 
477
- const messages = buildLabChatResponseMessages({
514
+ // Resolve signed URLs for image attachments so the LLM can see them
515
+ const messagesWithAttachments = await resolveAttachmentUrls(recentMessages.reverse());
516
+
517
+ const { system, messages } = buildLabChatAgentMessages({
478
518
  context: fullLabChat.context,
479
519
  classContext,
480
- recentMessages: recentMessages.reverse(),
520
+ recentMessages: messagesWithAttachments,
481
521
  isAIUser,
482
522
  });
483
-
484
-
485
- // const completion = await inferenceClient.chat.completions.create({
486
- // model: 'command-a-03-2025',
487
- // messages,
488
- // temperature: 0.7,
489
- // response_format: zodTextFormat(labChatResponseSchema, "lab_chat_response_format"),
490
- // });
491
-
492
- const response = await withTimeout(
493
- inference<LabChatResponse>(messages, labChatResponseSchema, {
494
- reason: 'lab_chat_response',
495
- metadata: {
496
- labChatId,
523
+
524
+ // Genuine agentic generation: the model streams prose token-by-token and
525
+ // calls real tools inline — read-only RAG lookups plus "propose_*" tools
526
+ // that record create-actions into `proposals`. `streamChatResponse`
527
+ // broadcasts the real text chunks and tool-call events as they happen.
528
+ const proposals = createLabProposals();
529
+ const tools = {
530
+ ...aiTools(fullLabChat.classId),
531
+ ...labProposalTools(proposals),
532
+ };
533
+
534
+ const { text: streamedText, parts: rawStreamParts } = await withTimeout(
535
+ streamChatResponse({
497
536
  conversationId,
498
- classId: fullLabChat.classId,
499
537
  aiMessageId: emitOptions.aiMessageId,
500
- },
501
- }),
502
- LAB_CHAT_RESPONSE_TIMEOUT_MS,
503
- "Lab chat response generation",
504
- );
505
-
506
- if (!response) {
507
- throw new Error('No response generated from inference API');
508
- }
509
- // Parse the JSON response and generate PDF if docs are provided
538
+ system,
539
+ messages,
540
+ tools,
541
+ temperature: 0.7,
542
+ maxOutputTokens: 4096,
543
+ maxSteps: 8,
544
+ }),
545
+ LAB_CHAT_RESPONSE_TIMEOUT_MS,
546
+ "Lab chat response generation",
547
+ );
548
+
549
+ // Real text + tool-call events were already broadcast inline (in order)
550
+ // during the stream by `streamChatResponse`. Just close the stream.
551
+ await broadcastStreamComplete(conversationId, emitOptions.aiMessageId);
552
+
553
+ // `generate_document` is an inline tool that only QUEUES into `proposals.docs`
554
+ // during the stream. OpenAI Code Interpreter renders files here, after the
555
+ // stream ends, then we link them to this message.
510
556
  try {
511
- const jsonData = response;
512
-
513
-
514
557
  const attachmentIds: string[] = [];
515
- // Generate PDFs if docs are provided
516
- if (jsonData.docs && Array.isArray(jsonData.docs)) {
517
-
518
-
519
- for (let i = 0; i < jsonData.docs.length; i++) {
520
- const doc = jsonData.docs[i];
521
- if (!doc.title || !doc.blocks || !Array.isArray(doc.blocks)) {
522
- logger.error(`Document ${i + 1} is missing title or blocks`);
523
- continue;
524
- }
525
-
526
-
527
- try {
528
- let pdfBytes = await createPdf(doc.blocks as DocumentBlock[]);
529
- if (pdfBytes) {
530
- // Sanitize filename - remove special characters and limit length
531
- const sanitizedTitle = doc.title
532
- .replace(/[^a-zA-Z0-9\s\-_]/g, '')
533
- .replace(/\s+/g, '_')
534
- .substring(0, 50);
535
-
536
- const filename = `${sanitizedTitle}_${v4().substring(0, 8)}.pdf`;
537
- const filePath = `class/generated/${fullLabChat.classId}/${filename}`;
538
-
539
- logger.info(`PDF ${i + 1} generated successfully`, { labChatId, title: doc.title });
540
-
541
- // Upload directly to Google Cloud Storage
542
- const gcsFile = bucket.file(filePath);
543
- await gcsFile.save(Buffer.from(pdfBytes), {
544
- metadata: {
545
- contentType: 'application/pdf',
546
- }
547
- });
548
-
549
- logger.info(`PDF ${i + 1} uploaded successfully`, { labChatId, filename });
550
-
551
- const file = await prisma.file.create({
552
- data: {
553
- name: filename,
554
- path: filePath,
555
- type: 'application/pdf',
556
- size: pdfBytes.length,
557
- userId: fullLabChat.createdById,
558
- uploadStatus: 'COMPLETED',
559
- uploadedAt: new Date(),
560
- },
561
- });
562
- attachmentIds.push(file.id);
563
- } else {
564
- logger.error(`PDF ${i + 1} creation returned undefined/null`, { labChatId, title: doc.title });
565
- }
566
- } catch (pdfError) {
567
- logger.error(`PDF creation threw an error for document ${i + 1}:`, {
568
- error: pdfError instanceof Error ? {
569
- message: pdfError.message,
570
- stack: pdfError.stack,
571
- name: pdfError.name
572
- } : pdfError,
558
+ const generatedDocFiles: GeneratedDocFileLink[] = [];
559
+ const resolvedDocs: Array<{ title: string; format: string }> = [];
560
+
561
+ for (let i = 0; i < proposals.docs.length; i++) {
562
+ const doc = proposals.docs[i];
563
+ if (!doc.title || !doc.content?.trim()) {
564
+ logger.error(`Document ${i + 1} is missing title or content`, { labChatId, doc });
565
+ continue;
566
+ }
567
+
568
+ const format = inferDocumentFormat(
569
+ doc.title,
570
+ doc.content,
571
+ doc.format,
572
+ teacherMessage,
573
+ );
574
+ resolvedDocs.push({ title: doc.title, format });
575
+
576
+ if (format !== normalizeDocumentFormat(doc.format)) {
577
+ logger.info('Document format overridden from model default', {
578
+ labChatId,
579
+ title: doc.title,
580
+ modelFormat: doc.format,
581
+ resolvedFormat: format,
582
+ });
583
+ }
584
+
585
+ try {
586
+ const generated = await generateDocumentWithOpenAI({
587
+ title: doc.title,
588
+ content: doc.content,
589
+ format,
590
+ });
591
+ if (!generated) {
592
+ logger.error(`Document ${i + 1} generation returned null`, {
573
593
  labChatId,
574
- title: doc.title
594
+ title: doc.title,
595
+ format,
575
596
  });
597
+ continue;
576
598
  }
599
+
600
+ const sanitizedTitle = doc.title
601
+ .replace(/[^a-zA-Z0-9\s\-_]/g, '')
602
+ .replace(/\s+/g, '_')
603
+ .substring(0, 50);
604
+ const filename = `${sanitizedTitle}_${v4().substring(0, 8)}.${generated.format}`;
605
+ const filePath = `class/generated/${fullLabChat.classId}/${filename}`;
606
+
607
+ logger.info(`Document ${i + 1} generated successfully`, {
608
+ labChatId,
609
+ title: doc.title,
610
+ format: generated.format,
611
+ });
612
+
613
+ await bucket.file(filePath).save(generated.bytes, {
614
+ metadata: { contentType: generated.mimeType },
615
+ });
616
+
617
+ logger.info(`Document ${i + 1} uploaded successfully`, { labChatId, filename });
618
+
619
+ const file = await prisma.file.create({
620
+ data: {
621
+ name: filename,
622
+ path: filePath,
623
+ type: generated.mimeType,
624
+ size: generated.bytes.length,
625
+ userId: fullLabChat.createdById,
626
+ conversationId,
627
+ messageId: emitOptions.aiMessageId,
628
+ uploadStatus: 'COMPLETED',
629
+ uploadedAt: new Date(),
630
+ },
631
+ });
632
+
633
+ attachmentIds.push(file.id);
634
+ generatedDocFiles.push({
635
+ title: doc.title,
636
+ fileId: file.id,
637
+ fileName: filename,
638
+ format: generated.format,
639
+ });
640
+ } catch (docError) {
641
+ logger.error(`Document generation failed for item ${i + 1}:`, {
642
+ error: docError instanceof Error ? {
643
+ message: docError.message,
644
+ stack: docError.stack,
645
+ name: docError.name,
646
+ } : docError,
647
+ labChatId,
648
+ title: doc.title,
649
+ format: doc.format,
650
+ });
577
651
  }
578
652
  }
579
-
580
- await completePendingAIMessage(emitOptions.aiMessageId, jsonData.text, {
581
- attachments: {
582
- connect: attachmentIds.map(id => ({ id })),
583
- },
653
+
654
+ const streamParts: StreamPart[] = finalizeDeferredToolParts(
655
+ enrichStreamPartsWithGeneratedFiles(
656
+ applyResolvedFormatsToStreamParts(rawStreamParts, resolvedDocs),
657
+ generatedDocFiles,
658
+ ),
659
+ );
660
+
661
+ // While the message is still PENDING, resolve deferred tool indicators
662
+ // (spinner → attachment or error) before the final persist event.
663
+ for (const part of streamParts) {
664
+ if (part.type !== 'tool' || part.toolName !== 'generate_document') {
665
+ continue;
666
+ }
667
+ if (part.status === 'executing') continue;
668
+ await pusher
669
+ .trigger(chatChannel(conversationId), 'message-tool-call', {
670
+ messageId: emitOptions.aiMessageId,
671
+ toolCallId: part.toolCallId,
672
+ toolName: part.toolName,
673
+ ...(part.title ? { title: part.title } : {}),
674
+ ...(part.format ? { format: part.format } : {}),
675
+ ...(part.fileId ? { fileId: part.fileId, fileName: part.fileName } : {}),
676
+ status: part.status,
677
+ })
678
+ .catch(() => {});
679
+ }
680
+
681
+ await completePendingAIMessage(emitOptions.aiMessageId, streamedText, {
682
+ ...(attachmentIds.length > 0 && {
683
+ attachments: { connect: attachmentIds.map((id) => ({ id })) },
684
+ }),
584
685
  meta: {
585
- assignmentsToCreate: jsonData.assignmentsToCreate?.map(assignment => ({
586
- ...assignment,
587
- id: v4(),
588
- })) || null,
589
- worksheetsToCreate: jsonData.worksheetsToCreate?.map(worksheet => ({
590
- ...worksheet,
591
- id: v4(),
592
- })) || null,
593
- sectionsToCreate: jsonData.sectionsToCreate?.map(section => ({
594
- ...section,
595
- id: v4(),
596
- })) || null,
686
+ assignmentsToCreate: proposals.assignments.length > 0
687
+ ? proposals.assignments.map(assignment => ({ ...assignment, id: v4() }))
688
+ : null,
689
+ worksheetsToCreate: proposals.worksheets.length > 0
690
+ ? proposals.worksheets.map(worksheet => ({ ...worksheet, id: v4() }))
691
+ : null,
692
+ sectionsToCreate: proposals.sections.length > 0
693
+ ? proposals.sections.map(section => ({ ...section, id: v4() }))
694
+ : null,
695
+ streamParts: streamParts.length > 0 ? streamParts : null,
597
696
  },
598
697
  });
599
- } catch (parseError) {
600
- logger.error('Failed to parse AI response or generate PDF:', { error: parseError, labChatId });
698
+ } catch (persistError) {
699
+ logger.error('Failed to persist AI response or generate PDF:', { error: persistError, labChatId });
601
700
  // Fallback: complete the placeholder with the raw text so the
602
701
  // teacher still sees something useful instead of FAILED.
603
- await completePendingAIMessage(emitOptions.aiMessageId, response.text);
702
+ await completePendingAIMessage(emitOptions.aiMessageId, streamedText);
604
703
  }
605
704
 
606
705
  try {
@@ -1,75 +1,65 @@
1
1
  import z from "zod";
2
2
 
3
- export const labChatResponseSchema = z.object({
4
- text: z.string(),
5
- worksheetsToCreate: z.array(z.object({
6
- title: z.string(),
7
- questions: z.array(z.object({
8
- type: z.enum(["MULTIPLE_CHOICE", "TRUE_FALSE", "SHORT_ANSWER", "LONG_ANSWER", "MATH_EXPRESSION", "ESSAY"]),
9
- question: z.string(),
10
- answer: z.string(),
11
- options: z.array(z.object({
12
- id: z.string(),
13
- text: z.string(),
14
- isCorrect: z.boolean(),
15
- })).optional().default([]),
16
- markScheme: z.array(z.object({
17
- id: z.string(),
18
- points: z.number(),
19
- description: z.string(),
20
- })).optional().default([]),
21
- points: z.number().optional().default(0),
22
- order: z.number(),
23
- })),
24
- })).default([]),
25
- sectionsToCreate: z.array(z.object({
26
- name: z.string(),
27
- color: z.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/).nullable().optional(),
28
- })).default([]),
29
- assignmentsToCreate: z.array(z.object({
30
- title: z.string(),
31
- instructions: z.string(),
32
- dueDate: z.string().datetime(),
33
- acceptFiles: z.boolean(),
34
- acceptExtendedResponse: z.boolean(),
35
- acceptWorksheet: z.boolean(),
36
- maxGrade: z.number(),
37
- gradingBoundaryId: z.string().nullable().optional(),
38
- markSchemeId: z.string().nullable().optional(),
39
- worksheetIds: z.array(z.string()),
40
- studentIds: z.array(z.string()),
41
- sectionId: z.string().nullable().optional(),
42
- type: z.enum(["HOMEWORK", "QUIZ", "TEST", "PROJECT", "ESSAY", "DISCUSSION", "PRESENTATION", "LAB", "OTHER"]),
43
- attachments: z.array(z.object({
44
- id: z.string(),
45
- })),
46
- })).nullable().optional(),
47
- docs: z.array(z.object({
48
- title: z.string(),
49
- blocks: z.array(z.object({
50
- format: z.number().int().min(0).max(12),
51
- content: z.union([z.string(), z.array(z.string())]),
52
- metadata: z.object({
53
- fontSize: z.number().min(6).nullable().optional(),
54
- lineHeight: z.number().min(0.6).nullable().optional(),
55
- paragraphSpacing: z.number().min(0).nullable().optional(),
56
- indentWidth: z.number().min(0).nullable().optional(),
57
- paddingX: z.number().min(0).nullable().optional(),
58
- paddingY: z.number().min(0).nullable().optional(),
59
- font: z.number().int().min(0).max(5).nullable().optional(),
60
- color: z.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/).nullable().optional(),
61
- background: z.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/).nullable().optional(),
62
- align: z.enum(["left", "center", "right"]).nullable().optional(),
63
- }).nullable().optional(),
64
- })),
65
- })).nullable().optional(),
3
+ export const worksheetQuestionSchema = z.object({
4
+ type: z.enum(["MULTIPLE_CHOICE", "TRUE_FALSE", "SHORT_ANSWER", "LONG_ANSWER", "MATH_EXPRESSION", "ESSAY"]),
5
+ question: z.string(),
6
+ answer: z.string(),
7
+ options: z.array(z.object({
8
+ id: z.string(),
9
+ text: z.string(),
10
+ isCorrect: z.boolean(),
11
+ })).optional().default([]),
12
+ markScheme: z.array(z.object({
13
+ id: z.string(),
14
+ points: z.number(),
15
+ description: z.string(),
16
+ })).optional().default([]),
17
+ points: z.number().optional().default(0),
18
+ order: z.number(),
66
19
  });
67
20
 
68
- export type LabChatResponse = z.infer<typeof labChatResponseSchema>;
21
+ export const worksheetToCreateSchema = z.object({
22
+ title: z.string(),
23
+ questions: z.array(worksheetQuestionSchema),
24
+ });
25
+
26
+ export const sectionToCreateSchema = z.object({
27
+ name: z.string(),
28
+ color: z.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/).nullable().optional(),
29
+ });
69
30
 
70
- export const labChatResponseFormat = `{ "text": string, "docs": null | array, "worksheetsToCreate": array, "sectionsToCreate": array, "assignmentsToCreate": null | array }`;
31
+ export const assignmentToCreateSchema = z.object({
32
+ title: z.string(),
33
+ instructions: z.string(),
34
+ dueDate: z.string().datetime(),
35
+ acceptFiles: z.boolean(),
36
+ acceptExtendedResponse: z.boolean(),
37
+ acceptWorksheet: z.boolean(),
38
+ maxGrade: z.number(),
39
+ gradingBoundaryId: z.string().nullable().optional(),
40
+ markSchemeId: z.string().nullable().optional(),
41
+ worksheetIds: z.array(z.string()),
42
+ studentIds: z.array(z.string()),
43
+ sectionId: z.string().nullable().optional(),
44
+ type: z.enum(["HOMEWORK", "QUIZ", "TEST", "PROJECT", "ESSAY", "DISCUSSION", "PRESENTATION", "LAB", "OTHER"]),
45
+ attachments: z.array(z.object({
46
+ id: z.string(),
47
+ })),
48
+ });
49
+
50
+ export const documentFormatSchema = z.enum([
51
+ 'pdf', 'docx', 'doc', 'xlsx', 'pptx', 'csv', 'md', 'html', 'txt', 'json',
52
+ 'xml', 'tex', 'png', 'jpeg', 'jpg', 'gif', 'zip', 'tar', 'py', 'js', 'ts',
53
+ 'css', 'sh',
54
+ ]);
55
+
56
+ export const docToCreateSchema = z.object({
57
+ title: z.string(),
58
+ content: z.string().min(1),
59
+ format: documentFormatSchema.optional().default('pdf'),
60
+ });
71
61
 
72
- export const labChatArrayFieldInstructions = [
73
- `- "worksheetsToCreate": always output an array. Use [] when there are no worksheets to create.`,
74
- `- "sectionsToCreate": always output an array. Use [] when there are no sections to create.`,
75
- ].join("\n");
62
+ export type WorksheetToCreate = z.infer<typeof worksheetToCreateSchema>;
63
+ export type SectionToCreate = z.infer<typeof sectionToCreateSchema>;
64
+ export type AssignmentToCreate = z.infer<typeof assignmentToCreateSchema>;
65
+ export type DocToCreate = z.infer<typeof docToCreateSchema>;