@studious-lms/server 1.13.0 → 1.14.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 (306) 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/lib/config/cors.d.ts.map +1 -1
  8. package/dist/lib/config/cors.js +31 -11
  9. package/dist/lib/config/cors.js.map +1 -1
  10. package/dist/lib/config/env.d.ts +17 -0
  11. package/dist/lib/config/env.d.ts.map +1 -1
  12. package/dist/lib/config/env.js +8 -2
  13. package/dist/lib/config/env.js.map +1 -1
  14. package/dist/lib/googleCloudStorage.d.ts +10 -0
  15. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  16. package/dist/lib/googleCloudStorage.js +22 -2
  17. package/dist/lib/googleCloudStorage.js.map +1 -1
  18. package/dist/lib/prisma.d.ts +104 -5
  19. package/dist/lib/prisma.d.ts.map +1 -1
  20. package/dist/lib/prisma.js +33 -2
  21. package/dist/lib/prisma.js.map +1 -1
  22. package/dist/models/agenda.d.ts +20 -20
  23. package/dist/models/announcement.d.ts +177 -69
  24. package/dist/models/announcement.d.ts.map +1 -1
  25. package/dist/models/assignment.d.ts +551 -353
  26. package/dist/models/assignment.d.ts.map +1 -1
  27. package/dist/models/attendance.d.ts +197 -71
  28. package/dist/models/attendance.d.ts.map +1 -1
  29. package/dist/models/auth.d.ts +348 -114
  30. package/dist/models/auth.d.ts.map +1 -1
  31. package/dist/models/class.d.ts +324 -198
  32. package/dist/models/class.d.ts.map +1 -1
  33. package/dist/models/class.js +2 -2
  34. package/dist/models/class.js.map +1 -1
  35. package/dist/models/cloudStorage.d.ts +146 -56
  36. package/dist/models/cloudStorage.d.ts.map +1 -1
  37. package/dist/models/comment.d.ts +267 -115
  38. package/dist/models/comment.d.ts.map +1 -1
  39. package/dist/models/conversation.d.ts +199 -73
  40. package/dist/models/conversation.d.ts.map +1 -1
  41. package/dist/models/department.d.ts +196 -70
  42. package/dist/models/department.d.ts.map +1 -1
  43. package/dist/models/event.d.ts +287 -125
  44. package/dist/models/event.d.ts.map +1 -1
  45. package/dist/models/file.d.ts +389 -263
  46. package/dist/models/file.d.ts.map +1 -1
  47. package/dist/models/folder.d.ts +433 -181
  48. package/dist/models/folder.d.ts.map +1 -1
  49. package/dist/models/labChat.d.ts +209 -83
  50. package/dist/models/labChat.d.ts.map +1 -1
  51. package/dist/models/marketing.d.ts +80 -26
  52. package/dist/models/marketing.d.ts.map +1 -1
  53. package/dist/models/message.d.ts +95 -41
  54. package/dist/models/message.d.ts.map +1 -1
  55. package/dist/models/newtonChat.d.ts +83 -29
  56. package/dist/models/newtonChat.d.ts.map +1 -1
  57. package/dist/models/notification.d.ts +61 -25
  58. package/dist/models/notification.d.ts.map +1 -1
  59. package/dist/models/school.d.ts +349 -133
  60. package/dist/models/school.d.ts.map +1 -1
  61. package/dist/models/section.d.ts +216 -72
  62. package/dist/models/section.d.ts.map +1 -1
  63. package/dist/models/tag.d.ts +156 -48
  64. package/dist/models/tag.d.ts.map +1 -1
  65. package/dist/models/user.d.ts +82 -28
  66. package/dist/models/user.d.ts.map +1 -1
  67. package/dist/models/worksheet.d.ts +1020 -411
  68. package/dist/models/worksheet.d.ts.map +1 -1
  69. package/dist/models/worksheet.js +8 -2
  70. package/dist/models/worksheet.js.map +1 -1
  71. package/dist/pipelines/aiLabChat.d.ts +0 -8
  72. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  73. package/dist/pipelines/aiLabChat.js +50 -67
  74. package/dist/pipelines/aiLabChat.js.map +1 -1
  75. package/dist/pipelines/aiLabChatContract.d.ts +393 -388
  76. package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
  77. package/dist/pipelines/aiLabChatContract.js +63 -69
  78. package/dist/pipelines/aiLabChatContract.js.map +1 -1
  79. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  80. package/dist/pipelines/aiNewtonChat.js +41 -41
  81. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  82. package/dist/pipelines/gradeWorksheet.d.ts +8 -8
  83. package/dist/pipelines/labChatPrompt.d.ts +10 -8
  84. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  85. package/dist/pipelines/labChatPrompt.js +47 -67
  86. package/dist/pipelines/labChatPrompt.js.map +1 -1
  87. package/dist/pipelines/pdfToWorksheet.d.ts +2 -2
  88. package/dist/routers/_app.d.ts +2203 -1957
  89. package/dist/routers/_app.d.ts.map +1 -1
  90. package/dist/routers/_app.js +4 -2
  91. package/dist/routers/_app.js.map +1 -1
  92. package/dist/routers/admin.d.ts +13 -13
  93. package/dist/routers/agenda.d.ts +18 -18
  94. package/dist/routers/announcement.d.ts +32 -32
  95. package/dist/routers/assignment.d.ts +335 -335
  96. package/dist/routers/attendance.d.ts +10 -10
  97. package/dist/routers/auth.d.ts +2 -2
  98. package/dist/routers/class.d.ts +147 -147
  99. package/dist/routers/comment.d.ts +22 -22
  100. package/dist/routers/conversation.d.ts +17 -17
  101. package/dist/routers/department.d.ts +34 -34
  102. package/dist/routers/discussion.d.ts +65 -53
  103. package/dist/routers/discussion.d.ts.map +1 -1
  104. package/dist/routers/discussion.js +6 -2
  105. package/dist/routers/discussion.js.map +1 -1
  106. package/dist/routers/embedding.d.ts +69 -0
  107. package/dist/routers/embedding.d.ts.map +1 -0
  108. package/dist/routers/embedding.js +143 -0
  109. package/dist/routers/embedding.js.map +1 -0
  110. package/dist/routers/event.d.ts +39 -39
  111. package/dist/routers/file.d.ts +12 -12
  112. package/dist/routers/folder.d.ts +38 -38
  113. package/dist/routers/labChat.d.ts +19 -19
  114. package/dist/routers/marketing.d.ts +2 -2
  115. package/dist/routers/message.d.ts +13 -13
  116. package/dist/routers/newtonChat.d.ts +4 -4
  117. package/dist/routers/notifications.d.ts +9 -9
  118. package/dist/routers/permissions.d.ts +14 -14
  119. package/dist/routers/school.d.ts +55 -55
  120. package/dist/routers/search.d.ts +22 -1
  121. package/dist/routers/search.d.ts.map +1 -1
  122. package/dist/routers/search.js +9 -3
  123. package/dist/routers/search.js.map +1 -1
  124. package/dist/routers/section.d.ts +4 -4
  125. package/dist/routers/studentProgress.d.ts +1 -1
  126. package/dist/routers/tag.d.ts +7 -7
  127. package/dist/routers/user.d.ts +10 -10
  128. package/dist/routers/worksheet.d.ts +95 -57
  129. package/dist/routers/worksheet.d.ts.map +1 -1
  130. package/dist/routers/worksheet.js +26 -2
  131. package/dist/routers/worksheet.js.map +1 -1
  132. package/dist/routes/aiStream.d.ts +2 -0
  133. package/dist/routes/aiStream.d.ts.map +1 -0
  134. package/dist/routes/aiStream.js +696 -0
  135. package/dist/routes/aiStream.js.map +1 -0
  136. package/dist/seedDatabase.d.ts +2 -2
  137. package/dist/seedDatabase.js +6 -6
  138. package/dist/seedDatabase.js.map +1 -1
  139. package/dist/services/adminInstitution.d.ts +8 -8
  140. package/dist/services/adminRunJob.d.ts +4 -4
  141. package/dist/services/agenda.d.ts +18 -18
  142. package/dist/services/aiChatStream.d.ts +68 -0
  143. package/dist/services/aiChatStream.d.ts.map +1 -0
  144. package/dist/services/aiChatStream.js +157 -0
  145. package/dist/services/aiChatStream.js.map +1 -0
  146. package/dist/services/aiModifierTools.d.ts +32 -0
  147. package/dist/services/aiModifierTools.d.ts.map +1 -0
  148. package/dist/services/aiModifierTools.js +165 -0
  149. package/dist/services/aiModifierTools.js.map +1 -0
  150. package/dist/services/aiProposalTools.d.ts +32 -0
  151. package/dist/services/aiProposalTools.d.ts.map +1 -0
  152. package/dist/services/aiProposalTools.js +207 -0
  153. package/dist/services/aiProposalTools.js.map +1 -0
  154. package/dist/services/aiStream.d.ts +22 -0
  155. package/dist/services/aiStream.d.ts.map +1 -0
  156. package/dist/services/aiStream.js +142 -0
  157. package/dist/services/aiStream.js.map +1 -0
  158. package/dist/services/aiTools.d.ts +16 -0
  159. package/dist/services/aiTools.d.ts.map +1 -0
  160. package/dist/services/aiTools.js +271 -0
  161. package/dist/services/aiTools.js.map +1 -0
  162. package/dist/services/announcement.d.ts +12 -12
  163. package/dist/services/announcement.d.ts.map +1 -1
  164. package/dist/services/announcement.js +7 -2
  165. package/dist/services/announcement.js.map +1 -1
  166. package/dist/services/assignment.d.ts +293 -293
  167. package/dist/services/assignment.d.ts.map +1 -1
  168. package/dist/services/assignment.js +28 -9
  169. package/dist/services/assignment.js.map +1 -1
  170. package/dist/services/attendance.d.ts +10 -10
  171. package/dist/services/auth.d.ts +2 -2
  172. package/dist/services/class.d.ts +138 -138
  173. package/dist/services/class.d.ts.map +1 -1
  174. package/dist/services/class.js +19 -3
  175. package/dist/services/class.js.map +1 -1
  176. package/dist/services/comment.d.ts +22 -22
  177. package/dist/services/conversation.d.ts +17 -17
  178. package/dist/services/department.d.ts +33 -33
  179. package/dist/services/discussion.d.ts +58 -58
  180. package/dist/services/discussion.d.ts.map +1 -1
  181. package/dist/services/discussion.js +8 -8
  182. package/dist/services/discussion.js.map +1 -1
  183. package/dist/services/discussionSummary.d.ts +21 -0
  184. package/dist/services/discussionSummary.d.ts.map +1 -0
  185. package/dist/services/discussionSummary.js +179 -0
  186. package/dist/services/discussionSummary.js.map +1 -0
  187. package/dist/services/embedding.d.ts +67 -0
  188. package/dist/services/embedding.d.ts.map +1 -0
  189. package/dist/services/embedding.js +226 -0
  190. package/dist/services/embedding.js.map +1 -0
  191. package/dist/services/event.d.ts +37 -37
  192. package/dist/services/file.d.ts +24 -24
  193. package/dist/services/folder.d.ts +36 -36
  194. package/dist/services/labChat.d.ts +19 -19
  195. package/dist/services/marketing.d.ts +2 -2
  196. package/dist/services/message.d.ts +11 -11
  197. package/dist/services/message.d.ts.map +1 -1
  198. package/dist/services/message.js +2 -2
  199. package/dist/services/message.js.map +1 -1
  200. package/dist/services/newtonChat.d.ts +4 -4
  201. package/dist/services/notification.d.ts +9 -9
  202. package/dist/services/notification.d.ts.map +1 -1
  203. package/dist/services/permissions.d.ts +14 -14
  204. package/dist/services/school.d.ts +50 -50
  205. package/dist/services/search.d.ts +12 -0
  206. package/dist/services/search.d.ts.map +1 -1
  207. package/dist/services/search.js +120 -2
  208. package/dist/services/search.js.map +1 -1
  209. package/dist/services/section.d.ts +3 -3
  210. package/dist/services/tag.d.ts +4 -4
  211. package/dist/services/user.d.ts +4 -4
  212. package/dist/services/user.d.ts.map +1 -1
  213. package/dist/services/user.js +10 -15
  214. package/dist/services/user.js.map +1 -1
  215. package/dist/services/worksheet.d.ts +60 -48
  216. package/dist/services/worksheet.d.ts.map +1 -1
  217. package/dist/services/worksheet.js +11 -4
  218. package/dist/services/worksheet.js.map +1 -1
  219. package/dist/utils/streamBroadcast.d.ts +11 -0
  220. package/dist/utils/streamBroadcast.d.ts.map +1 -0
  221. package/dist/utils/streamBroadcast.js +32 -0
  222. package/dist/utils/streamBroadcast.js.map +1 -0
  223. package/dist/workers/connection.d.ts +15 -0
  224. package/dist/workers/connection.d.ts.map +1 -1
  225. package/dist/workers/connection.js +20 -2
  226. package/dist/workers/connection.js.map +1 -1
  227. package/dist/workers/index.js +4 -2
  228. package/dist/workers/index.js.map +1 -1
  229. package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -1
  230. package/dist/workers/processors/createRunJobWorker.js +31 -8
  231. package/dist/workers/processors/createRunJobWorker.js.map +1 -1
  232. package/dist/workers/processors/embedContent.d.ts +8 -0
  233. package/dist/workers/processors/embedContent.d.ts.map +1 -0
  234. package/dist/workers/processors/embedContent.js +76 -0
  235. package/dist/workers/processors/embedContent.js.map +1 -0
  236. package/dist/workers/queues.d.ts +6 -0
  237. package/dist/workers/queues.d.ts.map +1 -1
  238. package/dist/workers/queues.js +5 -3
  239. package/dist/workers/queues.js.map +1 -1
  240. package/package.json +5 -1
  241. package/prisma/migrations/20260609215135_init/migration.sql +1412 -0
  242. package/prisma/migrations/20260610130000_add_worksheet_tags/migration.sql +13 -0
  243. package/prisma/schema.prisma +38 -7
  244. package/src/index.ts +4 -0
  245. package/src/lib/config/cors.ts +32 -9
  246. package/src/lib/config/env.ts +6 -0
  247. package/src/lib/googleCloudStorage.ts +21 -0
  248. package/src/lib/prisma.ts +40 -1
  249. package/src/models/class.ts +3 -2
  250. package/src/models/worksheet.ts +5 -2
  251. package/src/pipelines/aiLabChat.ts +54 -79
  252. package/src/pipelines/aiLabChatContract.ts +69 -69
  253. package/src/pipelines/aiNewtonChat.ts +46 -43
  254. package/src/pipelines/labChatPrompt.ts +63 -88
  255. package/src/routers/_app.ts +2 -0
  256. package/src/routers/discussion.ts +7 -0
  257. package/src/routers/embedding.ts +159 -0
  258. package/src/routers/search.ts +10 -1
  259. package/src/routers/worksheet.ts +27 -0
  260. package/src/routes/aiStream.ts +789 -0
  261. package/src/seedDatabase.ts +4 -4
  262. package/src/services/aiChatStream.ts +219 -0
  263. package/src/services/aiModifierTools.ts +188 -0
  264. package/src/services/aiProposalTools.ts +272 -0
  265. package/src/services/aiStream.ts +175 -0
  266. package/src/services/aiTools.ts +290 -0
  267. package/src/services/announcement.ts +7 -0
  268. package/src/services/assignment.ts +28 -7
  269. package/src/services/class.ts +20 -1
  270. package/src/services/discussion.ts +9 -8
  271. package/src/services/discussionSummary.ts +226 -0
  272. package/src/services/embedding.ts +285 -0
  273. package/src/services/message.ts +2 -2
  274. package/src/services/search.ts +138 -0
  275. package/src/services/user.ts +9 -17
  276. package/src/services/worksheet.ts +9 -2
  277. package/src/utils/streamBroadcast.ts +48 -0
  278. package/src/workers/connection.ts +19 -0
  279. package/src/workers/index.ts +2 -0
  280. package/src/workers/processors/createRunJobWorker.ts +32 -6
  281. package/src/workers/processors/embedContent.ts +90 -0
  282. package/src/workers/queues.ts +5 -1
  283. package/tests/unit/pipelines/aiLabChatContract.test.ts +38 -21
  284. package/tests/unit/pipelines/labChatPrompt.test.ts +23 -36
  285. package/prisma/migrations/20250807062924_init/migration.sql +0 -436
  286. package/prisma/migrations/20250919063704_init2/migration.sql +0 -24
  287. package/prisma/migrations/20250920143543_add_profile_fields/migration.sql +0 -15
  288. package/prisma/migrations/20250922165147_add_chat_models/migration.sql +0 -68
  289. package/prisma/migrations/20250922165314_add_chat_roles/migration.sql +0 -5
  290. package/prisma/migrations/20250922171242_refactor_notification_system/migration.sql +0 -20
  291. package/prisma/migrations/20250922172208_add_mentions_system/migration.sql +0 -21
  292. package/prisma/migrations/20250925072732_add_lab_chat_system/migration.sql +0 -25
  293. package/prisma/migrations/20251020151505_add_upload_tracking_fields/migration.sql +0 -57
  294. package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +0 -30
  295. package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +0 -35
  296. package/prisma/migrations/20260410124000_add_submission_recommendation_state/migration.sql +0 -14
  297. package/prisma/migrations/20260415103500_add_session_type/migration.sql +0 -33
  298. package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +0 -19
  299. package/prisma/migrations/20260417083000_add_conversation_member_hidden_at/migration.sql +0 -2
  300. package/prisma/migrations/20260502230500_assignment_submission_comments/migration.sql +0 -60
  301. package/prisma/migrations/20260504071500_school_memberships/migration.sql +0 -81
  302. package/prisma/migrations/20260506162500_consolidate_run_job_and_unify_job_status/migration.sql +0 -166
  303. package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +0 -184
  304. package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +0 -93
  305. package/prisma/migrations/20260510183000_add_discussions/migration.sql +0 -151
  306. /package/.github/workflows/{npm.yml → npm-publish.yml} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aiModifierTools.js","sources":["services/aiModifierTools.ts"],"sourceRoot":"/","sourcesContent":["/**\n * AI modifier \"field-update\" tools.\n *\n * Unlike the Lab-chat proposal tools (which only *record* a payload for the\n * teacher to accept later), these tools DIRECTLY edit a single key of an\n * existing assignment or worksheet and persist it immediately via the normal\n * service layer. The modifier route locks the resource (Redis flag + Pusher\n * `modifier-started`) while the stream runs, and each successful field write\n * fires a callback so the route can broadcast `modifier-field-updated` to every\n * open editor — they apply the change live and stay read-only until the run\n * completes or fails.\n *\n * NOTE: like `aiTools.ts` / `aiProposalTools.ts`, these intentionally use\n * `jsonSchema()` rather than the zod `tool()` helper to avoid the TS2589\n * \"excessively deep\" compiler blow-up on nested schemas.\n */\nimport { jsonSchema, type ToolSet } from 'ai';\nimport { updateAssignmentRecord } from './assignment.js';\nimport { updateWorksheetRecord } from './worksheet.js';\n\n/** A single field write that was applied to a resource. */\nexport interface ModifierFieldUpdate {\n field: string;\n value: unknown;\n /** Human-readable label for the change, e.g. \"title\" or \"due date\". */\n label: string;\n}\n\nconst ASSIGNMENT_TYPES = [\n 'HOMEWORK', 'QUIZ', 'TEST', 'PROJECT', 'ESSAY', 'DISCUSSION', 'PRESENTATION', 'LAB', 'OTHER',\n] as const;\n\n/**\n * Editable scalar keys of an assignment, with the coercion + human label used\n * when the model passes a (string) value. Keys map 1:1 to\n * `updateAssignmentRecord` input fields.\n */\nconst ASSIGNMENT_FIELDS: Record<\n string,\n { label: string; type: 'string' | 'number' | 'boolean' | 'date' | 'enum'; enumValues?: readonly string[] }\n> = {\n title: { label: 'title', type: 'string' },\n instructions: { label: 'instructions', type: 'string' },\n dueDate: { label: 'due date', type: 'date' },\n type: { label: 'type', type: 'enum', enumValues: ASSIGNMENT_TYPES },\n maxGrade: { label: 'max grade', type: 'number' },\n graded: { label: 'graded', type: 'boolean' },\n weight: { label: 'weight', type: 'number' },\n acceptFiles: { label: 'file submissions', type: 'boolean' },\n acceptExtendedResponse: { label: 'extended response', type: 'boolean' },\n acceptWorksheet: { label: 'worksheet submissions', type: 'boolean' },\n aiPolicyLevel: { label: 'AI policy level', type: 'number' },\n};\n\nconst WORKSHEET_FIELDS: Record<string, { label: string; type: 'string' }> = {\n name: { label: 'name', type: 'string' },\n};\n\n/** Coerce a model-supplied string value into the type a field expects. */\nfunction coerceValue(\n raw: string,\n spec: { type: 'string' | 'number' | 'boolean' | 'date' | 'enum'; enumValues?: readonly string[] },\n): unknown {\n switch (spec.type) {\n case 'number': {\n const n = Number(raw);\n if (Number.isNaN(n)) throw new Error(`Expected a number but got \"${raw}\"`);\n return n;\n }\n case 'boolean': {\n const v = raw.trim().toLowerCase();\n if (['true', 'yes', '1'].includes(v)) return true;\n if (['false', 'no', '0'].includes(v)) return false;\n throw new Error(`Expected a boolean but got \"${raw}\"`);\n }\n case 'date': {\n const d = new Date(raw);\n if (Number.isNaN(d.getTime())) throw new Error(`Expected an ISO date but got \"${raw}\"`);\n return d.toISOString();\n }\n case 'enum': {\n const v = raw.trim().toUpperCase();\n if (spec.enumValues && !spec.enumValues.includes(v)) {\n throw new Error(`Expected one of ${spec.enumValues.join(', ')} but got \"${raw}\"`);\n }\n return v;\n }\n default:\n return raw;\n }\n}\n\n/**\n * Build the field-update tool for an assignment. The `onFieldUpdate` callback\n * runs after a successful persist so the route can broadcast the change.\n */\nexport function assignmentModifierTools(\n userId: string,\n assignmentId: string,\n classId: string,\n onFieldUpdate: (update: ModifierFieldUpdate) => Promise<void> | void,\n): ToolSet {\n const fieldNames = Object.keys(ASSIGNMENT_FIELDS);\n return {\n update_assignment_field: {\n description:\n 'Directly update a single field of the assignment and save it immediately. ' +\n 'Call this once per field the teacher wants changed. The assignment is locked ' +\n 'for editing until you finish.',\n inputSchema: jsonSchema({\n type: 'object',\n properties: {\n field: {\n type: 'string',\n enum: fieldNames,\n description: 'The assignment field to update.',\n },\n value: {\n type: 'string',\n description:\n 'The new value as a string. Booleans use \"true\"/\"false\", numbers as digits, ' +\n 'dates as ISO 8601, type as one of the allowed enum values.',\n },\n },\n required: ['field', 'value'],\n }),\n execute: async ({ field, value }: { field: string; value: string }) => {\n const spec = ASSIGNMENT_FIELDS[field];\n if (!spec) {\n return `Unknown field \"${field}\". Allowed: ${fieldNames.join(', ')}.`;\n }\n let coerced: unknown;\n try {\n coerced = coerceValue(value, spec);\n } catch (err) {\n return `Could not update ${spec.label}: ${(err as Error).message}`;\n }\n const updateInput: Parameters<typeof updateAssignmentRecord>[1] = {\n id: assignmentId,\n classId,\n };\n (updateInput as Record<string, unknown>)[field] = coerced;\n await updateAssignmentRecord(userId, updateInput);\n await onFieldUpdate({ field, value: coerced, label: spec.label });\n return `Updated ${spec.label}.`;\n },\n },\n };\n}\n\n/** Build the field-update tool for a worksheet. */\nexport function worksheetModifierTools(\n worksheetId: string,\n userId: string,\n onFieldUpdate: (update: ModifierFieldUpdate) => Promise<void> | void,\n): ToolSet {\n const fieldNames = Object.keys(WORKSHEET_FIELDS);\n return {\n update_worksheet_field: {\n description:\n 'Directly update a single field of the worksheet and save it immediately. ' +\n 'The worksheet is locked for editing until you finish.',\n inputSchema: jsonSchema({\n type: 'object',\n properties: {\n field: {\n type: 'string',\n enum: fieldNames,\n description: 'The worksheet field to update.',\n },\n value: { type: 'string', description: 'The new value as a string.' },\n },\n required: ['field', 'value'],\n }),\n execute: async ({ field, value }: { field: string; value: string }) => {\n const spec = WORKSHEET_FIELDS[field];\n if (!spec) {\n return `Unknown field \"${field}\". Allowed: ${fieldNames.join(', ')}.`;\n }\n const updateInput: Parameters<typeof updateWorksheetRecord>[2] = {};\n (updateInput as Record<string, unknown>)[field] = value;\n await updateWorksheetRecord(worksheetId, userId, updateInput);\n await onFieldUpdate({ field, value, label: spec.label });\n return `Updated ${spec.label}.`;\n },\n },\n };\n}\n"],"names":[],"mappings":"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","debug_id":"d1f30ff9-5af3-5e92-b758-96498a1a819d"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Lab-chat "proposal" tools.
3
+ *
4
+ * In the agentic Lab pipeline the model produces create-actions by *calling
5
+ * tools* (e.g. `propose_assignment`) instead of returning a JSON blob. Each
6
+ * tool records a structured payload into a shared accumulator and returns a
7
+ * short confirmation so the model can keep narrating. After the stream ends
8
+ * the pipeline turns the accumulator into the `message.meta.*ToCreate` shape
9
+ * the frontend accept-cards already consume — so the data contract is
10
+ * preserved while the generation becomes genuine inline tool calls.
11
+ *
12
+ * NOTE: these tools intentionally use `jsonSchema()` rather than the zod
13
+ * `tool()` helper. Zod-typed `tool()` definitions with deeply nested schemas
14
+ * make the TS compiler instantiate excessively deep types (TS2589) and OOM,
15
+ * so we mirror the existing `aiTools.ts` pattern (plain JSON Schema + manually
16
+ * typed execute args).
17
+ */
18
+ import { type ToolSet } from 'ai';
19
+ import type { AssignmentToCreate, WorksheetToCreate, SectionToCreate, DocToCreate } from '../pipelines/aiLabChatContract.js';
20
+ export interface LabProposals {
21
+ assignments: AssignmentToCreate[];
22
+ worksheets: WorksheetToCreate[];
23
+ sections: SectionToCreate[];
24
+ docs: DocToCreate[];
25
+ }
26
+ export declare function createLabProposals(): LabProposals;
27
+ /**
28
+ * Build the set of proposal tools backed by the given accumulator.
29
+ * Merge these with the read-only RAG tools from `aiTools(classId)`.
30
+ */
31
+ export declare function labProposalTools(proposals: LabProposals): ToolSet;
32
+ //# sourceMappingURL=aiProposalTools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aiProposalTools.d.ts","sourceRoot":"/","sources":["services/aiProposalTools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,IAAI,CAAC;AAE9C,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,WAAW,EACZ,MAAM,mCAAmC,CAAC;AAG3C,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,kBAAkB,EAAE,CAAC;IAClC,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,IAAI,EAAE,WAAW,EAAE,CAAC;CACrB;AAED,wBAAgB,kBAAkB,IAAI,YAAY,CAEjD;AAsDD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,YAAY,GAAG,OAAO,CAiLjE"}
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Lab-chat "proposal" tools.
3
+ *
4
+ * In the agentic Lab pipeline the model produces create-actions by *calling
5
+ * tools* (e.g. `propose_assignment`) instead of returning a JSON blob. Each
6
+ * tool records a structured payload into a shared accumulator and returns a
7
+ * short confirmation so the model can keep narrating. After the stream ends
8
+ * the pipeline turns the accumulator into the `message.meta.*ToCreate` shape
9
+ * the frontend accept-cards already consume — so the data contract is
10
+ * preserved while the generation becomes genuine inline tool calls.
11
+ *
12
+ * NOTE: these tools intentionally use `jsonSchema()` rather than the zod
13
+ * `tool()` helper. Zod-typed `tool()` definitions with deeply nested schemas
14
+ * make the TS compiler instantiate excessively deep types (TS2589) and OOM,
15
+ * so we mirror the existing `aiTools.ts` pattern (plain JSON Schema + manually
16
+ * typed execute args).
17
+ */
18
+
19
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="68fcd1e0-9203-516c-abb8-0ea09e76afca")}catch(e){}}();
20
+ import { jsonSchema } from 'ai';
21
+ import { v4 } from 'uuid';
22
+ export function createLabProposals() {
23
+ return { assignments: [], worksheets: [], sections: [], docs: [] };
24
+ }
25
+ function normalizeDueDate(input) {
26
+ if (input) {
27
+ const parsed = new Date(input);
28
+ if (!Number.isNaN(parsed.getTime()))
29
+ return parsed.toISOString();
30
+ }
31
+ // Default: one week out.
32
+ return new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
33
+ }
34
+ const ASSIGNMENT_TYPES = [
35
+ 'HOMEWORK', 'QUIZ', 'TEST', 'PROJECT', 'ESSAY', 'DISCUSSION', 'PRESENTATION', 'LAB', 'OTHER',
36
+ ];
37
+ const QUESTION_TYPES = [
38
+ 'MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'LONG_ANSWER', 'MATH_EXPRESSION', 'ESSAY',
39
+ ];
40
+ /**
41
+ * Build the set of proposal tools backed by the given accumulator.
42
+ * Merge these with the read-only RAG tools from `aiTools(classId)`.
43
+ */
44
+ export function labProposalTools(proposals) {
45
+ return {
46
+ propose_section: {
47
+ description: 'Propose a new section (folder) to organise class content. Call once per section you want to create.',
48
+ inputSchema: jsonSchema({
49
+ type: 'object',
50
+ properties: {
51
+ name: { type: 'string', description: 'Section name' },
52
+ color: { type: 'string', description: 'Optional hex color like #3b82f6' },
53
+ },
54
+ required: ['name'],
55
+ }),
56
+ execute: async ({ name, color }) => {
57
+ proposals.sections.push({ name, color: color ?? null });
58
+ return `Section "${name}" proposed.`;
59
+ },
60
+ },
61
+ propose_worksheet: {
62
+ description: 'Propose a worksheet of questions for the teacher to review and create. Include all questions in a single call.',
63
+ inputSchema: jsonSchema({
64
+ type: 'object',
65
+ properties: {
66
+ title: { type: 'string', description: 'Worksheet title' },
67
+ questions: {
68
+ type: 'array',
69
+ description: 'The list of questions',
70
+ items: {
71
+ type: 'object',
72
+ properties: {
73
+ type: { type: 'string', enum: QUESTION_TYPES },
74
+ question: { type: 'string' },
75
+ answer: { type: 'string' },
76
+ options: {
77
+ type: 'array',
78
+ description: 'For MULTIPLE_CHOICE/TRUE_FALSE questions',
79
+ items: {
80
+ type: 'object',
81
+ properties: {
82
+ text: { type: 'string' },
83
+ isCorrect: { type: 'boolean' },
84
+ },
85
+ required: ['text', 'isCorrect'],
86
+ },
87
+ },
88
+ markScheme: {
89
+ type: 'array',
90
+ items: {
91
+ type: 'object',
92
+ properties: {
93
+ points: { type: 'number' },
94
+ description: { type: 'string' },
95
+ },
96
+ required: ['points', 'description'],
97
+ },
98
+ },
99
+ points: { type: 'number' },
100
+ },
101
+ required: ['type', 'question'],
102
+ },
103
+ },
104
+ },
105
+ required: ['title', 'questions'],
106
+ }),
107
+ execute: async ({ title, questions }) => {
108
+ const worksheet = {
109
+ title,
110
+ questions: (questions ?? []).map((q, index) => ({
111
+ type: q.type,
112
+ question: q.question,
113
+ answer: q.answer ?? '',
114
+ options: (q.options ?? []).map((o) => ({
115
+ id: v4(),
116
+ text: o.text,
117
+ isCorrect: o.isCorrect,
118
+ })),
119
+ markScheme: (q.markScheme ?? []).map((m) => ({
120
+ id: v4(),
121
+ points: m.points,
122
+ description: m.description,
123
+ })),
124
+ points: q.points ?? 0,
125
+ order: index,
126
+ })),
127
+ };
128
+ proposals.worksheets.push(worksheet);
129
+ return `Worksheet "${title}" with ${worksheet.questions.length} question(s) proposed.`;
130
+ },
131
+ },
132
+ propose_assignment: {
133
+ description: 'Propose an assignment for the teacher to review and create. Call once per assignment.',
134
+ inputSchema: jsonSchema({
135
+ type: 'object',
136
+ properties: {
137
+ title: { type: 'string' },
138
+ instructions: { type: 'string' },
139
+ dueDate: {
140
+ type: 'string',
141
+ description: 'ISO 8601 datetime. Defaults to one week from now if omitted.',
142
+ },
143
+ type: { type: 'string', enum: ASSIGNMENT_TYPES },
144
+ maxGrade: { type: 'number' },
145
+ acceptFiles: { type: 'boolean' },
146
+ acceptExtendedResponse: { type: 'boolean' },
147
+ acceptWorksheet: { type: 'boolean' },
148
+ },
149
+ required: ['title', 'instructions'],
150
+ }),
151
+ execute: async ({ title, instructions, dueDate, type, maxGrade, acceptFiles, acceptExtendedResponse, acceptWorksheet, }) => {
152
+ const assignment = {
153
+ title,
154
+ instructions,
155
+ dueDate: normalizeDueDate(dueDate),
156
+ type: type ?? 'HOMEWORK',
157
+ maxGrade: maxGrade ?? 100,
158
+ acceptFiles: acceptFiles ?? true,
159
+ acceptExtendedResponse: acceptExtendedResponse ?? false,
160
+ acceptWorksheet: acceptWorksheet ?? false,
161
+ gradingBoundaryId: null,
162
+ markSchemeId: null,
163
+ worksheetIds: [],
164
+ studentIds: [],
165
+ sectionId: null,
166
+ attachments: [],
167
+ };
168
+ proposals.assignments.push(assignment);
169
+ return `Assignment "${title}" proposed.`;
170
+ },
171
+ },
172
+ generate_document: {
173
+ description: 'Generate a document (rendered to a PDF and attached to the message). Provide the document title and an ordered list of formatting blocks.',
174
+ inputSchema: jsonSchema({
175
+ type: 'object',
176
+ properties: {
177
+ title: { type: 'string' },
178
+ blocks: {
179
+ type: 'array',
180
+ description: 'Ordered list of document formatting blocks',
181
+ items: {
182
+ type: 'object',
183
+ properties: {
184
+ format: { type: 'number', description: 'Block format id (0-12)' },
185
+ content: {
186
+ description: 'String, or array of strings for list blocks',
187
+ },
188
+ metadata: {
189
+ type: 'object',
190
+ description: 'Optional styling metadata',
191
+ },
192
+ },
193
+ required: ['format', 'content'],
194
+ },
195
+ },
196
+ },
197
+ required: ['title', 'blocks'],
198
+ }),
199
+ execute: async ({ title, blocks }) => {
200
+ proposals.docs.push({ title, blocks });
201
+ return `Document "${title}" queued for generation.`;
202
+ },
203
+ },
204
+ };
205
+ }
206
+ //# sourceMappingURL=aiProposalTools.js.map
207
+ //# debugId=68fcd1e0-9203-516c-abb8-0ea09e76afca
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aiProposalTools.js","sources":["services/aiProposalTools.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Lab-chat \"proposal\" tools.\n *\n * In the agentic Lab pipeline the model produces create-actions by *calling\n * tools* (e.g. `propose_assignment`) instead of returning a JSON blob. Each\n * tool records a structured payload into a shared accumulator and returns a\n * short confirmation so the model can keep narrating. After the stream ends\n * the pipeline turns the accumulator into the `message.meta.*ToCreate` shape\n * the frontend accept-cards already consume — so the data contract is\n * preserved while the generation becomes genuine inline tool calls.\n *\n * NOTE: these tools intentionally use `jsonSchema()` rather than the zod\n * `tool()` helper. Zod-typed `tool()` definitions with deeply nested schemas\n * make the TS compiler instantiate excessively deep types (TS2589) and OOM,\n * so we mirror the existing `aiTools.ts` pattern (plain JSON Schema + manually\n * typed execute args).\n */\nimport { jsonSchema, type ToolSet } from 'ai';\nimport { v4 } from 'uuid';\nimport type {\n AssignmentToCreate,\n WorksheetToCreate,\n SectionToCreate,\n DocToCreate,\n} from '../pipelines/aiLabChatContract.js';\nimport type { DocumentBlock } from '../lib/jsonStyles.js';\n\nexport interface LabProposals {\n assignments: AssignmentToCreate[];\n worksheets: WorksheetToCreate[];\n sections: SectionToCreate[];\n docs: DocToCreate[];\n}\n\nexport function createLabProposals(): LabProposals {\n return { assignments: [], worksheets: [], sections: [], docs: [] };\n}\n\nfunction normalizeDueDate(input?: string | null): string {\n if (input) {\n const parsed = new Date(input);\n if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();\n }\n // Default: one week out.\n return new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();\n}\n\nconst ASSIGNMENT_TYPES = [\n 'HOMEWORK', 'QUIZ', 'TEST', 'PROJECT', 'ESSAY', 'DISCUSSION', 'PRESENTATION', 'LAB', 'OTHER',\n] as const;\ntype AssignmentType = (typeof ASSIGNMENT_TYPES)[number];\n\nconst QUESTION_TYPES = [\n 'MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'LONG_ANSWER', 'MATH_EXPRESSION', 'ESSAY',\n] as const;\ntype QuestionType = (typeof QUESTION_TYPES)[number];\n\ninterface ProposeSectionArgs {\n name: string;\n color?: string | null;\n}\n\ninterface ProposeWorksheetArgs {\n title: string;\n questions: Array<{\n type: QuestionType;\n question: string;\n answer?: string;\n options?: Array<{ text: string; isCorrect: boolean }>;\n markScheme?: Array<{ points: number; description: string }>;\n points?: number;\n }>;\n}\n\ninterface ProposeAssignmentArgs {\n title: string;\n instructions: string;\n dueDate?: string;\n type?: AssignmentType;\n maxGrade?: number;\n acceptFiles?: boolean;\n acceptExtendedResponse?: boolean;\n acceptWorksheet?: boolean;\n}\n\ninterface GenerateDocumentArgs {\n title: string;\n blocks: DocumentBlock[];\n}\n\n/**\n * Build the set of proposal tools backed by the given accumulator.\n * Merge these with the read-only RAG tools from `aiTools(classId)`.\n */\nexport function labProposalTools(proposals: LabProposals): ToolSet {\n return {\n propose_section: {\n description:\n 'Propose a new section (folder) to organise class content. Call once per section you want to create.',\n inputSchema: jsonSchema({\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Section name' },\n color: { type: 'string', description: 'Optional hex color like #3b82f6' },\n },\n required: ['name'],\n }),\n execute: async ({ name, color }: ProposeSectionArgs) => {\n proposals.sections.push({ name, color: color ?? null });\n return `Section \"${name}\" proposed.`;\n },\n },\n\n propose_worksheet: {\n description:\n 'Propose a worksheet of questions for the teacher to review and create. Include all questions in a single call.',\n inputSchema: jsonSchema({\n type: 'object',\n properties: {\n title: { type: 'string', description: 'Worksheet title' },\n questions: {\n type: 'array',\n description: 'The list of questions',\n items: {\n type: 'object',\n properties: {\n type: { type: 'string', enum: QUESTION_TYPES as unknown as string[] },\n question: { type: 'string' },\n answer: { type: 'string' },\n options: {\n type: 'array',\n description: 'For MULTIPLE_CHOICE/TRUE_FALSE questions',\n items: {\n type: 'object',\n properties: {\n text: { type: 'string' },\n isCorrect: { type: 'boolean' },\n },\n required: ['text', 'isCorrect'],\n },\n },\n markScheme: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n points: { type: 'number' },\n description: { type: 'string' },\n },\n required: ['points', 'description'],\n },\n },\n points: { type: 'number' },\n },\n required: ['type', 'question'],\n },\n },\n },\n required: ['title', 'questions'],\n }),\n execute: async ({ title, questions }: ProposeWorksheetArgs) => {\n const worksheet: WorksheetToCreate = {\n title,\n questions: (questions ?? []).map((q, index) => ({\n type: q.type,\n question: q.question,\n answer: q.answer ?? '',\n options: (q.options ?? []).map((o) => ({\n id: v4(),\n text: o.text,\n isCorrect: o.isCorrect,\n })),\n markScheme: (q.markScheme ?? []).map((m) => ({\n id: v4(),\n points: m.points,\n description: m.description,\n })),\n points: q.points ?? 0,\n order: index,\n })),\n };\n proposals.worksheets.push(worksheet);\n return `Worksheet \"${title}\" with ${worksheet.questions.length} question(s) proposed.`;\n },\n },\n\n propose_assignment: {\n description:\n 'Propose an assignment for the teacher to review and create. Call once per assignment.',\n inputSchema: jsonSchema({\n type: 'object',\n properties: {\n title: { type: 'string' },\n instructions: { type: 'string' },\n dueDate: {\n type: 'string',\n description: 'ISO 8601 datetime. Defaults to one week from now if omitted.',\n },\n type: { type: 'string', enum: ASSIGNMENT_TYPES as unknown as string[] },\n maxGrade: { type: 'number' },\n acceptFiles: { type: 'boolean' },\n acceptExtendedResponse: { type: 'boolean' },\n acceptWorksheet: { type: 'boolean' },\n },\n required: ['title', 'instructions'],\n }),\n execute: async ({\n title,\n instructions,\n dueDate,\n type,\n maxGrade,\n acceptFiles,\n acceptExtendedResponse,\n acceptWorksheet,\n }: ProposeAssignmentArgs) => {\n const assignment: AssignmentToCreate = {\n title,\n instructions,\n dueDate: normalizeDueDate(dueDate),\n type: type ?? 'HOMEWORK',\n maxGrade: maxGrade ?? 100,\n acceptFiles: acceptFiles ?? true,\n acceptExtendedResponse: acceptExtendedResponse ?? false,\n acceptWorksheet: acceptWorksheet ?? false,\n gradingBoundaryId: null,\n markSchemeId: null,\n worksheetIds: [],\n studentIds: [],\n sectionId: null,\n attachments: [],\n };\n proposals.assignments.push(assignment);\n return `Assignment \"${title}\" proposed.`;\n },\n },\n\n generate_document: {\n description:\n 'Generate a document (rendered to a PDF and attached to the message). Provide the document title and an ordered list of formatting blocks.',\n inputSchema: jsonSchema({\n type: 'object',\n properties: {\n title: { type: 'string' },\n blocks: {\n type: 'array',\n description: 'Ordered list of document formatting blocks',\n items: {\n type: 'object',\n properties: {\n format: { type: 'number', description: 'Block format id (0-12)' },\n content: {\n description: 'String, or array of strings for list blocks',\n },\n metadata: {\n type: 'object',\n description: 'Optional styling metadata',\n },\n },\n required: ['format', 'content'],\n },\n },\n },\n required: ['title', 'blocks'],\n }),\n execute: async ({ title, blocks }: GenerateDocumentArgs) => {\n proposals.docs.push({ title, blocks } as unknown as DocToCreate);\n return `Document \"${title}\" queued for generation.`;\n },\n },\n };\n}\n"],"names":[],"mappings":"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","debug_id":"68fcd1e0-9203-516c-abb8-0ea09e76afca"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * AI Streaming service – handles streamed AI responses with tool calls.
3
+ * Uses Vercel AI SDK for streaming and Redis for temporary chunk caching.
4
+ */
5
+ import { type ModelMessage } from 'ai';
6
+ export interface StreamOptions {
7
+ conversationId: string;
8
+ aiMessageId: string;
9
+ messages: ModelMessage[];
10
+ systemPrompt: string;
11
+ classId?: string;
12
+ enableTools?: boolean;
13
+ model?: string;
14
+ temperature?: number;
15
+ maxOutputTokens?: number;
16
+ }
17
+ /**
18
+ * Stream an AI response, broadcasting chunks via Pusher and caching in Redis.
19
+ * On completion, persists the full response to the database.
20
+ */
21
+ export declare function streamAIResponse(options: StreamOptions): Promise<void>;
22
+ //# sourceMappingURL=aiStream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aiStream.d.ts","sourceRoot":"/","sources":["services/aiStream.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,IAAI,CAAC;AAahE,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA6I5E"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * AI Streaming service – handles streamed AI responses with tool calls.
3
+ * Uses Vercel AI SDK for streaming and Redis for temporary chunk caching.
4
+ */
5
+
6
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="1fe100cb-9555-5dad-9a73-f4e62fb469b2")}catch(e){}}();
7
+ import { streamText, stepCountIs } from 'ai';
8
+ import { openai } from '@ai-sdk/openai';
9
+ import { getRedis } from '../lib/redis.js';
10
+ import { prisma } from '../lib/prisma.js';
11
+ import { pusher, chatChannel } from '../lib/pusher.js';
12
+ import { getAIUserId, ensureAIUserExists } from '../utils/aiUser.js';
13
+ import { logger } from '../utils/logger.js';
14
+ import { JobStatus } from '@prisma/client';
15
+ import { aiTools } from './aiTools.js';
16
+ const STREAM_REDIS_TTL = 600; // 10 minutes TTL for stream keys
17
+ const STREAM_KEY_PREFIX = 'stream:';
18
+ /**
19
+ * Stream an AI response, broadcasting chunks via Pusher and caching in Redis.
20
+ * On completion, persists the full response to the database.
21
+ */
22
+ export async function streamAIResponse(options) {
23
+ const { conversationId, aiMessageId, messages, systemPrompt, classId, enableTools = true, model = 'gpt-4o-mini', temperature = 0.7, maxOutputTokens = 4096, } = options;
24
+ await ensureAIUserExists();
25
+ const redis = getRedis();
26
+ const streamKey = `${STREAM_KEY_PREFIX}${aiMessageId}`;
27
+ // Initialize Redis stream cache
28
+ if (redis) {
29
+ await redis.set(streamKey, '', 'EX', STREAM_REDIS_TTL);
30
+ }
31
+ let fullContent = '';
32
+ const toolCallResults = [];
33
+ try {
34
+ const toolSet = enableTools && classId ? aiTools(classId) : undefined;
35
+ const result = streamText({
36
+ model: openai(model),
37
+ system: systemPrompt,
38
+ messages,
39
+ tools: toolSet,
40
+ temperature,
41
+ maxOutputTokens,
42
+ stopWhen: stepCountIs(5),
43
+ onStepFinish: async ({ toolCalls, toolResults }) => {
44
+ if (toolCalls && toolCalls.length > 0) {
45
+ for (const tc of toolCalls) {
46
+ try {
47
+ await pusher.trigger(chatChannel(conversationId), 'message-tool-call', {
48
+ messageId: aiMessageId,
49
+ toolName: tc.toolName,
50
+ status: 'executing',
51
+ });
52
+ }
53
+ catch (err) {
54
+ logger.error('Failed to broadcast tool call event', { err, aiMessageId });
55
+ }
56
+ }
57
+ }
58
+ if (toolResults) {
59
+ for (const tr of toolResults) {
60
+ toolCallResults.push({
61
+ toolName: tr.toolName,
62
+ result: tr.output,
63
+ });
64
+ }
65
+ }
66
+ },
67
+ });
68
+ // Stream text chunks to client
69
+ for await (const chunk of result.textStream) {
70
+ fullContent += chunk;
71
+ // Cache in Redis
72
+ if (redis) {
73
+ await redis.append(streamKey, chunk);
74
+ }
75
+ // Broadcast chunk to client via Pusher
76
+ try {
77
+ await pusher.trigger(chatChannel(conversationId), 'message-stream-chunk', {
78
+ messageId: aiMessageId,
79
+ chunk,
80
+ accumulated: fullContent.length,
81
+ });
82
+ }
83
+ catch (err) {
84
+ logger.warn('Failed to broadcast stream chunk', { err, aiMessageId });
85
+ }
86
+ }
87
+ // Stream complete — persist to DB
88
+ const meta = toolCallResults.length > 0
89
+ ? JSON.stringify({ toolCalls: toolCallResults })
90
+ : undefined;
91
+ await prisma.message.update({
92
+ where: { id: aiMessageId },
93
+ data: {
94
+ content: fullContent,
95
+ status: JobStatus.COMPLETED,
96
+ ...(meta ? { meta: JSON.parse(meta) } : {}),
97
+ },
98
+ });
99
+ // Broadcast final message-updated event
100
+ await pusher.trigger(chatChannel(conversationId), 'message-updated', {
101
+ id: aiMessageId,
102
+ content: fullContent,
103
+ senderId: getAIUserId(),
104
+ conversationId,
105
+ status: JobStatus.COMPLETED,
106
+ sender: {
107
+ id: getAIUserId(),
108
+ username: 'Newton_AI',
109
+ profile: { displayName: 'Newton AI', profilePicture: null },
110
+ },
111
+ mentionedUserIds: [],
112
+ meta: meta ? JSON.parse(meta) : {},
113
+ attachments: [],
114
+ });
115
+ logger.info('AI stream completed', {
116
+ aiMessageId,
117
+ conversationId,
118
+ contentLength: fullContent.length,
119
+ toolCalls: toolCallResults.length,
120
+ });
121
+ }
122
+ catch (error) {
123
+ logger.error('AI stream failed', { error, aiMessageId, conversationId });
124
+ await prisma.message.update({
125
+ where: { id: aiMessageId },
126
+ data: { status: JobStatus.FAILED },
127
+ });
128
+ await pusher.trigger(chatChannel(conversationId), 'message-updated', {
129
+ id: aiMessageId,
130
+ conversationId,
131
+ status: JobStatus.FAILED,
132
+ });
133
+ throw error;
134
+ }
135
+ finally {
136
+ if (redis) {
137
+ await redis.del(streamKey);
138
+ }
139
+ }
140
+ }
141
+ //# sourceMappingURL=aiStream.js.map
142
+ //# debugId=1fe100cb-9555-5dad-9a73-f4e62fb469b2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aiStream.js","sources":["services/aiStream.ts"],"sourceRoot":"/","sourcesContent":["/**\n * AI Streaming service – handles streamed AI responses with tool calls.\n * Uses Vercel AI SDK for streaming and Redis for temporary chunk caching.\n */\nimport { streamText, stepCountIs, type ModelMessage } from 'ai';\nimport { openai } from '@ai-sdk/openai';\nimport { getRedis } from '../lib/redis.js';\nimport { prisma } from '../lib/prisma.js';\nimport { pusher, chatChannel } from '../lib/pusher.js';\nimport { getAIUserId, ensureAIUserExists } from '../utils/aiUser.js';\nimport { logger } from '../utils/logger.js';\nimport { JobStatus } from '@prisma/client';\nimport { aiTools, type ToolCallResult } from './aiTools.js';\n\nconst STREAM_REDIS_TTL = 600; // 10 minutes TTL for stream keys\nconst STREAM_KEY_PREFIX = 'stream:';\n\nexport interface StreamOptions {\n conversationId: string;\n aiMessageId: string;\n messages: ModelMessage[];\n systemPrompt: string;\n classId?: string;\n enableTools?: boolean;\n model?: string;\n temperature?: number;\n maxOutputTokens?: number;\n}\n\n/**\n * Stream an AI response, broadcasting chunks via Pusher and caching in Redis.\n * On completion, persists the full response to the database.\n */\nexport async function streamAIResponse(options: StreamOptions): Promise<void> {\n const {\n conversationId,\n aiMessageId,\n messages,\n systemPrompt,\n classId,\n enableTools = true,\n model = 'gpt-4o-mini',\n temperature = 0.7,\n maxOutputTokens = 4096,\n } = options;\n\n await ensureAIUserExists();\n\n const redis = getRedis();\n const streamKey = `${STREAM_KEY_PREFIX}${aiMessageId}`;\n\n // Initialize Redis stream cache\n if (redis) {\n await redis.set(streamKey, '', 'EX', STREAM_REDIS_TTL);\n }\n\n let fullContent = '';\n const toolCallResults: ToolCallResult[] = [];\n\n try {\n const toolSet = enableTools && classId ? aiTools(classId) : undefined;\n\n const result = streamText({\n model: openai(model),\n system: systemPrompt,\n messages,\n tools: toolSet,\n temperature,\n maxOutputTokens,\n stopWhen: stepCountIs(5),\n onStepFinish: async ({ toolCalls, toolResults }) => {\n if (toolCalls && toolCalls.length > 0) {\n for (const tc of toolCalls) {\n try {\n await pusher.trigger(chatChannel(conversationId), 'message-tool-call', {\n messageId: aiMessageId,\n toolName: tc.toolName,\n status: 'executing',\n });\n } catch (err) {\n logger.error('Failed to broadcast tool call event', { err, aiMessageId });\n }\n }\n }\n\n if (toolResults) {\n for (const tr of toolResults) {\n toolCallResults.push({\n toolName: tr.toolName,\n result: tr.output,\n });\n }\n }\n },\n });\n\n // Stream text chunks to client\n for await (const chunk of result.textStream) {\n fullContent += chunk;\n\n // Cache in Redis\n if (redis) {\n await redis.append(streamKey, chunk);\n }\n\n // Broadcast chunk to client via Pusher\n try {\n await pusher.trigger(chatChannel(conversationId), 'message-stream-chunk', {\n messageId: aiMessageId,\n chunk,\n accumulated: fullContent.length,\n });\n } catch (err) {\n logger.warn('Failed to broadcast stream chunk', { err, aiMessageId });\n }\n }\n\n // Stream complete — persist to DB\n const meta = toolCallResults.length > 0\n ? JSON.stringify({ toolCalls: toolCallResults })\n : undefined;\n\n await prisma.message.update({\n where: { id: aiMessageId },\n data: {\n content: fullContent,\n status: JobStatus.COMPLETED,\n ...(meta ? { meta: JSON.parse(meta) } : {}),\n },\n });\n\n // Broadcast final message-updated event\n await pusher.trigger(chatChannel(conversationId), 'message-updated', {\n id: aiMessageId,\n content: fullContent,\n senderId: getAIUserId(),\n conversationId,\n status: JobStatus.COMPLETED,\n sender: {\n id: getAIUserId(),\n username: 'Newton_AI',\n profile: { displayName: 'Newton AI', profilePicture: null },\n },\n mentionedUserIds: [],\n meta: meta ? JSON.parse(meta) : {},\n attachments: [],\n });\n\n logger.info('AI stream completed', {\n aiMessageId,\n conversationId,\n contentLength: fullContent.length,\n toolCalls: toolCallResults.length,\n });\n } catch (error) {\n logger.error('AI stream failed', { error, aiMessageId, conversationId });\n\n await prisma.message.update({\n where: { id: aiMessageId },\n data: { status: JobStatus.FAILED },\n });\n\n await pusher.trigger(chatChannel(conversationId), 'message-updated', {\n id: aiMessageId,\n conversationId,\n status: JobStatus.FAILED,\n });\n\n throw error;\n } finally {\n if (redis) {\n await redis.del(streamKey);\n }\n }\n}\n"],"names":[],"mappings":"AAAA;;;GAGG;;;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAqB,MAAM,IAAI,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAuB,MAAM,cAAc,CAAC;AAE5D,MAAM,gBAAgB,GAAG,GAAG,CAAC,CAAC,iCAAiC;AAC/D,MAAM,iBAAiB,GAAG,SAAS,CAAC;AAcpC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAsB;IAC3D,MAAM,EACJ,cAAc,EACd,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,OAAO,EACP,WAAW,GAAG,IAAI,EAClB,KAAK,GAAG,aAAa,EACrB,WAAW,GAAG,GAAG,EACjB,eAAe,GAAG,IAAI,GACvB,GAAG,OAAO,CAAC;IAEZ,MAAM,kBAAkB,EAAE,CAAC;IAE3B,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,MAAM,SAAS,GAAG,GAAG,iBAAiB,GAAG,WAAW,EAAE,CAAC;IAEvD,gCAAgC;IAChC,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,EAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACzD,CAAC;IAED,IAAI,WAAW,GAAG,EAAE,CAAC;IACrB,MAAM,eAAe,GAAqB,EAAE,CAAC;IAE7C,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,WAAW,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAEtE,MAAM,MAAM,GAAG,UAAU,CAAC;YACxB,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC;YACpB,MAAM,EAAE,YAAY;YACpB,QAAQ;YACR,KAAK,EAAE,OAAO;YACd,WAAW;YACX,eAAe;YACf,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC;YACxB,YAAY,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,EAAE;gBACjD,IAAI,SAAS,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACtC,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;wBAC3B,IAAI,CAAC;4BACH,MAAM,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,EAAE,mBAAmB,EAAE;gCACrE,SAAS,EAAE,WAAW;gCACtB,QAAQ,EAAE,EAAE,CAAC,QAAQ;gCACrB,MAAM,EAAE,WAAW;6BACpB,CAAC,CAAC;wBACL,CAAC;wBAAC,OAAO,GAAG,EAAE,CAAC;4BACb,MAAM,CAAC,KAAK,CAAC,qCAAqC,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE,CAAC,CAAC;wBAC5E,CAAC;oBACH,CAAC;gBACH,CAAC;gBAED,IAAI,WAAW,EAAE,CAAC;oBAChB,KAAK,MAAM,EAAE,IAAI,WAAW,EAAE,CAAC;wBAC7B,eAAe,CAAC,IAAI,CAAC;4BACnB,QAAQ,EAAE,EAAE,CAAC,QAAQ;4BACrB,MAAM,EAAE,EAAE,CAAC,MAAM;yBAClB,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC;SACF,CAAC,CAAC;QAEH,+BAA+B;QAC/B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;YAC5C,WAAW,IAAI,KAAK,CAAC;YAErB,iBAAiB;YACjB,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,KAAK,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;YACvC,CAAC;YAED,uCAAuC;YACvC,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,EAAE,sBAAsB,EAAE;oBACxE,SAAS,EAAE,WAAW;oBACtB,KAAK;oBACL,WAAW,EAAE,WAAW,CAAC,MAAM;iBAChC,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,CAAC,IAAI,CAAC,kCAAkC,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE,CAAC,CAAC;YACxE,CAAC;QACH,CAAC;QAED,kCAAkC;QAClC,MAAM,IAAI,GAAG,eAAe,CAAC,MAAM,GAAG,CAAC;YACrC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC;YAChD,CAAC,CAAC,SAAS,CAAC;QAEd,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YAC1B,KAAK,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE;YAC1B,IAAI,EAAE;gBACJ,OAAO,EAAE,WAAW;gBACpB,MAAM,EAAE,SAAS,CAAC,SAAS;gBAC3B,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5C;SACF,CAAC,CAAC;QAEH,wCAAwC;QACxC,MAAM,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,EAAE,iBAAiB,EAAE;YACnE,EAAE,EAAE,WAAW;YACf,OAAO,EAAE,WAAW;YACpB,QAAQ,EAAE,WAAW,EAAE;YACvB,cAAc;YACd,MAAM,EAAE,SAAS,CAAC,SAAS;YAC3B,MAAM,EAAE;gBACN,EAAE,EAAE,WAAW,EAAE;gBACjB,QAAQ,EAAE,WAAW;gBACrB,OAAO,EAAE,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,IAAI,EAAE;aAC5D;YACD,gBAAgB,EAAE,EAAE;YACpB,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE;YAClC,WAAW,EAAE,EAAE;SAChB,CAAC,CAAC;QAEH,MAAM,CAAC,IAAI,CAAC,qBAAqB,EAAE;YACjC,WAAW;YACX,cAAc;YACd,aAAa,EAAE,WAAW,CAAC,MAAM;YACjC,SAAS,EAAE,eAAe,CAAC,MAAM;SAClC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,CAAC;QAEzE,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YAC1B,KAAK,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE;YAC1B,IAAI,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE;SACnC,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,EAAE,iBAAiB,EAAE;YACnE,EAAE,EAAE,WAAW;YACf,cAAc;YACd,MAAM,EAAE,SAAS,CAAC,MAAM;SACzB,CAAC,CAAC;QAEH,MAAM,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACT,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;AACH,CAAC","debug_id":"1fe100cb-9555-5dad-9a73-f4e62fb469b2"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * AI Tool definitions and execution for agent tool calls.
3
+ * Provides RAG search, assignment lookup, worksheet creation, student progress,
4
+ * and web search tools.
5
+ */
6
+ import { type ToolSet } from 'ai';
7
+ export interface ToolCallResult {
8
+ toolName: string;
9
+ result: unknown;
10
+ }
11
+ /**
12
+ * Build the tool set for a given class context.
13
+ * Tools are scoped to a specific class for security.
14
+ */
15
+ export declare function aiTools(classId: string): ToolSet;
16
+ //# sourceMappingURL=aiTools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aiTools.d.ts","sourceRoot":"/","sources":["services/aiTools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,IAAI,CAAC;AAM9C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CA6QhD"}