@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
@@ -0,0 +1,446 @@
1
+ /**
2
+ * Embedding service – pgvector-backed RAG for class materials.
3
+ * Handles text chunking, embedding generation, storage, and semantic search.
4
+ */
5
+ import { prisma } from '../lib/prisma.js';
6
+ import { openAIClient } from '../utils/inference.js';
7
+ import { logger } from '../utils/logger.js';
8
+ import { enqueueRunJob } from '../workers/queues.js';
9
+ import type { EmbeddingSourceType } from '@prisma/client';
10
+
11
+ const EMBEDDING_MODEL = 'text-embedding-3-small';
12
+ const EMBEDDING_DIMENSIONS = 1536;
13
+ const CHUNK_SIZE = 512; // tokens (~2048 chars)
14
+ const CHUNK_OVERLAP = 100; // tokens (~400 chars)
15
+ const CHARS_PER_TOKEN = 4;
16
+
17
+ export interface EmbeddingSearchResult {
18
+ id: string;
19
+ sourceType: EmbeddingSourceType;
20
+ sourceId: string;
21
+ content: string;
22
+ metadata: Record<string, unknown>;
23
+ similarity: number;
24
+ }
25
+
26
+ /**
27
+ * Split text into overlapping chunks for embedding.
28
+ */
29
+ export function chunkText(text: string, chunkSize = CHUNK_SIZE, overlap = CHUNK_OVERLAP): string[] {
30
+ const maxChars = chunkSize * CHARS_PER_TOKEN;
31
+ const overlapChars = overlap * CHARS_PER_TOKEN;
32
+
33
+ if (text.length <= maxChars) {
34
+ return [text];
35
+ }
36
+
37
+ const chunks: string[] = [];
38
+ let start = 0;
39
+
40
+ while (start < text.length) {
41
+ const end = Math.min(start + maxChars, text.length);
42
+ chunks.push(text.slice(start, end));
43
+
44
+ if (end >= text.length) break;
45
+ start = end - overlapChars;
46
+ }
47
+
48
+ return chunks;
49
+ }
50
+
51
+ /**
52
+ * Generate embedding vector for a text string.
53
+ */
54
+ export async function generateEmbedding(text: string): Promise<number[]> {
55
+ const response = await openAIClient.embeddings.create({
56
+ model: EMBEDDING_MODEL,
57
+ input: text,
58
+ dimensions: EMBEDDING_DIMENSIONS,
59
+ });
60
+
61
+ return response.data[0].embedding;
62
+ }
63
+
64
+ /**
65
+ * Generate embeddings for multiple texts in a single batch.
66
+ */
67
+ export async function generateEmbeddings(texts: string[]): Promise<number[][]> {
68
+ if (texts.length === 0) return [];
69
+
70
+ const response = await openAIClient.embeddings.create({
71
+ model: EMBEDDING_MODEL,
72
+ input: texts,
73
+ dimensions: EMBEDDING_DIMENSIONS,
74
+ });
75
+
76
+ return response.data
77
+ .sort((a, b) => a.index - b.index)
78
+ .map((item) => item.embedding);
79
+ }
80
+
81
+ /**
82
+ * Store embeddings for a source document. Replaces any existing embeddings for
83
+ * the same sourceType + sourceId combination.
84
+ */
85
+ export async function indexContent(params: {
86
+ classId: string;
87
+ sourceType: EmbeddingSourceType;
88
+ sourceId: string;
89
+ text: string;
90
+ metadata?: Record<string, unknown>;
91
+ }): Promise<number> {
92
+ const { classId, sourceType, sourceId, text, metadata = {} } = params;
93
+
94
+ // Remove existing embeddings for this source
95
+ await prisma.embedding.deleteMany({
96
+ where: { classId, sourceType, sourceId },
97
+ });
98
+
99
+ const chunks = chunkText(text);
100
+ if (chunks.length === 0) return 0;
101
+
102
+ const embeddings = await generateEmbeddings(chunks);
103
+
104
+ // Insert using raw SQL for pgvector support
105
+ for (let i = 0; i < chunks.length; i++) {
106
+ const vectorStr = `[${embeddings[i].join(',')}]`;
107
+ const chunkMetadata = { ...metadata, chunkIndex: i, totalChunks: chunks.length };
108
+
109
+ await prisma.$executeRaw`
110
+ INSERT INTO "Embedding" (id, "classId", "sourceType", "sourceId", content, vector, metadata, "createdAt")
111
+ VALUES (
112
+ gen_random_uuid(),
113
+ ${classId},
114
+ ${sourceType}::"EmbeddingSourceType",
115
+ ${sourceId},
116
+ ${chunks[i]},
117
+ ${vectorStr}::vector,
118
+ ${JSON.stringify(chunkMetadata)}::jsonb,
119
+ NOW()
120
+ )
121
+ `;
122
+ }
123
+
124
+ logger.info('Content indexed', { classId, sourceType, sourceId, chunks: chunks.length });
125
+ return chunks.length;
126
+ }
127
+
128
+ /**
129
+ * Semantic search over class embeddings using cosine similarity.
130
+ */
131
+ export async function searchEmbeddings(params: {
132
+ classId: string;
133
+ query: string;
134
+ limit?: number;
135
+ sourceTypes?: EmbeddingSourceType[];
136
+ }): Promise<EmbeddingSearchResult[]> {
137
+ const { classId, query, limit = 5, sourceTypes } = params;
138
+
139
+ const queryEmbedding = await generateEmbedding(query);
140
+ const vectorStr = `[${queryEmbedding.join(',')}]`;
141
+
142
+ let results: EmbeddingSearchResult[];
143
+
144
+ if (sourceTypes && sourceTypes.length > 0) {
145
+ results = await prisma.$queryRaw<EmbeddingSearchResult[]>`
146
+ SELECT
147
+ id,
148
+ "sourceType",
149
+ "sourceId",
150
+ content,
151
+ metadata,
152
+ 1 - (vector <=> ${vectorStr}::vector) as similarity
153
+ FROM "Embedding"
154
+ WHERE "classId" = ${classId}
155
+ AND "sourceType" = ANY(${sourceTypes}::"EmbeddingSourceType"[])
156
+ ORDER BY vector <=> ${vectorStr}::vector
157
+ LIMIT ${limit}
158
+ `;
159
+ } else {
160
+ results = await prisma.$queryRaw<EmbeddingSearchResult[]>`
161
+ SELECT
162
+ id,
163
+ "sourceType",
164
+ "sourceId",
165
+ content,
166
+ metadata,
167
+ 1 - (vector <=> ${vectorStr}::vector) as similarity
168
+ FROM "Embedding"
169
+ WHERE "classId" = ${classId}
170
+ ORDER BY vector <=> ${vectorStr}::vector
171
+ LIMIT ${limit}
172
+ `;
173
+ }
174
+
175
+ return results;
176
+ }
177
+
178
+ /**
179
+ * Remove all embeddings for a given source.
180
+ */
181
+ export async function removeEmbeddings(params: {
182
+ classId: string;
183
+ sourceType: EmbeddingSourceType;
184
+ sourceId: string;
185
+ }): Promise<void> {
186
+ await prisma.embedding.deleteMany({
187
+ where: {
188
+ classId: params.classId,
189
+ sourceType: params.sourceType,
190
+ sourceId: params.sourceId,
191
+ },
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Index an assignment (title + instructions + attached file names) into the vector store.
197
+ */
198
+ export async function indexAssignment(classId: string, assignmentId: string): Promise<void> {
199
+ const assignment = await prisma.assignment.findUnique({
200
+ where: { id: assignmentId },
201
+ select: {
202
+ title: true,
203
+ instructions: true,
204
+ type: true,
205
+ attachments: { select: { name: true, type: true } },
206
+ },
207
+ });
208
+
209
+ if (!assignment) return;
210
+
211
+ const parts = [
212
+ `Title: ${assignment.title}`,
213
+ `Type: ${assignment.type}`,
214
+ assignment.instructions ? `Instructions: ${assignment.instructions}` : '',
215
+ ];
216
+
217
+ if (assignment.attachments.length > 0) {
218
+ const fileList = assignment.attachments.map((f) => `${f.name} (${f.type})`).join(', ');
219
+ parts.push(`Attached files: ${fileList}`);
220
+ }
221
+
222
+ await indexContent({
223
+ classId,
224
+ sourceType: 'ASSIGNMENT',
225
+ sourceId: assignmentId,
226
+ text: parts.filter(Boolean).join('\n'),
227
+ metadata: { title: assignment.title, type: assignment.type, fileCount: assignment.attachments.length },
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Index a worksheet (questions + answers) into the vector store.
233
+ */
234
+ export async function indexWorksheet(classId: string, worksheetId: string): Promise<void> {
235
+ const worksheet = await prisma.worksheet.findUnique({
236
+ where: { id: worksheetId },
237
+ include: { questions: true },
238
+ });
239
+
240
+ if (!worksheet) return;
241
+
242
+ const questionTexts = worksheet.questions.map((q, i) =>
243
+ `Question ${i + 1} (${q.type}): ${q.question}\nAnswer: ${q.answer}`
244
+ );
245
+
246
+ const text = [
247
+ `Worksheet: ${worksheet.name}`,
248
+ ...questionTexts,
249
+ ].join('\n\n');
250
+
251
+ await indexContent({
252
+ classId,
253
+ sourceType: 'WORKSHEET',
254
+ sourceId: worksheetId,
255
+ text,
256
+ metadata: { name: worksheet.name, questionCount: worksheet.questions.length },
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Index an announcement into the vector store.
262
+ */
263
+ export async function indexAnnouncement(classId: string, announcementId: string): Promise<void> {
264
+ const announcement = await prisma.announcement.findUnique({
265
+ where: { id: announcementId },
266
+ select: { remarks: true, teacherId: true },
267
+ });
268
+
269
+ if (!announcement || !announcement.remarks) return;
270
+
271
+ const text = `Announcement: ${announcement.remarks}`;
272
+
273
+ await indexContent({
274
+ classId,
275
+ sourceType: 'ANNOUNCEMENT',
276
+ sourceId: announcementId,
277
+ text,
278
+ metadata: { teacherId: announcement.teacherId },
279
+ });
280
+ }
281
+
282
+ /**
283
+ * Index a discussion (title + body + replies) into the vector store.
284
+ */
285
+ export async function indexDiscussion(classId: string, discussionId: string): Promise<void> {
286
+ const discussion = await prisma.discussion.findUnique({
287
+ where: { id: discussionId },
288
+ select: {
289
+ title: true,
290
+ body: true,
291
+ type: true,
292
+ author: { select: { username: true, profile: { select: { displayName: true } } } },
293
+ replies: {
294
+ select: { body: true, author: { select: { username: true, profile: { select: { displayName: true } } } } },
295
+ orderBy: { createdAt: 'asc' },
296
+ take: 20,
297
+ },
298
+ },
299
+ });
300
+
301
+ if (!discussion) return;
302
+
303
+ const authorName = discussion.author.profile?.displayName || discussion.author.username;
304
+ const parts = [
305
+ `Discussion: ${discussion.title}`,
306
+ `Type: ${discussion.type}`,
307
+ `Author: ${authorName}`,
308
+ `Body: ${discussion.body}`,
309
+ ];
310
+
311
+ if (discussion.replies.length > 0) {
312
+ parts.push('Replies:');
313
+ for (const reply of discussion.replies) {
314
+ const replyAuthor = reply.author.profile?.displayName || reply.author.username;
315
+ parts.push(` ${replyAuthor}: ${reply.body.slice(0, 300)}`);
316
+ }
317
+ }
318
+
319
+ await indexContent({
320
+ classId,
321
+ sourceType: 'DISCUSSION',
322
+ sourceId: discussionId,
323
+ text: parts.join('\n'),
324
+ metadata: { title: discussion.title, type: discussion.type, replyCount: discussion.replies.length },
325
+ });
326
+ }
327
+
328
+ /**
329
+ * Index a submission (grade + feedback + file attachments + extended response) into the vector store.
330
+ */
331
+ export async function indexSubmission(classId: string, submissionId: string): Promise<void> {
332
+ const submission = await prisma.submission.findUnique({
333
+ where: { id: submissionId },
334
+ select: {
335
+ gradeReceived: true,
336
+ teacherComments: true,
337
+ extendedResponse: true,
338
+ returned: true,
339
+ student: { select: { username: true, profile: { select: { displayName: true } } } },
340
+ assignment: { select: { title: true, maxGrade: true, type: true } },
341
+ attachments: { select: { name: true, type: true } },
342
+ annotations: { select: { name: true, type: true } },
343
+ },
344
+ });
345
+
346
+ if (!submission) return;
347
+
348
+ const studentName = submission.student.profile?.displayName || submission.student.username;
349
+ const parts = [
350
+ `Submission for: ${submission.assignment.title}`,
351
+ `Student: ${studentName}`,
352
+ `Assignment type: ${submission.assignment.type}`,
353
+ ];
354
+
355
+ if (submission.gradeReceived != null) {
356
+ parts.push(`Grade: ${submission.gradeReceived}/${submission.assignment.maxGrade ?? '?'}`);
357
+ }
358
+ if (submission.teacherComments) {
359
+ parts.push(`Teacher feedback: ${submission.teacherComments}`);
360
+ }
361
+ if (submission.extendedResponse) {
362
+ parts.push(`Student response: ${submission.extendedResponse.slice(0, 500)}`);
363
+ }
364
+ if (submission.attachments.length > 0) {
365
+ parts.push(`Submitted files: ${submission.attachments.map((f) => f.name).join(', ')}`);
366
+ }
367
+ if (submission.annotations.length > 0) {
368
+ parts.push(`Teacher annotations: ${submission.annotations.map((f) => f.name).join(', ')}`);
369
+ }
370
+
371
+ await indexContent({
372
+ classId,
373
+ sourceType: 'SUBMISSION',
374
+ sourceId: submissionId,
375
+ text: parts.join('\n'),
376
+ metadata: { studentName, assignmentTitle: submission.assignment.title, grade: submission.gradeReceived },
377
+ });
378
+ }
379
+
380
+ /**
381
+ * Index a class syllabus into the vector store.
382
+ * Uses classId as both classId and sourceId since syllabus is a field on the class.
383
+ */
384
+ export async function indexSyllabus(classId: string): Promise<void> {
385
+ const cls = await prisma.class.findUnique({
386
+ where: { id: classId },
387
+ select: { name: true, subject: true, syllabus: true },
388
+ });
389
+
390
+ if (!cls?.syllabus) return;
391
+
392
+ const text = [
393
+ `Syllabus for ${cls.name} (${cls.subject})`,
394
+ cls.syllabus,
395
+ ].join('\n\n');
396
+
397
+ await indexContent({
398
+ classId,
399
+ sourceType: 'SYLLABUS',
400
+ sourceId: classId,
401
+ text,
402
+ metadata: { className: cls.name, subject: cls.subject },
403
+ });
404
+ }
405
+
406
+ /**
407
+ * Index a file (name + type + path) into the vector store.
408
+ * For text-based content, PDF extraction would be added here in the future.
409
+ */
410
+ export async function indexFile(classId: string, fileId: string): Promise<void> {
411
+ const file = await prisma.file.findUnique({
412
+ where: { id: fileId },
413
+ select: { name: true, path: true, type: true, size: true },
414
+ });
415
+
416
+ if (!file) return;
417
+
418
+ const text = [
419
+ `File: ${file.name}`,
420
+ `Type: ${file.type}`,
421
+ `Path: ${file.path}`,
422
+ ].join('\n');
423
+
424
+ await indexContent({
425
+ classId,
426
+ sourceType: 'FILE',
427
+ sourceId: fileId,
428
+ text,
429
+ metadata: { name: file.name, type: file.type, size: file.size },
430
+ });
431
+ }
432
+
433
+ /**
434
+ * Fire-and-forget: enqueue a background job to embed content.
435
+ * Safe to call from service functions after a create/update.
436
+ */
437
+ export function enqueueEmbedding(
438
+ classId: string,
439
+ sourceType: EmbeddingSourceType,
440
+ sourceId: string,
441
+ ): void {
442
+ enqueueRunJob('EMBED_CONTENT', { classId, sourceType: sourceType as string, sourceId }, { classId })
443
+ .catch((err) => {
444
+ logger.error('Failed to enqueue embedding job', { err, classId, sourceType, sourceId });
445
+ });
446
+ }
@@ -28,6 +28,7 @@ import {
28
28
  type DirectFileData,
29
29
  confirmDirectUpload,
30
30
  } from "../lib/fileUpload.js";
31
+ import { enqueueEmbedding } from "./embedding.js";
31
32
  import {
32
33
  type AccessLevel,
33
34
  getScopeDefault,
@@ -421,9 +422,39 @@ export async function confirmFolderUpload(
421
422
  input.uploadSuccess,
422
423
  input.errorMessage
423
424
  );
425
+
426
+ // Index file for RAG search if it belongs to a class folder
427
+ if (input.uploadSuccess) {
428
+ const file = await prisma.file.findUnique({
429
+ where: { id: input.fileId },
430
+ select: { folderId: true },
431
+ });
432
+ if (file?.folderId) {
433
+ const classId = await resolveFileClassId(file.folderId);
434
+ if (classId) {
435
+ enqueueEmbedding(classId, 'FILE', input.fileId);
436
+ }
437
+ }
438
+ }
439
+
424
440
  return { success: true };
425
441
  }
426
442
 
443
+ /** Walk up folder tree to find classId, if any. */
444
+ async function resolveFileClassId(folderId: string): Promise<string | null> {
445
+ let currentId: string | null = folderId;
446
+ while (currentId) {
447
+ const folder = await prisma.folder.findUnique({
448
+ where: { id: currentId },
449
+ select: { classId: true, parentFolderId: true },
450
+ });
451
+ if (!folder) return null;
452
+ if (folder.classId) return folder.classId;
453
+ currentId = folder.parentFolderId;
454
+ }
455
+ return null;
456
+ }
457
+
427
458
  /** What scopes can this user pick from in cloud-storage UIs? */
428
459
  export async function listUserScopes(userId: string) {
429
460
  const memberships = await prisma.schoolMembership.findMany({
@@ -4,7 +4,7 @@
4
4
  import { v4 } from "uuid";
5
5
  import {
6
6
  createSchoolDevelopmentProgram,
7
- findSchoolDevelopmentProgramById,
7
+ findSchoolDevelopmentProgramStatusById,
8
8
  createEarlyAccessRequest,
9
9
  } from "../models/marketing.js";
10
10
 
@@ -42,9 +42,9 @@ export async function createSchoolDevelopmentProgramRecord(input: {
42
42
  return { id: program.id };
43
43
  }
44
44
 
45
- /** Look up a school development program by ID. */
45
+ /** Public application status lookup (no contact PII). */
46
46
  export async function searchSchoolDevelopmentPrograms(id: string) {
47
- return findSchoolDevelopmentProgramById(id);
47
+ return findSchoolDevelopmentProgramStatusById(id);
48
48
  }
49
49
 
50
50
  /** Create an early access request. */
@@ -2,7 +2,7 @@
2
2
  * Message service – send, update, delete messages; mark as read; track mentions.
3
3
  * Broadcasts real-time updates via Pusher.
4
4
  */
5
- import type { Prisma } from "@prisma/client";
5
+ import type { AppTransactionClient } from "../lib/prisma.js";
6
6
  import { TRPCError } from "@trpc/server";
7
7
  import { prisma } from "../lib/prisma.js";
8
8
  import { chatChannel, pusher } from "../lib/pusher.js";
@@ -35,7 +35,7 @@ export function ensureMessageHasContentOrAttachments(
35
35
  }
36
36
 
37
37
  export async function claimMessageAttachments(
38
- tx: Prisma.TransactionClient,
38
+ tx: AppTransactionClient,
39
39
  userId: string,
40
40
  conversationId: string,
41
41
  messageId: string,