@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,10 +1,12 @@
1
1
  import type { DiscussionType, Prisma, ReactionType } from "@prisma/client";
2
+ import type { AppTransactionClient } from "../lib/prisma.js";
2
3
  import { TRPCError } from "@trpc/server";
3
4
  import { prisma } from "../lib/prisma.js";
4
5
  import { discussionChannel, pusher } from "../lib/pusher.js";
5
6
  import { logger } from "../utils/logger.js";
6
7
  import { classMemberOrSchoolAdminWhere, classTeacherOrSchoolAdminWhere } from "../models/access.js";
7
8
  import { sendNotification } from "./notification.js";
9
+ import { enqueueEmbedding } from "./embedding.js";
8
10
 
9
11
  const authorSelect = {
10
12
  id: true,
@@ -225,7 +227,7 @@ async function getDiscussionForUser(discussionId: string, userId: string) {
225
227
  }
226
228
 
227
229
  async function claimDiscussionAttachments(
228
- tx: Prisma.TransactionClient,
230
+ tx: AppTransactionClient,
229
231
  userId: string,
230
232
  discussionId: string,
231
233
  attachmentIds: string[] = []
@@ -242,7 +244,7 @@ async function claimDiscussionAttachments(
242
244
  }
243
245
 
244
246
  async function claimReplyAttachments(
245
- tx: Prisma.TransactionClient,
247
+ tx: AppTransactionClient,
246
248
  userId: string,
247
249
  replyId: string,
248
250
  attachmentIds: string[] = []
@@ -440,6 +442,10 @@ export async function createDiscussion(userId: string, input: {
440
442
 
441
443
  triggerDiscussionEvent(input.classId, "discussion-created", { discussion: serializeDiscussion(discussion, userId) });
442
444
  await notifyMentionedUsers(input.classId, userId, input.body, `/class/${input.classId}/discussions/${discussion.id}`);
445
+
446
+ // Auto-index discussion for RAG search
447
+ enqueueEmbedding(input.classId, 'DISCUSSION', discussion.id);
448
+
443
449
  return { discussion: serializeDiscussion(discussion, userId) };
444
450
  }
445
451
 
@@ -470,6 +476,10 @@ export async function updateDiscussion(userId: string, input: {
470
476
  include: discussionInclude,
471
477
  });
472
478
  triggerDiscussionEvent(updated.classId, "discussion-updated", { discussion: serializeDiscussion(updated, userId) });
479
+
480
+ // Re-index discussion for RAG search after update
481
+ enqueueEmbedding(updated.classId, 'DISCUSSION', updated.id);
482
+
473
483
  return { discussion: serializeDiscussion(updated, userId) };
474
484
  }
475
485
 
@@ -525,6 +535,10 @@ export async function replyToDiscussion(userId: string, input: {
525
535
  notifyMentionedUsers(discussion.classId, userId, input.body, link),
526
536
  ]);
527
537
  triggerDiscussionEvent(discussion.classId, "reply-created", { discussionId: discussion.id, reply: serializeReply(reply, userId) });
538
+
539
+ // Re-index discussion for RAG search (includes new reply)
540
+ enqueueEmbedding(discussion.classId, 'DISCUSSION', discussion.id);
541
+
528
542
  return { reply: serializeReply(reply, userId) };
529
543
  }
530
544
 
@@ -674,28 +688,28 @@ export async function toggleDiscussionReaction(userId: string, input: { discussi
674
688
 
675
689
  export async function listDiscussionTags(userId: string, classId: string) {
676
690
  await getDiscussionClassAccess(classId, userId);
677
- return prisma.discussionTag.findMany({ where: { classId }, orderBy: { name: "asc" } });
691
+ return prisma.classTag.findMany({ where: { classId }, orderBy: { name: "asc" } });
678
692
  }
679
693
 
680
694
  export async function createDiscussionTag(userId: string, input: { classId: string; name: string; color: string }) {
681
695
  const isTeacher = await prisma.class.findFirst({ where: { id: input.classId, ...classTeacherOrSchoolAdminWhere(userId) }, select: { id: true, discussionsEnabled: true } });
682
696
  if (!isTeacher?.discussionsEnabled) throw new TRPCError({ code: "FORBIDDEN", message: "Only teachers can manage discussion tags" });
683
- return prisma.discussionTag.create({ data: { classId: input.classId, name: input.name.trim(), color: input.color } });
697
+ return prisma.classTag.create({ data: { classId: input.classId, name: input.name.trim(), color: input.color } });
684
698
  }
685
699
 
686
700
  export async function updateDiscussionTag(userId: string, input: { tagId: string; name?: string; color?: string }) {
687
- const tag = await prisma.discussionTag.findUnique({ where: { id: input.tagId }, select: { classId: true } });
701
+ const tag = await prisma.classTag.findUnique({ where: { id: input.tagId }, select: { classId: true } });
688
702
  if (!tag) throw new TRPCError({ code: "NOT_FOUND", message: "Tag not found" });
689
703
  const isTeacher = await prisma.class.findFirst({ where: { id: tag.classId, ...classTeacherOrSchoolAdminWhere(userId) }, select: { id: true, discussionsEnabled: true } });
690
704
  if (!isTeacher?.discussionsEnabled) throw new TRPCError({ code: "FORBIDDEN", message: "Only teachers can manage discussion tags" });
691
- return prisma.discussionTag.update({ where: { id: input.tagId }, data: { name: input.name?.trim(), color: input.color } });
705
+ return prisma.classTag.update({ where: { id: input.tagId }, data: { name: input.name?.trim(), color: input.color } });
692
706
  }
693
707
 
694
708
  export async function deleteDiscussionTag(userId: string, tagId: string) {
695
- const tag = await prisma.discussionTag.findUnique({ where: { id: tagId }, select: { classId: true } });
709
+ const tag = await prisma.classTag.findUnique({ where: { id: tagId }, select: { classId: true } });
696
710
  if (!tag) throw new TRPCError({ code: "NOT_FOUND", message: "Tag not found" });
697
711
  const isTeacher = await prisma.class.findFirst({ where: { id: tag.classId, ...classTeacherOrSchoolAdminWhere(userId) }, select: { id: true, discussionsEnabled: true } });
698
712
  if (!isTeacher?.discussionsEnabled) throw new TRPCError({ code: "FORBIDDEN", message: "Only teachers can manage discussion tags" });
699
- await prisma.discussionTag.delete({ where: { id: tagId } });
713
+ await prisma.classTag.delete({ where: { id: tagId } });
700
714
  return { success: true };
701
715
  }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * AI-powered discussion summary service with in-memory + Redis caching.
3
+ */
4
+ import { prisma } from "../lib/prisma.js";
5
+ import { getRedis } from "../lib/redis.js";
6
+ import { inference } from "../utils/inference.js";
7
+ import { logger } from "../utils/logger.js";
8
+ import { TRPCError } from "@trpc/server";
9
+ import { z } from "zod";
10
+ import { classMemberOrSchoolAdminWhere } from "../models/access.js";
11
+
12
+ const discussionSummarySchema = z.object({
13
+ summary: z
14
+ .string()
15
+ .describe("A concise 2-4 sentence TL;DR of the entire discussion thread."),
16
+ keyQuestions: z
17
+ .array(z.string())
18
+ .describe("Up to 5 important student questions raised in the thread."),
19
+ unresolvedIssues: z
20
+ .array(z.string())
21
+ .describe(
22
+ "Up to 3 questions or topics that remain unanswered or unresolved.",
23
+ ),
24
+ participantCount: z
25
+ .number()
26
+ .describe("Number of unique participants in the thread."),
27
+ });
28
+
29
+ export type DiscussionSummary = z.infer<typeof discussionSummarySchema>;
30
+
31
+ const REDIS_PREFIX = "discussionSummary:";
32
+ const REDIS_TTL_SECONDS = 60 * 30; // 30 min
33
+
34
+ const memoryCache = new Map<
35
+ string,
36
+ { value: DiscussionSummary; replyCount: number; cachedAt: number }
37
+ >();
38
+ const MEMORY_TTL_MS = 10 * 60 * 1000; // 10 min
39
+
40
+ function memoryCacheKey(discussionId: string) {
41
+ return discussionId;
42
+ }
43
+
44
+ function getMemoryCacheHit(
45
+ discussionId: string,
46
+ currentReplyCount: number,
47
+ ): DiscussionSummary | undefined {
48
+ const entry = memoryCache.get(memoryCacheKey(discussionId));
49
+ if (!entry) return undefined;
50
+ if (Date.now() - entry.cachedAt > MEMORY_TTL_MS) {
51
+ memoryCache.delete(memoryCacheKey(discussionId));
52
+ return undefined;
53
+ }
54
+ if (entry.replyCount !== currentReplyCount) {
55
+ memoryCache.delete(memoryCacheKey(discussionId));
56
+ return undefined;
57
+ }
58
+ return entry.value;
59
+ }
60
+
61
+ function setMemoryCache(
62
+ discussionId: string,
63
+ value: DiscussionSummary,
64
+ replyCount: number,
65
+ ) {
66
+ memoryCache.set(memoryCacheKey(discussionId), {
67
+ value,
68
+ replyCount,
69
+ cachedAt: Date.now(),
70
+ });
71
+ }
72
+
73
+ async function getRedisCacheHit(
74
+ discussionId: string,
75
+ currentReplyCount: number,
76
+ ): Promise<DiscussionSummary | undefined> {
77
+ const redis = getRedis();
78
+ if (!redis) return undefined;
79
+ try {
80
+ const raw = await redis.get(`${REDIS_PREFIX}${discussionId}`);
81
+ if (!raw) return undefined;
82
+ const parsed = JSON.parse(raw) as {
83
+ value: DiscussionSummary;
84
+ replyCount: number;
85
+ };
86
+ if (parsed.replyCount !== currentReplyCount) return undefined;
87
+ return parsed.value;
88
+ } catch {
89
+ return undefined;
90
+ }
91
+ }
92
+
93
+ async function setRedisCache(
94
+ discussionId: string,
95
+ value: DiscussionSummary,
96
+ replyCount: number,
97
+ ) {
98
+ const redis = getRedis();
99
+ if (!redis) return;
100
+ try {
101
+ await redis.set(
102
+ `${REDIS_PREFIX}${discussionId}`,
103
+ JSON.stringify({ value, replyCount }),
104
+ "EX",
105
+ REDIS_TTL_SECONDS,
106
+ );
107
+ } catch (err) {
108
+ logger.error("Failed to cache discussion summary in Redis", { err });
109
+ }
110
+ }
111
+
112
+ export async function getDiscussionSummary(
113
+ userId: string,
114
+ discussionId: string,
115
+ ): Promise<DiscussionSummary> {
116
+ const discussion = await prisma.discussion.findUnique({
117
+ where: { id: discussionId },
118
+ select: {
119
+ id: true,
120
+ classId: true,
121
+ title: true,
122
+ body: true,
123
+ type: true,
124
+ replies: {
125
+ where: { deletedAt: null },
126
+ select: {
127
+ body: true,
128
+ author: {
129
+ select: {
130
+ username: true,
131
+ profile: { select: { displayName: true } },
132
+ },
133
+ },
134
+ createdAt: true,
135
+ },
136
+ orderBy: { createdAt: "asc" },
137
+ },
138
+ author: {
139
+ select: {
140
+ username: true,
141
+ profile: { select: { displayName: true } },
142
+ },
143
+ },
144
+ },
145
+ });
146
+
147
+ if (!discussion) {
148
+ throw new TRPCError({
149
+ code: "NOT_FOUND",
150
+ message: "Discussion not found",
151
+ });
152
+ }
153
+
154
+ // Verify user is a member of the class
155
+ const classMembership = await prisma.class.findFirst({
156
+ where: { id: discussion.classId, ...classMemberOrSchoolAdminWhere(userId) },
157
+ select: { id: true },
158
+ });
159
+ if (!classMembership) {
160
+ throw new TRPCError({
161
+ code: "UNAUTHORIZED",
162
+ message: "You are not a member of this class",
163
+ });
164
+ }
165
+
166
+ const replyCount = discussion.replies.length;
167
+
168
+ // Check memory cache
169
+ const memoryCached = getMemoryCacheHit(discussionId, replyCount);
170
+ if (memoryCached) return memoryCached;
171
+
172
+ // Check Redis cache
173
+ const redisCached = await getRedisCacheHit(discussionId, replyCount);
174
+ if (redisCached) {
175
+ setMemoryCache(discussionId, redisCached, replyCount);
176
+ return redisCached;
177
+ }
178
+
179
+ // Build context for inference
180
+ const authorName =
181
+ discussion.author.profile?.displayName ?? discussion.author.username;
182
+ const threadText = [
183
+ `Title: ${discussion.title}`,
184
+ `Type: ${discussion.type}`,
185
+ `Author: ${authorName}`,
186
+ `Original post:\n${discussion.body}`,
187
+ "",
188
+ ...discussion.replies.map((reply, i) => {
189
+ const name =
190
+ reply.author.profile?.displayName ?? reply.author.username;
191
+ return `Reply ${i + 1} by ${name}:\n${reply.body}`;
192
+ }),
193
+ ].join("\n\n");
194
+
195
+ const messages = [
196
+ {
197
+ role: "system" as const,
198
+ content:
199
+ "You are a helpful assistant summarizing a class discussion thread for a teacher. Be concise, specific, and grounded in the actual content. Do not fabricate information.",
200
+ },
201
+ {
202
+ role: "user" as const,
203
+ content: threadText,
204
+ },
205
+ {
206
+ role: "user" as const,
207
+ content:
208
+ "Generate a structured summary of this discussion thread. Include a TL;DR, key student questions, and any unresolved issues.",
209
+ },
210
+ ];
211
+
212
+ const result = await inference<DiscussionSummary>(
213
+ messages,
214
+ discussionSummarySchema,
215
+ {
216
+ reason: "discussion_summary",
217
+ metadata: { discussionId, classId: discussion.classId },
218
+ },
219
+ );
220
+
221
+ // Cache result
222
+ setMemoryCache(discussionId, result, replyCount);
223
+ await setRedisCache(discussionId, result, replyCount);
224
+
225
+ return result;
226
+ }