@studious-lms/server 1.25.0 → 1.27.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 (175) hide show
  1. package/.env.example +3 -1
  2. package/.github/workflows/npm-publish.yml +1 -0
  3. package/AGENTS.md +84 -0
  4. package/dist/models/cloudStorage.d.ts +11 -0
  5. package/dist/models/cloudStorage.d.ts.map +1 -1
  6. package/dist/models/cloudStorage.js +18 -2
  7. package/dist/models/cloudStorage.js.map +1 -1
  8. package/dist/models/conversation.d.ts +1 -0
  9. package/dist/models/conversation.d.ts.map +1 -1
  10. package/dist/models/labChat.d.ts +40 -0
  11. package/dist/models/labChat.d.ts.map +1 -1
  12. package/dist/models/labChat.js +32 -2
  13. package/dist/models/labChat.js.map +1 -1
  14. package/dist/models/marketplace.d.ts +35 -0
  15. package/dist/models/marketplace.d.ts.map +1 -1
  16. package/dist/models/marketplace.js +9 -2
  17. package/dist/models/marketplace.js.map +1 -1
  18. package/dist/models/message.d.ts +3 -0
  19. package/dist/models/message.d.ts.map +1 -1
  20. package/dist/models/module.d.ts +228 -0
  21. package/dist/models/module.d.ts.map +1 -0
  22. package/dist/models/module.js +69 -0
  23. package/dist/models/module.js.map +1 -0
  24. package/dist/pipelines/aiLabChat.d.ts +2 -0
  25. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  26. package/dist/pipelines/aiLabChat.js +60 -16
  27. package/dist/pipelines/aiLabChat.js.map +1 -1
  28. package/dist/pipelines/aiLabChatContract.d.ts +170 -2
  29. package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
  30. package/dist/pipelines/aiLabChatContract.js +30 -2
  31. package/dist/pipelines/aiLabChatContract.js.map +1 -1
  32. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  33. package/dist/pipelines/aiNewtonChat.js +30 -5
  34. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  35. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  36. package/dist/pipelines/gradeWorksheet.js +4 -2
  37. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  38. package/dist/pipelines/labChatPrompt.d.ts +1 -1
  39. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  40. package/dist/pipelines/labChatPrompt.js +13 -14
  41. package/dist/pipelines/labChatPrompt.js.map +1 -1
  42. package/dist/routers/_app.d.ts +1185 -215
  43. package/dist/routers/_app.d.ts.map +1 -1
  44. package/dist/routers/_app.js +4 -2
  45. package/dist/routers/_app.js.map +1 -1
  46. package/dist/routers/admin.d.ts +0 -1
  47. package/dist/routers/admin.d.ts.map +1 -1
  48. package/dist/routers/conversation.d.ts +1 -0
  49. package/dist/routers/conversation.d.ts.map +1 -1
  50. package/dist/routers/labChat.d.ts +10 -0
  51. package/dist/routers/labChat.d.ts.map +1 -1
  52. package/dist/routers/labChat.js +6 -3
  53. package/dist/routers/labChat.js.map +1 -1
  54. package/dist/routers/marketplace.d.ts +41 -0
  55. package/dist/routers/marketplace.d.ts.map +1 -1
  56. package/dist/routers/marketplace.js +13 -3
  57. package/dist/routers/marketplace.js.map +1 -1
  58. package/dist/routers/message.d.ts +37 -4
  59. package/dist/routers/message.d.ts.map +1 -1
  60. package/dist/routers/message.js +24 -7
  61. package/dist/routers/message.js.map +1 -1
  62. package/dist/routers/module.d.ts +345 -0
  63. package/dist/routers/module.d.ts.map +1 -0
  64. package/dist/routers/module.js +82 -0
  65. package/dist/routers/module.js.map +1 -0
  66. package/dist/routers/parent.d.ts +18 -0
  67. package/dist/routers/parent.d.ts.map +1 -1
  68. package/dist/routers/parent.js +10 -3
  69. package/dist/routers/parent.js.map +1 -1
  70. package/dist/routers/studentProgress.d.ts +44 -0
  71. package/dist/routers/studentProgress.d.ts.map +1 -1
  72. package/dist/routers/studentProgress.js +12 -3
  73. package/dist/routers/studentProgress.js.map +1 -1
  74. package/dist/routes/aiStream.js +32 -2
  75. package/dist/routes/aiStream.js.map +1 -1
  76. package/dist/seedDatabase.d.ts.map +1 -1
  77. package/dist/seedDatabase.js +107 -3
  78. package/dist/seedDatabase.js.map +1 -1
  79. package/dist/services/adminUser.d.ts +0 -1
  80. package/dist/services/adminUser.d.ts.map +1 -1
  81. package/dist/services/adminUser.js +7 -4
  82. package/dist/services/adminUser.js.map +1 -1
  83. package/dist/services/aiChatStream.d.ts +15 -0
  84. package/dist/services/aiChatStream.d.ts.map +1 -1
  85. package/dist/services/aiChatStream.js +144 -74
  86. package/dist/services/aiChatStream.js.map +1 -1
  87. package/dist/services/aiGeneration.d.ts +4 -0
  88. package/dist/services/aiGeneration.d.ts.map +1 -0
  89. package/dist/services/aiGeneration.js +88 -0
  90. package/dist/services/aiGeneration.js.map +1 -0
  91. package/dist/services/aiProposalTools.d.ts +4 -1
  92. package/dist/services/aiProposalTools.d.ts.map +1 -1
  93. package/dist/services/aiProposalTools.js +128 -3
  94. package/dist/services/aiProposalTools.js.map +1 -1
  95. package/dist/services/aiProposals.d.ts +70 -0
  96. package/dist/services/aiProposals.d.ts.map +1 -0
  97. package/dist/services/aiProposals.js +363 -0
  98. package/dist/services/aiProposals.js.map +1 -0
  99. package/dist/services/assignment.d.ts.map +1 -1
  100. package/dist/services/assignment.js +8 -3
  101. package/dist/services/assignment.js.map +1 -1
  102. package/dist/services/conversation.d.ts +1 -0
  103. package/dist/services/conversation.d.ts.map +1 -1
  104. package/dist/services/labChat.d.ts +10 -0
  105. package/dist/services/labChat.d.ts.map +1 -1
  106. package/dist/services/labChat.js +52 -3
  107. package/dist/services/labChat.js.map +1 -1
  108. package/dist/services/marketplace.d.ts +36 -0
  109. package/dist/services/marketplace.d.ts.map +1 -1
  110. package/dist/services/marketplace.js +30 -3
  111. package/dist/services/marketplace.js.map +1 -1
  112. package/dist/services/message.d.ts +24 -8
  113. package/dist/services/message.d.ts.map +1 -1
  114. package/dist/services/message.js +170 -117
  115. package/dist/services/message.js.map +1 -1
  116. package/dist/services/module.d.ts +288 -0
  117. package/dist/services/module.d.ts.map +1 -0
  118. package/dist/services/module.js +518 -0
  119. package/dist/services/module.js.map +1 -0
  120. package/dist/services/parentPortal.d.ts +14 -0
  121. package/dist/services/parentPortal.d.ts.map +1 -1
  122. package/dist/services/parentPortal.js +72 -2
  123. package/dist/services/parentPortal.js.map +1 -1
  124. package/dist/services/school.d.ts.map +1 -1
  125. package/dist/services/school.js +17 -5
  126. package/dist/services/school.js.map +1 -1
  127. package/dist/services/studentProgress.d.ts +50 -0
  128. package/dist/services/studentProgress.d.ts.map +1 -1
  129. package/dist/services/studentProgress.js +107 -2
  130. package/dist/services/studentProgress.js.map +1 -1
  131. package/dist/utils/inference.d.ts +10 -0
  132. package/dist/utils/inference.d.ts.map +1 -1
  133. package/dist/utils/inference.js +77 -2
  134. package/dist/utils/inference.js.map +1 -1
  135. package/dist/workers/processors/createRunJobWorker.js +17 -16
  136. package/dist/workers/processors/createRunJobWorker.js.map +1 -1
  137. package/package.json +1 -1
  138. package/prisma/migrations/20260708013000_add_modules/migration.sql +100 -0
  139. package/prisma/migrations/20260811130000_add_ai_message_feedback/migration.sql +7 -0
  140. package/prisma/schema.prisma +101 -0
  141. package/src/models/cloudStorage.ts +19 -0
  142. package/src/models/labChat.ts +34 -0
  143. package/src/models/marketplace.ts +16 -0
  144. package/src/models/module.ts +77 -0
  145. package/src/pipelines/aiLabChat.ts +79 -18
  146. package/src/pipelines/aiLabChatContract.ts +35 -0
  147. package/src/pipelines/aiNewtonChat.ts +34 -2
  148. package/src/pipelines/gradeWorksheet.ts +6 -0
  149. package/src/pipelines/labChatPrompt.ts +11 -12
  150. package/src/routers/_app.ts +2 -0
  151. package/src/routers/labChat.ts +5 -0
  152. package/src/routers/marketplace.ts +16 -0
  153. package/src/routers/message.ts +34 -5
  154. package/src/routers/module.ts +118 -0
  155. package/src/routers/parent.ts +11 -0
  156. package/src/routers/studentProgress.ts +15 -0
  157. package/src/routes/aiStream.ts +32 -0
  158. package/src/seedDatabase.ts +107 -2
  159. package/src/services/adminUser.ts +5 -2
  160. package/src/services/aiChatStream.ts +83 -7
  161. package/src/services/aiGeneration.ts +92 -0
  162. package/src/services/aiProposalTools.ts +166 -1
  163. package/src/services/aiProposals.ts +428 -0
  164. package/src/services/assignment.ts +12 -1
  165. package/src/services/labChat.ts +65 -0
  166. package/src/services/marketplace.ts +39 -0
  167. package/src/services/message.ts +205 -142
  168. package/src/services/module.ts +683 -0
  169. package/src/services/parentPortal.ts +85 -0
  170. package/src/services/school.ts +15 -3
  171. package/src/services/studentProgress.ts +135 -0
  172. package/src/utils/inference.ts +85 -0
  173. package/src/workers/processors/createRunJobWorker.ts +17 -17
  174. package/tests/unit/services/adminUser.test.ts +75 -0
  175. package/tests/unit/utils/adminPanelAccess.test.ts +26 -0
@@ -1 +1 @@
1
- {"version":3,"file":"inference.js","sources":["utils/inference.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Inference util – OpenAI client, inference() for structured output, sendAIMessage for chat.\n */\nimport OpenAI from 'openai';\nimport type { ResponseInput } from \"openai/resources/responses/responses.js\";\nimport { logger } from './logger.js';\nimport { prisma } from '../lib/prisma.js';\nimport { chatChannel, pusher } from '../lib/pusher.js';\nimport { ensureAIUserExists, getAIUserId } from './aiUser.js';\nimport { env } from '../lib/config/env.js';\nimport { JobStatus, Prisma } from '@prisma/client';\nimport { ZodSchema } from 'zod';\nimport { zodTextFormat } from 'openai/helpers/zod';\n\nexport const inferenceClient = new OpenAI({\n apiKey: env.INFERENCE_API_KEY,\n baseURL: env.INFERENCE_API_BASE_URL,\n});\n\nexport const openAIClient = new OpenAI();\n\nconst DEFAULT_INFERENCE_MODEL = 'gpt-5-nano';\n\ntype InferenceMessage = {\n role: 'user' | 'assistant' | 'system';\n content: string;\n};\n\n/**\n * Attribution context for AI usage logging. `schoolId` is resolved from\n * `classId` at log time when not supplied by the caller.\n */\nexport interface AiUsageAttribution {\n userId?: string | null;\n schoolId?: string | null;\n classId?: string | null;\n}\n\nexport interface InferenceOptions {\n reason?: string;\n metadata?: Prisma.InputJsonValue;\n model?: string;\n temperature?: number;\n maxTokens?: number;\n attribution?: AiUsageAttribution;\n}\n\ninterface InferenceUsage {\n promptTokens?: number;\n completionTokens?: number;\n totalTokens?: number;\n}\n\ninterface InferenceAuditLog {\n model: string;\n operation: 'chat.completions' | 'responses.parse' | 'stream.text';\n success: boolean;\n estimatedInputTokens: number;\n reason: string;\n metadata?: Prisma.InputJsonValue;\n usage?: InferenceUsage;\n error?: unknown;\n attribution?: AiUsageAttribution;\n}\n\n// Types for lab chat context\nexport interface LabChatContext {\n subject: string;\n topic: string;\n difficulty: 'beginner' | 'intermediate' | 'advanced';\n objectives: string[];\n resources?: string[];\n persona: string;\n constraints: string[];\n examples?: any[];\n metadata?: Record<string, any>;\n}\n\nexport interface InferenceResponse {\n content: string;\n model: string;\n tokensUsed: number;\n finishReason: string;\n}\n\nfunction normalizeInferenceInput(\n content: string | OpenAI.Chat.Completions.ChatCompletionMessageParam[],\n): InferenceMessage[] {\n if (typeof content === 'string') {\n return [{ role: 'user', content }];\n }\n\n return content.map((message) => ({\n role: message.role as InferenceMessage['role'],\n content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content),\n }));\n}\n\nfunction getInferenceReason(options?: InferenceOptions) {\n return options?.reason?.trim() || 'unspecified';\n}\n\nfunction serializeInferenceError(error: unknown) {\n if (error instanceof Error) {\n return error.message;\n }\n\n if (typeof error === 'string') {\n return error;\n }\n\n try {\n return JSON.stringify(error);\n } catch {\n return 'Unknown inference error';\n }\n}\n\nasync function resolveAttribution(\n attribution?: AiUsageAttribution,\n): Promise<{ userId: string | null; schoolId: string | null; classId: string | null }> {\n const userId = attribution?.userId ?? null;\n const classId = attribution?.classId ?? null;\n let schoolId = attribution?.schoolId ?? null;\n\n if (!schoolId && classId) {\n const classRow = await prisma.class.findUnique({\n where: { id: classId },\n select: { schoolId: true },\n });\n schoolId = classRow?.schoolId ?? null;\n }\n\n return { userId, schoolId, classId };\n}\n\nexport async function logInferenceUsage({\n model,\n operation,\n success,\n estimatedInputTokens,\n reason,\n metadata,\n usage,\n error,\n attribution,\n}: InferenceAuditLog) {\n const context = {\n reason,\n model,\n operation,\n success,\n estimatedInputTokens,\n promptTokens: usage?.promptTokens,\n completionTokens: usage?.completionTokens,\n totalTokens: usage?.totalTokens,\n metadata,\n ...(error ? { error: serializeInferenceError(error) } : {}),\n };\n\n try {\n const resolved = await resolveAttribution(attribution);\n await prisma.aiUsageLog.create({\n data: {\n reason,\n model,\n operation,\n success,\n estimatedInputTokens,\n promptTokens: usage?.promptTokens,\n completionTokens: usage?.completionTokens,\n totalTokens: usage?.totalTokens,\n userId: resolved.userId,\n schoolId: resolved.schoolId,\n classId: resolved.classId,\n ...(metadata !== undefined ? { metadata } : {}),\n ...(error ? { error: serializeInferenceError(error) } : {}),\n },\n });\n } catch (loggingError) {\n logger.error('Failed to persist AI usage log', {\n ...context,\n loggingError: serializeInferenceError(loggingError),\n });\n return;\n }\n\n logger.info('AI usage logged', context);\n}\n\nfunction getChatCompletionUsage(completion: {\n usage?: {\n prompt_tokens?: number;\n completion_tokens?: number;\n total_tokens?: number;\n };\n}): InferenceUsage {\n return {\n promptTokens: completion.usage?.prompt_tokens,\n completionTokens: completion.usage?.completion_tokens,\n totalTokens: completion.usage?.total_tokens,\n };\n}\n\nfunction getResponseUsage(completion: {\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n total_tokens?: number;\n };\n}): InferenceUsage {\n return {\n promptTokens: completion.usage?.input_tokens,\n completionTokens: completion.usage?.output_tokens,\n totalTokens: completion.usage?.total_tokens,\n };\n}\n\n/**\n * Centralized function to send AI messages to conversations\n * Handles database storage and Pusher broadcasting\n */\nexport async function sendAIMessage(\n content: string,\n conversationId: string,\n options: {\n subject?: string;\n attachments?: {\n connect: { id: string }[];\n };\n meta?: Record<string, any>;\n customSender?: {\n displayName: string;\n profilePicture?: string | null;\n };\n } = {}\n): Promise<{\n id: string;\n content: string;\n senderId: string;\n conversationId: string;\n createdAt: Date;\n meta?: Record<string, any>;\n}> {\n await ensureAIUserExists();\n\n const aiMessage = await prisma.message.create({\n data: {\n content,\n senderId: getAIUserId(),\n conversationId,\n ...(options.attachments && {\n attachments: {\n connect: options.attachments.connect,\n },\n }),\n ...(options.meta && {\n meta: options.meta,\n }),\n },\n include: {\n attachments: true,\n },\n });\n\n logger.info('AI Message sent', {\n messageId: aiMessage.id,\n conversationId,\n contentLength: content.length,\n });\n\n const senderInfo = {\n id: getAIUserId(),\n username: 'Newton_AI',\n profile: {\n displayName: 'Newton AI',\n profilePicture: options.customSender?.profilePicture || null,\n },\n };\n\n try {\n await pusher.trigger(chatChannel(conversationId), 'new-message', {\n id: aiMessage.id,\n content: aiMessage.content,\n senderId: getAIUserId(),\n conversationId: aiMessage.conversationId,\n createdAt: aiMessage.createdAt,\n sender: senderInfo,\n mentionedUserIds: [],\n meta: aiMessage.meta,\n attachments: aiMessage.attachments.map((attachment) => ({\n id: attachment.id,\n attachmentId: attachment.id,\n name: attachment.name,\n type: attachment.type,\n size: attachment.size,\n path: attachment.path,\n })),\n });\n } catch (error) {\n logger.error('Failed to broadcast AI message:', { error, messageId: aiMessage.id });\n }\n\n return {\n id: aiMessage.id,\n content: aiMessage.content,\n senderId: getAIUserId(),\n conversationId: aiMessage.conversationId,\n createdAt: aiMessage.createdAt,\n meta: aiMessage.meta as Record<string, any>,\n };\n}\n\ninterface AIMessageSenderInfo {\n displayName?: string;\n profilePicture?: string | null;\n}\n\nfunction buildAISenderInfo(customSender?: AIMessageSenderInfo) {\n return {\n id: getAIUserId(),\n username: 'Newton_AI',\n profile: {\n displayName: customSender?.displayName ?? 'Newton AI',\n profilePicture: customSender?.profilePicture ?? null,\n },\n };\n}\n\n/**\n * Create an AI message placeholder in PENDING state and broadcast it.\n *\n * The AI message lifecycle == job lifecycle: this row is the bubble the\n * client renders for \"AI is thinking...\" and is later flipped by\n * {@link completePendingAIMessage} / {@link failPendingAIMessage}.\n */\nexport async function createPendingAIMessage(\n conversationId: string,\n options: {\n customSender?: AIMessageSenderInfo;\n } = {},\n): Promise<{\n id: string;\n conversationId: string;\n createdAt: Date;\n senderId: string;\n status: JobStatus;\n}> {\n await ensureAIUserExists();\n\n const aiMessage = await prisma.message.create({\n data: {\n content: '',\n senderId: getAIUserId(),\n conversationId,\n status: JobStatus.PENDING,\n },\n });\n\n logger.info('AI placeholder message created', {\n messageId: aiMessage.id,\n conversationId,\n });\n\n const senderInfo = buildAISenderInfo(options.customSender);\n\n try {\n await pusher.trigger(chatChannel(conversationId), 'new-message', {\n id: aiMessage.id,\n content: '',\n senderId: getAIUserId(),\n conversationId: aiMessage.conversationId,\n status: JobStatus.PENDING,\n createdAt: aiMessage.createdAt,\n sender: senderInfo,\n mentionedUserIds: [],\n meta: {},\n attachments: [],\n });\n } catch (error) {\n logger.error('Failed to broadcast pending AI message:', {\n error,\n messageId: aiMessage.id,\n });\n }\n\n return {\n id: aiMessage.id,\n conversationId: aiMessage.conversationId,\n createdAt: aiMessage.createdAt,\n senderId: aiMessage.senderId,\n status: JobStatus.PENDING,\n };\n}\n\n/**\n * Fill a PENDING AI message with its generated content and flip it to\n * COMPLETED. Broadcasts a `message-updated` event so clients that already\n * rendered the placeholder can patch it in place.\n */\nexport async function completePendingAIMessage(\n messageId: string,\n content: string,\n options: {\n attachments?: { connect: { id: string }[] };\n meta?: Record<string, any>;\n customSender?: AIMessageSenderInfo;\n } = {},\n): Promise<{\n id: string;\n content: string;\n conversationId: string;\n createdAt: Date;\n status: JobStatus;\n}> {\n const updated = await prisma.message.update({\n where: { id: messageId },\n data: {\n content,\n status: JobStatus.COMPLETED,\n ...(options.attachments && {\n attachments: {\n connect: options.attachments.connect,\n },\n }),\n ...(options.meta !== undefined && {\n meta: options.meta as Prisma.InputJsonValue,\n }),\n },\n include: {\n attachments: true,\n },\n });\n\n logger.info('AI message completed', {\n messageId: updated.id,\n conversationId: updated.conversationId,\n contentLength: content.length,\n });\n\n const senderInfo = buildAISenderInfo(options.customSender);\n\n try {\n await pusher.trigger(chatChannel(updated.conversationId), 'message-updated', {\n id: updated.id,\n content: updated.content,\n senderId: updated.senderId,\n conversationId: updated.conversationId,\n status: updated.status,\n createdAt: updated.createdAt,\n sender: senderInfo,\n mentionedUserIds: [],\n meta: updated.meta ?? {},\n attachments: updated.attachments.map((attachment) => ({\n id: attachment.id,\n attachmentId: attachment.id,\n name: attachment.name,\n type: attachment.type,\n size: attachment.size,\n path: attachment.path,\n })),\n });\n } catch (error) {\n logger.error('Failed to broadcast completed AI message:', {\n error,\n messageId,\n });\n }\n\n return {\n id: updated.id,\n content: updated.content,\n conversationId: updated.conversationId,\n createdAt: updated.createdAt,\n status: (updated.status ?? JobStatus.COMPLETED) as JobStatus,\n };\n}\n\n/**\n * Flip a PENDING AI message to FAILED and broadcast the status change.\n * Does NOT throw — failure paths in the pipelines call this in catch blocks\n * and we do not want broadcast/DB errors to mask the original error.\n */\nexport async function failPendingAIMessage(\n messageId: string,\n options: { reason?: string } = {},\n): Promise<void> {\n try {\n const updated = await prisma.message.update({\n where: { id: messageId },\n data: { status: JobStatus.FAILED },\n select: { id: true, conversationId: true, status: true },\n });\n\n logger.info('AI message marked as failed', {\n messageId: updated.id,\n conversationId: updated.conversationId,\n reason: options.reason,\n });\n\n try {\n await pusher.trigger(chatChannel(updated.conversationId), 'message-updated', {\n id: updated.id,\n conversationId: updated.conversationId,\n status: updated.status,\n });\n } catch (broadcastError) {\n logger.error('Failed to broadcast failed AI message:', {\n error: broadcastError,\n messageId,\n });\n }\n } catch (error) {\n logger.error('Failed to mark AI message as failed:', {\n error,\n messageId,\n reason: options.reason,\n });\n }\n}\n\n/** Responses API user parts use `type: \"input_*\"`; chat completions use `text` / `image_url` / etc. */\nfunction isResponseInputWithInputParts(\n content: OpenAI.Chat.Completions.ChatCompletionMessageParam[] | ResponseInput\n): content is ResponseInput {\n if (!Array.isArray(content)) return false;\n for (const msg of content) {\n if (!msg || typeof msg !== \"object\" || !(\"content\" in msg)) continue;\n const parts = (msg as { content?: unknown }).content;\n if (!Array.isArray(parts)) continue;\n for (const part of parts) {\n if (\n part &&\n typeof part === \"object\" &&\n \"type\" in part &&\n typeof (part as { type: string }).type === \"string\" &&\n (part as { type: string }).type.startsWith(\"input_\")\n ) {\n return true;\n }\n }\n }\n return false;\n}\n\nfunction buildResponsesParseInput(\n content: string | OpenAI.Chat.Completions.ChatCompletionMessageParam[] | ResponseInput\n): ResponseInput {\n if (typeof content === \"string\") {\n return [{ role: \"user\", content: [{ type: \"input_text\", text: content }] }];\n }\n if (isResponseInputWithInputParts(content)) {\n return content;\n }\n return content as unknown as ResponseInput;\n}\n\nexport async function inference<T>(\n content:\n | string\n | OpenAI.Chat.Completions.ChatCompletionMessageParam[]\n | ResponseInput,\n format?: ZodSchema,\n options?: InferenceOptions,\n): Promise<T> {\n const reason = getInferenceReason(options);\n const model = options?.model ?? DEFAULT_INFERENCE_MODEL;\n const estimatedInputTokens =\n typeof content === 'string'\n ? estimateTokenCount(content)\n : isResponseInputWithInputParts(content)\n ? estimateTokenCount(\n content\n .flatMap((item) =>\n typeof (item as any)?.content === 'string'\n ? [(item as any).content]\n : Array.isArray((item as any)?.content)\n ? (item as any).content\n .filter((p: any) => p?.type === 'input_text')\n .map((p: any) => p?.text ?? '')\n : [],\n )\n .join('\\n'),\n )\n : (content as OpenAI.Chat.Completions.ChatCompletionMessageParam[]).reduce(\n (total, message) =>\n total +\n estimateTokenCount(\n typeof message.content === 'string' ? message.content : '',\n ),\n 0,\n );\n const operation = format ? 'responses.parse' : 'chat.completions';\n\n try {\n if (!format) {\n if (typeof content !== 'string' && isResponseInputWithInputParts(content)) {\n throw new Error(\n 'normalizeInferenceInput cannot handle ResponseInput content with input_* parts when format is undefined; pass a format to use responses.parse.',\n );\n }\n\n const messages = normalizeInferenceInput(\n content as string | OpenAI.Chat.Completions.ChatCompletionMessageParam[],\n );\n const completion = await openAIClient.chat.completions.create({\n model,\n messages,\n ...(options?.temperature !== undefined\n ? { temperature: options.temperature }\n : {}),\n ...(options?.maxTokens !== undefined\n ? { max_tokens: options.maxTokens }\n : {}),\n });\n\n await logInferenceUsage({\n model,\n operation,\n success: true,\n estimatedInputTokens,\n reason,\n metadata: options?.metadata,\n usage: getChatCompletionUsage(completion),\n attribution: options?.attribution,\n });\n\n return completion.choices[0]?.message?.content as T;\n }\n\n const input = buildResponsesParseInput(content);\n const completion = await openAIClient.responses.parse({\n model,\n input,\n text: {\n format: zodTextFormat(format, 'newton_response_format'),\n },\n });\n\n if (!completion) {\n throw new Error('No response generated from inference API');\n }\n\n await logInferenceUsage({\n model,\n operation,\n success: true,\n estimatedInputTokens,\n reason,\n metadata: options?.metadata,\n usage: getResponseUsage(completion),\n attribution: options?.attribution,\n });\n\n return completion.output_parsed as T;\n } catch (error) {\n await logInferenceUsage({\n model,\n operation,\n success: false,\n estimatedInputTokens,\n reason,\n metadata: options?.metadata,\n error,\n attribution: options?.attribution,\n });\n\n logger.error('Failed to generate inference response', {\n error,\n reason,\n model,\n operation,\n metadata: options?.metadata,\n });\n throw error;\n }\n}\n\n/**\n * Get available inference models (for admin/config purposes)\n */\nexport async function getAvailableModels(): Promise<string[]> {\n try {\n const models = await inferenceClient.models.list();\n return models.data\n .filter(model => model.id.includes('command'))\n .map(model => model.id)\n .sort();\n } catch (error) {\n logger.error('Failed to fetch inference models', { error });\n return ['command-r-plus', 'command-r', 'command-light'];\n }\n}\n\n/**\n * Estimate token count for a message (rough approximation)\n */\nexport function estimateTokenCount(text: string): number {\n return Math.ceil(text.length / 4);\n}\n"],"names":[],"mappings":"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","debug_id":"9b4ef7a2-3350-5df6-a238-3340a03e8b3e"}
1
+ {"version":3,"file":"inference.js","sources":["utils/inference.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Inference util – OpenAI client, inference() for structured output, sendAIMessage for chat.\n */\nimport OpenAI from 'openai';\nimport type { ResponseInput } from \"openai/resources/responses/responses.js\";\nimport { logger } from './logger.js';\nimport { prisma } from '../lib/prisma.js';\nimport { chatChannel, pusher } from '../lib/pusher.js';\nimport { ensureAIUserExists, getAIUserId } from './aiUser.js';\nimport { env } from '../lib/config/env.js';\nimport { JobStatus, Prisma } from '@prisma/client';\nimport { ZodSchema } from 'zod';\nimport { zodTextFormat } from 'openai/helpers/zod';\n\nexport const inferenceClient = new OpenAI({\n apiKey: env.INFERENCE_API_KEY,\n baseURL: env.INFERENCE_API_BASE_URL,\n});\n\nexport const openAIClient = new OpenAI();\n\nconst DEFAULT_INFERENCE_MODEL = 'gpt-5-nano';\n\ntype InferenceMessage = {\n role: 'user' | 'assistant' | 'system';\n content: string;\n};\n\n/**\n * Attribution context for AI usage logging. `schoolId` is resolved from\n * `classId` at log time when not supplied by the caller.\n */\nexport interface AiUsageAttribution {\n userId?: string | null;\n schoolId?: string | null;\n classId?: string | null;\n}\n\nexport interface InferenceOptions {\n reason?: string;\n metadata?: Prisma.InputJsonValue;\n model?: string;\n temperature?: number;\n maxTokens?: number;\n attribution?: AiUsageAttribution;\n}\n\ninterface InferenceUsage {\n promptTokens?: number;\n completionTokens?: number;\n totalTokens?: number;\n}\n\ninterface InferenceAuditLog {\n model: string;\n operation: 'chat.completions' | 'responses.parse' | 'stream.text';\n success: boolean;\n estimatedInputTokens: number;\n reason: string;\n metadata?: Prisma.InputJsonValue;\n usage?: InferenceUsage;\n error?: unknown;\n attribution?: AiUsageAttribution;\n}\n\n// Types for lab chat context\nexport interface LabChatContext {\n subject: string;\n topic: string;\n difficulty: 'beginner' | 'intermediate' | 'advanced';\n objectives: string[];\n resources?: string[];\n persona: string;\n constraints: string[];\n examples?: any[];\n metadata?: Record<string, any>;\n}\n\nexport interface InferenceResponse {\n content: string;\n model: string;\n tokensUsed: number;\n finishReason: string;\n}\n\nfunction normalizeInferenceInput(\n content: string | OpenAI.Chat.Completions.ChatCompletionMessageParam[],\n): InferenceMessage[] {\n if (typeof content === 'string') {\n return [{ role: 'user', content }];\n }\n\n return content.map((message) => ({\n role: message.role as InferenceMessage['role'],\n content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content),\n }));\n}\n\nfunction getInferenceReason(options?: InferenceOptions) {\n return options?.reason?.trim() || 'unspecified';\n}\n\nfunction serializeInferenceError(error: unknown) {\n if (error instanceof Error) {\n return error.message;\n }\n\n if (typeof error === 'string') {\n return error;\n }\n\n try {\n return JSON.stringify(error);\n } catch {\n return 'Unknown inference error';\n }\n}\n\nasync function resolveAttribution(\n attribution?: AiUsageAttribution,\n): Promise<{ userId: string | null; schoolId: string | null; classId: string | null }> {\n const userId = attribution?.userId ?? null;\n const classId = attribution?.classId ?? null;\n let schoolId = attribution?.schoolId ?? null;\n\n if (!schoolId && classId) {\n const classRow = await prisma.class.findUnique({\n where: { id: classId },\n select: { schoolId: true },\n });\n schoolId = classRow?.schoolId ?? null;\n }\n\n return { userId, schoolId, classId };\n}\n\nexport async function logInferenceUsage({\n model,\n operation,\n success,\n estimatedInputTokens,\n reason,\n metadata,\n usage,\n error,\n attribution,\n}: InferenceAuditLog) {\n const context = {\n reason,\n model,\n operation,\n success,\n estimatedInputTokens,\n promptTokens: usage?.promptTokens,\n completionTokens: usage?.completionTokens,\n totalTokens: usage?.totalTokens,\n metadata,\n ...(error ? { error: serializeInferenceError(error) } : {}),\n };\n\n try {\n const resolved = await resolveAttribution(attribution);\n await prisma.aiUsageLog.create({\n data: {\n reason,\n model,\n operation,\n success,\n estimatedInputTokens,\n promptTokens: usage?.promptTokens,\n completionTokens: usage?.completionTokens,\n totalTokens: usage?.totalTokens,\n userId: resolved.userId,\n schoolId: resolved.schoolId,\n classId: resolved.classId,\n ...(metadata !== undefined ? { metadata } : {}),\n ...(error ? { error: serializeInferenceError(error) } : {}),\n },\n });\n } catch (loggingError) {\n logger.error('Failed to persist AI usage log', {\n ...context,\n loggingError: serializeInferenceError(loggingError),\n });\n return;\n }\n\n logger.info('AI usage logged', context);\n}\n\nfunction getChatCompletionUsage(completion: {\n usage?: {\n prompt_tokens?: number;\n completion_tokens?: number;\n total_tokens?: number;\n };\n}): InferenceUsage {\n return {\n promptTokens: completion.usage?.prompt_tokens,\n completionTokens: completion.usage?.completion_tokens,\n totalTokens: completion.usage?.total_tokens,\n };\n}\n\nfunction getResponseUsage(completion: {\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n total_tokens?: number;\n };\n}): InferenceUsage {\n return {\n promptTokens: completion.usage?.input_tokens,\n completionTokens: completion.usage?.output_tokens,\n totalTokens: completion.usage?.total_tokens,\n };\n}\n\n/**\n * Centralized function to send AI messages to conversations\n * Handles database storage and Pusher broadcasting\n */\nexport async function sendAIMessage(\n content: string,\n conversationId: string,\n options: {\n subject?: string;\n attachments?: {\n connect: { id: string }[];\n };\n meta?: Record<string, any>;\n customSender?: {\n displayName: string;\n profilePicture?: string | null;\n };\n } = {}\n): Promise<{\n id: string;\n content: string;\n senderId: string;\n conversationId: string;\n createdAt: Date;\n meta?: Record<string, any>;\n}> {\n await ensureAIUserExists();\n\n const aiMessage = await prisma.message.create({\n data: {\n content,\n senderId: getAIUserId(),\n conversationId,\n ...(options.attachments && {\n attachments: {\n connect: options.attachments.connect,\n },\n }),\n ...(options.meta && {\n meta: options.meta,\n }),\n },\n include: {\n attachments: true,\n },\n });\n\n logger.info('AI Message sent', {\n messageId: aiMessage.id,\n conversationId,\n contentLength: content.length,\n });\n\n const senderInfo = {\n id: getAIUserId(),\n username: 'Newton_AI',\n profile: {\n displayName: 'Newton AI',\n profilePicture: options.customSender?.profilePicture || null,\n },\n };\n\n try {\n await pusher.trigger(chatChannel(conversationId), 'new-message', {\n id: aiMessage.id,\n content: aiMessage.content,\n senderId: getAIUserId(),\n conversationId: aiMessage.conversationId,\n createdAt: aiMessage.createdAt,\n sender: senderInfo,\n mentionedUserIds: [],\n meta: aiMessage.meta,\n attachments: aiMessage.attachments.map((attachment) => ({\n id: attachment.id,\n attachmentId: attachment.id,\n name: attachment.name,\n type: attachment.type,\n size: attachment.size,\n path: attachment.path,\n })),\n });\n } catch (error) {\n logger.error('Failed to broadcast AI message:', { error, messageId: aiMessage.id });\n }\n\n return {\n id: aiMessage.id,\n content: aiMessage.content,\n senderId: getAIUserId(),\n conversationId: aiMessage.conversationId,\n createdAt: aiMessage.createdAt,\n meta: aiMessage.meta as Record<string, any>,\n };\n}\n\ninterface AIMessageSenderInfo {\n displayName?: string;\n profilePicture?: string | null;\n}\n\nfunction buildAISenderInfo(customSender?: AIMessageSenderInfo) {\n return {\n id: getAIUserId(),\n username: 'Newton_AI',\n profile: {\n displayName: customSender?.displayName ?? 'Newton AI',\n profilePicture: customSender?.profilePicture ?? null,\n },\n };\n}\n\n/**\n * Create an AI message placeholder in PENDING state and broadcast it.\n *\n * The AI message lifecycle == job lifecycle: this row is the bubble the\n * client renders for \"AI is thinking...\" and is later flipped by\n * {@link completePendingAIMessage} / {@link failPendingAIMessage}.\n */\nexport async function createPendingAIMessage(\n conversationId: string,\n options: {\n customSender?: AIMessageSenderInfo;\n } = {},\n): Promise<{\n id: string;\n conversationId: string;\n createdAt: Date;\n senderId: string;\n status: JobStatus;\n}> {\n await ensureAIUserExists();\n\n const aiMessage = await prisma.message.create({\n data: {\n content: '',\n senderId: getAIUserId(),\n conversationId,\n status: JobStatus.PENDING,\n },\n });\n\n logger.info('AI placeholder message created', {\n messageId: aiMessage.id,\n conversationId,\n });\n\n const senderInfo = buildAISenderInfo(options.customSender);\n\n try {\n await pusher.trigger(chatChannel(conversationId), 'new-message', {\n id: aiMessage.id,\n content: '',\n senderId: getAIUserId(),\n conversationId: aiMessage.conversationId,\n status: JobStatus.PENDING,\n createdAt: aiMessage.createdAt,\n sender: senderInfo,\n mentionedUserIds: [],\n meta: {},\n attachments: [],\n });\n } catch (error) {\n logger.error('Failed to broadcast pending AI message:', {\n error,\n messageId: aiMessage.id,\n });\n }\n\n return {\n id: aiMessage.id,\n conversationId: aiMessage.conversationId,\n createdAt: aiMessage.createdAt,\n senderId: aiMessage.senderId,\n status: JobStatus.PENDING,\n };\n}\n\n/**\n * Fill a PENDING AI message with its generated content and flip it to\n * COMPLETED. Broadcasts a `message-updated` event so clients that already\n * rendered the placeholder can patch it in place.\n */\nexport async function completePendingAIMessage(\n messageId: string,\n content: string,\n options: {\n attachments?: { connect: { id: string }[] };\n meta?: Record<string, any>;\n customSender?: AIMessageSenderInfo;\n } = {},\n): Promise<{\n id: string;\n content: string;\n conversationId: string;\n createdAt: Date;\n status: JobStatus;\n}> {\n const updated = await prisma.message.update({\n where: { id: messageId },\n data: {\n content,\n status: JobStatus.COMPLETED,\n ...(options.attachments && {\n attachments: {\n connect: options.attachments.connect,\n },\n }),\n ...(options.meta !== undefined && {\n meta: options.meta as Prisma.InputJsonValue,\n }),\n },\n include: {\n attachments: true,\n },\n });\n\n logger.info('AI message completed', {\n messageId: updated.id,\n conversationId: updated.conversationId,\n contentLength: content.length,\n });\n\n const senderInfo = buildAISenderInfo(options.customSender);\n\n try {\n await pusher.trigger(chatChannel(updated.conversationId), 'message-updated', {\n id: updated.id,\n content: updated.content,\n senderId: updated.senderId,\n conversationId: updated.conversationId,\n status: updated.status,\n createdAt: updated.createdAt,\n sender: senderInfo,\n mentionedUserIds: [],\n meta: updated.meta ?? {},\n attachments: updated.attachments.map((attachment) => ({\n id: attachment.id,\n attachmentId: attachment.id,\n name: attachment.name,\n type: attachment.type,\n size: attachment.size,\n path: attachment.path,\n })),\n });\n } catch (error) {\n logger.error('Failed to broadcast completed AI message:', {\n error,\n messageId,\n });\n }\n\n return {\n id: updated.id,\n content: updated.content,\n conversationId: updated.conversationId,\n createdAt: updated.createdAt,\n status: (updated.status ?? JobStatus.COMPLETED) as JobStatus,\n };\n}\n\n/**\n * Persist the partial output of a cancelled generation and flip the AI\n * message to CANCELLED. Whatever text/tool parts streamed before the stop\n * are kept on the row (content + meta) so they survive reloads. Broadcasts\n * `message-updated` so clients patch the bubble in place.\n */\nexport async function cancelPendingAIMessage(\n messageId: string,\n content: string,\n options: {\n meta?: Record<string, any>;\n customSender?: AIMessageSenderInfo;\n } = {},\n): Promise<void> {\n try {\n const updated = await prisma.message.update({\n where: { id: messageId },\n data: {\n content,\n status: JobStatus.CANCELLED,\n ...(options.meta !== undefined && {\n meta: options.meta as Prisma.InputJsonValue,\n }),\n },\n include: {\n attachments: true,\n },\n });\n\n logger.info('AI message cancelled', {\n messageId: updated.id,\n conversationId: updated.conversationId,\n contentLength: content.length,\n });\n\n const senderInfo = buildAISenderInfo(options.customSender);\n\n try {\n await pusher.trigger(chatChannel(updated.conversationId), 'message-updated', {\n id: updated.id,\n content: updated.content,\n senderId: updated.senderId,\n conversationId: updated.conversationId,\n status: updated.status,\n createdAt: updated.createdAt,\n sender: senderInfo,\n mentionedUserIds: [],\n meta: updated.meta ?? {},\n attachments: updated.attachments.map((attachment) => ({\n id: attachment.id,\n attachmentId: attachment.id,\n name: attachment.name,\n type: attachment.type,\n size: attachment.size,\n path: attachment.path,\n })),\n });\n } catch (broadcastError) {\n logger.error('Failed to broadcast cancelled AI message:', {\n error: broadcastError,\n messageId,\n });\n }\n } catch (error) {\n logger.error('Failed to mark AI message as cancelled:', {\n error,\n messageId,\n });\n }\n}\n\n/**\n * Flip a PENDING AI message to FAILED and broadcast the status change.\n * Does NOT throw — failure paths in the pipelines call this in catch blocks\n * and we do not want broadcast/DB errors to mask the original error.\n */\nexport async function failPendingAIMessage(\n messageId: string,\n options: { reason?: string } = {},\n): Promise<void> {\n try {\n const current = await prisma.message.findUnique({\n where: { id: messageId },\n select: { status: true },\n });\n // A message the user already cancelled must stay CANCELLED — late\n // failures from the aborted pipeline are a consequence of the cancel.\n if (current?.status === JobStatus.CANCELLED) {\n logger.info('Skipping FAILED flip; AI message already cancelled', {\n messageId,\n reason: options.reason,\n });\n return;\n }\n\n const updated = await prisma.message.update({\n where: { id: messageId },\n data: { status: JobStatus.FAILED },\n select: { id: true, conversationId: true, status: true },\n });\n\n logger.info('AI message marked as failed', {\n messageId: updated.id,\n conversationId: updated.conversationId,\n reason: options.reason,\n });\n\n try {\n await pusher.trigger(chatChannel(updated.conversationId), 'message-updated', {\n id: updated.id,\n conversationId: updated.conversationId,\n status: updated.status,\n });\n } catch (broadcastError) {\n logger.error('Failed to broadcast failed AI message:', {\n error: broadcastError,\n messageId,\n });\n }\n } catch (error) {\n logger.error('Failed to mark AI message as failed:', {\n error,\n messageId,\n reason: options.reason,\n });\n }\n}\n\n/** Responses API user parts use `type: \"input_*\"`; chat completions use `text` / `image_url` / etc. */\nfunction isResponseInputWithInputParts(\n content: OpenAI.Chat.Completions.ChatCompletionMessageParam[] | ResponseInput\n): content is ResponseInput {\n if (!Array.isArray(content)) return false;\n for (const msg of content) {\n if (!msg || typeof msg !== \"object\" || !(\"content\" in msg)) continue;\n const parts = (msg as { content?: unknown }).content;\n if (!Array.isArray(parts)) continue;\n for (const part of parts) {\n if (\n part &&\n typeof part === \"object\" &&\n \"type\" in part &&\n typeof (part as { type: string }).type === \"string\" &&\n (part as { type: string }).type.startsWith(\"input_\")\n ) {\n return true;\n }\n }\n }\n return false;\n}\n\nfunction buildResponsesParseInput(\n content: string | OpenAI.Chat.Completions.ChatCompletionMessageParam[] | ResponseInput\n): ResponseInput {\n if (typeof content === \"string\") {\n return [{ role: \"user\", content: [{ type: \"input_text\", text: content }] }];\n }\n if (isResponseInputWithInputParts(content)) {\n return content;\n }\n return content as unknown as ResponseInput;\n}\n\nexport async function inference<T>(\n content:\n | string\n | OpenAI.Chat.Completions.ChatCompletionMessageParam[]\n | ResponseInput,\n format?: ZodSchema,\n options?: InferenceOptions,\n): Promise<T> {\n const reason = getInferenceReason(options);\n const model = options?.model ?? DEFAULT_INFERENCE_MODEL;\n const estimatedInputTokens =\n typeof content === 'string'\n ? estimateTokenCount(content)\n : isResponseInputWithInputParts(content)\n ? estimateTokenCount(\n content\n .flatMap((item) =>\n typeof (item as any)?.content === 'string'\n ? [(item as any).content]\n : Array.isArray((item as any)?.content)\n ? (item as any).content\n .filter((p: any) => p?.type === 'input_text')\n .map((p: any) => p?.text ?? '')\n : [],\n )\n .join('\\n'),\n )\n : (content as OpenAI.Chat.Completions.ChatCompletionMessageParam[]).reduce(\n (total, message) =>\n total +\n estimateTokenCount(\n typeof message.content === 'string' ? message.content : '',\n ),\n 0,\n );\n const operation = format ? 'responses.parse' : 'chat.completions';\n\n try {\n if (!format) {\n if (typeof content !== 'string' && isResponseInputWithInputParts(content)) {\n throw new Error(\n 'normalizeInferenceInput cannot handle ResponseInput content with input_* parts when format is undefined; pass a format to use responses.parse.',\n );\n }\n\n const messages = normalizeInferenceInput(\n content as string | OpenAI.Chat.Completions.ChatCompletionMessageParam[],\n );\n const completion = await openAIClient.chat.completions.create({\n model,\n messages,\n ...(options?.temperature !== undefined\n ? { temperature: options.temperature }\n : {}),\n ...(options?.maxTokens !== undefined\n ? { max_tokens: options.maxTokens }\n : {}),\n });\n\n await logInferenceUsage({\n model,\n operation,\n success: true,\n estimatedInputTokens,\n reason,\n metadata: options?.metadata,\n usage: getChatCompletionUsage(completion),\n attribution: options?.attribution,\n });\n\n return completion.choices[0]?.message?.content as T;\n }\n\n const input = buildResponsesParseInput(content);\n const completion = await openAIClient.responses.parse({\n model,\n input,\n text: {\n format: zodTextFormat(format, 'newton_response_format'),\n },\n });\n\n if (!completion) {\n throw new Error('No response generated from inference API');\n }\n\n await logInferenceUsage({\n model,\n operation,\n success: true,\n estimatedInputTokens,\n reason,\n metadata: options?.metadata,\n usage: getResponseUsage(completion),\n attribution: options?.attribution,\n });\n\n return completion.output_parsed as T;\n } catch (error) {\n await logInferenceUsage({\n model,\n operation,\n success: false,\n estimatedInputTokens,\n reason,\n metadata: options?.metadata,\n error,\n attribution: options?.attribution,\n });\n\n logger.error('Failed to generate inference response', {\n error,\n reason,\n model,\n operation,\n metadata: options?.metadata,\n });\n throw error;\n }\n}\n\n/**\n * Get available inference models (for admin/config purposes)\n */\nexport async function getAvailableModels(): Promise<string[]> {\n try {\n const models = await inferenceClient.models.list();\n return models.data\n .filter(model => model.id.includes('command'))\n .map(model => model.id)\n .sort();\n } catch (error) {\n logger.error('Failed to fetch inference models', { error });\n return ['command-r-plus', 'command-r', 'command-light'];\n }\n}\n\n/**\n * Estimate token count for a message (rough approximation)\n */\nexport function estimateTokenCount(text: string): number {\n return Math.ceil(text.length / 4);\n}\n"],"names":[],"mappings":"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","debug_id":"ba92dc8c-6ba8-5d95-a027-50c47894fcc3"}
@@ -16,7 +16,7 @@
16
16
  * `userId` / `classId` from the original `enqueueRunJob` call.
17
17
  */
18
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]="411f71f1-9246-5cac-bbd7-d82a51d25576")}catch(e){}}();
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]="c443bbb7-d5db-544b-a259-dc4d70dde58b")}catch(e){}}();
20
20
  import { Worker } from 'bullmq';
21
21
  import { Prisma } from '@prisma/client';
22
22
  import { bullConnection, getBullPrefix } from '../connection.js';
@@ -54,6 +54,13 @@ export function createRunJobWorker({ type, workerOptions, run, }) {
54
54
  });
55
55
  return existing.result;
56
56
  }
57
+ if (existing.status === 'CANCELLED') {
58
+ logger.info(`${name} RunJob cancelled before start; skipping`, {
59
+ runJobId,
60
+ bullJobId: job.id,
61
+ });
62
+ return existing.result;
63
+ }
57
64
  await prisma.runJob.update({
58
65
  where: { id: runJobId },
59
66
  data: {
@@ -69,8 +76,10 @@ export function createRunJobWorker({ type, workerOptions, run, }) {
69
76
  userId: existing.userId ?? null,
70
77
  classId: existing.classId ?? null,
71
78
  });
72
- await prisma.runJob.update({
73
- where: { id: runJobId },
79
+ // `updateMany` with a status guard so a cancellation that landed while
80
+ // the run was in flight is not overwritten with SUCCEEDED.
81
+ await prisma.runJob.updateMany({
82
+ where: { id: runJobId, status: { not: 'CANCELLED' } },
74
83
  data: {
75
84
  status: 'SUCCEEDED',
76
85
  finishedAt: new Date(),
@@ -100,9 +109,11 @@ export function createRunJobWorker({ type, workerOptions, run, }) {
100
109
  });
101
110
  if (!runJobId)
102
111
  return;
112
+ // Status guard: don't let a late failure (e.g. from an aborted stream)
113
+ // overwrite a RunJob the user already cancelled.
103
114
  await prisma.runJob
104
- .update({
105
- where: { id: runJobId },
115
+ .updateMany({
116
+ where: { id: runJobId, status: { not: 'CANCELLED' } },
106
117
  data: {
107
118
  error: err.message,
108
119
  ...(isFinalAttempt
@@ -111,16 +122,6 @@ export function createRunJobWorker({ type, workerOptions, run, }) {
111
122
  },
112
123
  })
113
124
  .catch((updateError) => {
114
- // P2025 = record not found. The RunJob row is gone (orphaned job from a
115
- // shared/reset Redis), so there's nothing to flag as FAILED — log
116
- // quietly instead of as an error to avoid noise.
117
- if (updateError instanceof Prisma.PrismaClientKnownRequestError &&
118
- updateError.code === 'P2025') {
119
- logger.warn(`${name} failure not recorded; RunJob no longer exists`, {
120
- runJobId,
121
- });
122
- return;
123
- }
124
125
  logger.error(`Failed to record ${name} failure on RunJob`, {
125
126
  runJobId,
126
127
  error: updateError instanceof Error
@@ -138,4 +139,4 @@ export function createRunJobWorker({ type, workerOptions, run, }) {
138
139
  return worker;
139
140
  }
140
141
  //# sourceMappingURL=createRunJobWorker.js.map
141
- //# debugId=411f71f1-9246-5cac-bbd7-d82a51d25576
142
+ //# debugId=c443bbb7-d5db-544b-a259-dc4d70dde58b
@@ -1 +1 @@
1
- {"version":3,"file":"createRunJobWorker.js","sources":["workers/processors/createRunJobWorker.ts"],"sourceRoot":"/","sourcesContent":["/**\n * `createRunJobWorker` — shared factory for every BullMQ processor in this app.\n *\n * Wraps the boilerplate that every RunJob-backed worker needs:\n * - Idempotent skip if the RunJob already SUCCEEDED (recovery after a crash\n * that lost the BullMQ ack).\n * - Status transitions: PENDING/RUNNING → RUNNING (with attempts++) → SUCCEEDED.\n * - Failure handling: record latest error every attempt, flip to FAILED only\n * on the final retry.\n * - Standardised structured logging on `error` / `failed` / `completed`.\n *\n * A processor implementation only has to provide its `RunJobType`, optional\n * BullMQ options (concurrency, limiter, etc.), and a `run` callback that does\n * the actual work. The callback receives the typed `JobInputs[type]` payload\n * and a context object with `runJobId`, `bullJobId`, `attempt`, plus the\n * `userId` / `classId` from the original `enqueueRunJob` call.\n */\nimport { Worker, type Job, type WorkerOptions } from 'bullmq';\nimport { Prisma, type RunJobType } from '@prisma/client';\nimport { bullConnection, getBullPrefix } from '../connection.js';\nimport { prisma } from '../../lib/prisma.js';\nimport { logger } from '../../utils/logger.js';\nimport { QUEUE_NAMES, type JobInputs } from '../queues.js';\n\nexport interface RunJobContext {\n runJobId: string;\n bullJobId: string | undefined;\n /** 1-indexed attempt number of the current run. */\n attempt: number;\n /** From `EnqueueOptions.userId` on the producer side. */\n userId: string | null;\n /** From `EnqueueOptions.classId` on the producer side. */\n classId: string | null;\n}\n\nexport interface CreateRunJobWorkerOptions<T extends RunJobType> {\n type: T;\n /** BullMQ `WorkerOptions` minus `connection`, which is supplied by the helper. */\n workerOptions?: Omit<WorkerOptions, 'connection'>;\n /**\n * The actual work. Return any JSON-serialisable value (or `null`) — it gets\n * persisted to `RunJob.result` for the client to read back.\n */\n run: (\n input: JobInputs[T],\n ctx: RunJobContext,\n ) => Promise<Prisma.InputJsonValue | null>;\n}\n\ntype WorkerData<T extends RunJobType> = JobInputs[T] & { runJobId: string };\n\nexport function createRunJobWorker<T extends RunJobType>({\n type,\n workerOptions,\n run,\n}: CreateRunJobWorkerOptions<T>): Worker<WorkerData<T>> {\n const name = QUEUE_NAMES[type];\n\n const worker = new Worker<WorkerData<T>>(\n name,\n async (job: Job<WorkerData<T>>) => {\n const { runJobId, ...rest } = job.data;\n // The destructured rest is `Omit<JobInputs[T] & { runJobId }, 'runJobId'>`,\n // which TS can't simplify back to `JobInputs[T]` while `T` is generic.\n const input = rest as unknown as JobInputs[T];\n\n const existing = await prisma.runJob.findUnique({\n where: { id: runJobId },\n select: { status: true, result: true, userId: true, classId: true },\n });\n\n if (!existing) {\n // Orphaned job: the RunJob row this job points at does not exist in\n // THIS database. Typically caused by a Redis instance shared across\n // environments/databases (the `prefix` namespacing in this file's\n // Worker options is the primary guard against that), or by a DB reset\n // that left jobs behind in Redis. Discard instead of throwing on the\n // `update` below and burning all retry attempts.\n logger.warn(`${name} RunJob not found; discarding orphaned job`, {\n runJobId,\n bullJobId: job.id,\n });\n return null;\n }\n\n if (existing.status === 'SUCCEEDED') {\n logger.info(`${name} RunJob already succeeded; skipping re-run`, {\n runJobId,\n bullJobId: job.id,\n });\n return existing.result;\n }\n\n await prisma.runJob.update({\n where: { id: runJobId },\n data: {\n status: 'RUNNING',\n startedAt: existing.status === 'RUNNING' ? undefined : new Date(),\n attempts: { increment: 1 },\n },\n });\n\n const result = await run(input, {\n runJobId,\n bullJobId: job.id,\n attempt: job.attemptsMade + 1,\n userId: existing.userId ?? null,\n classId: existing.classId ?? null,\n });\n\n await prisma.runJob.update({\n where: { id: runJobId },\n data: {\n status: 'SUCCEEDED',\n finishedAt: new Date(),\n error: null,\n result: result === null ? Prisma.JsonNull : result,\n },\n });\n\n return result;\n },\n { connection: bullConnection, prefix: getBullPrefix(), ...workerOptions },\n );\n\n worker.on('error', (err) => {\n logger.error(`${name} worker error`, {\n error: err.message,\n stack: err.stack,\n });\n });\n\n worker.on('failed', async (job, err) => {\n if (!job) return;\n\n const runJobId = job.data?.runJobId;\n const isFinalAttempt = job.attemptsMade >= (job.opts.attempts ?? 1);\n\n logger.error(`${name} job failed`, {\n bullJobId: job.id,\n runJobId,\n attempt: job.attemptsMade,\n finalAttempt: isFinalAttempt,\n error: err.message,\n });\n\n if (!runJobId) return;\n\n await prisma.runJob\n .update({\n where: { id: runJobId },\n data: {\n error: err.message,\n ...(isFinalAttempt\n ? { status: 'FAILED', finishedAt: new Date() }\n : {}),\n },\n })\n .catch((updateError) => {\n // P2025 = record not found. The RunJob row is gone (orphaned job from a\n // shared/reset Redis), so there's nothing to flag as FAILED — log\n // quietly instead of as an error to avoid noise.\n if (\n updateError instanceof Prisma.PrismaClientKnownRequestError &&\n updateError.code === 'P2025'\n ) {\n logger.warn(`${name} failure not recorded; RunJob no longer exists`, {\n runJobId,\n });\n return;\n }\n logger.error(`Failed to record ${name} failure on RunJob`, {\n runJobId,\n error:\n updateError instanceof Error\n ? updateError.message\n : String(updateError),\n });\n });\n });\n\n worker.on('completed', (job) => {\n logger.info(`${name} job completed`, {\n bullJobId: job.id,\n runJobId: job.data?.runJobId,\n });\n });\n\n return worker;\n}\n"],"names":[],"mappings":"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","debug_id":"411f71f1-9246-5cac-bbd7-d82a51d25576"}
1
+ {"version":3,"file":"createRunJobWorker.js","sources":["workers/processors/createRunJobWorker.ts"],"sourceRoot":"/","sourcesContent":["/**\n * `createRunJobWorker` — shared factory for every BullMQ processor in this app.\n *\n * Wraps the boilerplate that every RunJob-backed worker needs:\n * - Idempotent skip if the RunJob already SUCCEEDED (recovery after a crash\n * that lost the BullMQ ack).\n * - Status transitions: PENDING/RUNNING → RUNNING (with attempts++) → SUCCEEDED.\n * - Failure handling: record latest error every attempt, flip to FAILED only\n * on the final retry.\n * - Standardised structured logging on `error` / `failed` / `completed`.\n *\n * A processor implementation only has to provide its `RunJobType`, optional\n * BullMQ options (concurrency, limiter, etc.), and a `run` callback that does\n * the actual work. The callback receives the typed `JobInputs[type]` payload\n * and a context object with `runJobId`, `bullJobId`, `attempt`, plus the\n * `userId` / `classId` from the original `enqueueRunJob` call.\n */\nimport { Worker, type Job, type WorkerOptions } from 'bullmq';\nimport { Prisma, type RunJobType } from '@prisma/client';\nimport { bullConnection, getBullPrefix } from '../connection.js';\nimport { prisma } from '../../lib/prisma.js';\nimport { logger } from '../../utils/logger.js';\nimport { QUEUE_NAMES, type JobInputs } from '../queues.js';\n\nexport interface RunJobContext {\n runJobId: string;\n bullJobId: string | undefined;\n /** 1-indexed attempt number of the current run. */\n attempt: number;\n /** From `EnqueueOptions.userId` on the producer side. */\n userId: string | null;\n /** From `EnqueueOptions.classId` on the producer side. */\n classId: string | null;\n}\n\nexport interface CreateRunJobWorkerOptions<T extends RunJobType> {\n type: T;\n /** BullMQ `WorkerOptions` minus `connection`, which is supplied by the helper. */\n workerOptions?: Omit<WorkerOptions, 'connection'>;\n /**\n * The actual work. Return any JSON-serialisable value (or `null`) — it gets\n * persisted to `RunJob.result` for the client to read back.\n */\n run: (\n input: JobInputs[T],\n ctx: RunJobContext,\n ) => Promise<Prisma.InputJsonValue | null>;\n}\n\ntype WorkerData<T extends RunJobType> = JobInputs[T] & { runJobId: string };\n\nexport function createRunJobWorker<T extends RunJobType>({\n type,\n workerOptions,\n run,\n}: CreateRunJobWorkerOptions<T>): Worker<WorkerData<T>> {\n const name = QUEUE_NAMES[type];\n\n const worker = new Worker<WorkerData<T>>(\n name,\n async (job: Job<WorkerData<T>>) => {\n const { runJobId, ...rest } = job.data;\n // The destructured rest is `Omit<JobInputs[T] & { runJobId }, 'runJobId'>`,\n // which TS can't simplify back to `JobInputs[T]` while `T` is generic.\n const input = rest as unknown as JobInputs[T];\n\n const existing = await prisma.runJob.findUnique({\n where: { id: runJobId },\n select: { status: true, result: true, userId: true, classId: true },\n });\n\n if (!existing) {\n // Orphaned job: the RunJob row this job points at does not exist in\n // THIS database. Typically caused by a Redis instance shared across\n // environments/databases (the `prefix` namespacing in this file's\n // Worker options is the primary guard against that), or by a DB reset\n // that left jobs behind in Redis. Discard instead of throwing on the\n // `update` below and burning all retry attempts.\n logger.warn(`${name} RunJob not found; discarding orphaned job`, {\n runJobId,\n bullJobId: job.id,\n });\n return null;\n }\n\n if (existing.status === 'SUCCEEDED') {\n logger.info(`${name} RunJob already succeeded; skipping re-run`, {\n runJobId,\n bullJobId: job.id,\n });\n return existing.result;\n }\n\n if (existing.status === 'CANCELLED') {\n logger.info(`${name} RunJob cancelled before start; skipping`, {\n runJobId,\n bullJobId: job.id,\n });\n return existing.result;\n }\n\n await prisma.runJob.update({\n where: { id: runJobId },\n data: {\n status: 'RUNNING',\n startedAt: existing.status === 'RUNNING' ? undefined : new Date(),\n attempts: { increment: 1 },\n },\n });\n\n const result = await run(input, {\n runJobId,\n bullJobId: job.id,\n attempt: job.attemptsMade + 1,\n userId: existing.userId ?? null,\n classId: existing.classId ?? null,\n });\n\n // `updateMany` with a status guard so a cancellation that landed while\n // the run was in flight is not overwritten with SUCCEEDED.\n await prisma.runJob.updateMany({\n where: { id: runJobId, status: { not: 'CANCELLED' } },\n data: {\n status: 'SUCCEEDED',\n finishedAt: new Date(),\n error: null,\n result: result === null ? Prisma.JsonNull : result,\n },\n });\n\n return result;\n },\n { connection: bullConnection, prefix: getBullPrefix(), ...workerOptions },\n );\n\n worker.on('error', (err) => {\n logger.error(`${name} worker error`, {\n error: err.message,\n stack: err.stack,\n });\n });\n\n worker.on('failed', async (job, err) => {\n if (!job) return;\n\n const runJobId = job.data?.runJobId;\n const isFinalAttempt = job.attemptsMade >= (job.opts.attempts ?? 1);\n\n logger.error(`${name} job failed`, {\n bullJobId: job.id,\n runJobId,\n attempt: job.attemptsMade,\n finalAttempt: isFinalAttempt,\n error: err.message,\n });\n\n if (!runJobId) return;\n\n // Status guard: don't let a late failure (e.g. from an aborted stream)\n // overwrite a RunJob the user already cancelled.\n await prisma.runJob\n .updateMany({\n where: { id: runJobId, status: { not: 'CANCELLED' } },\n data: {\n error: err.message,\n ...(isFinalAttempt\n ? { status: 'FAILED' as const, finishedAt: new Date() }\n : {}),\n },\n })\n .catch((updateError) => {\n logger.error(`Failed to record ${name} failure on RunJob`, {\n runJobId,\n error:\n updateError instanceof Error\n ? updateError.message\n : String(updateError),\n });\n });\n });\n\n worker.on('completed', (job) => {\n logger.info(`${name} job completed`, {\n bullJobId: job.id,\n runJobId: job.data?.runJobId,\n });\n });\n\n return worker;\n}\n"],"names":[],"mappings":"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","debug_id":"c443bbb7-d5db-544b-a259-dc4d70dde58b"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studious-lms/server",
3
- "version": "1.25.0",
3
+ "version": "1.27.0",
4
4
  "description": "Backend server for Studious application",
5
5
  "main": "dist/exportType.js",
6
6
  "types": "dist/exportType.d.ts",
@@ -0,0 +1,100 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "CourseModuleItemType" AS ENUM ('ASSIGNMENT', 'WORKSHEET', 'FILE', 'ANNOUNCEMENT', 'DISCUSSION', 'EXTERNAL_URL', 'SUB_HEADER');
3
+
4
+ -- CreateEnum
5
+ CREATE TYPE "ModuleItemRequirementType" AS ENUM ('VIEW', 'SUBMIT', 'SCORE_AT_LEAST');
6
+
7
+ -- CreateEnum
8
+ CREATE TYPE "ModuleItemProgressStatus" AS ENUM ('INCOMPLETE', 'COMPLETE');
9
+
10
+ -- CreateTable
11
+ CREATE TABLE "Module" (
12
+ "id" TEXT NOT NULL,
13
+ "classId" TEXT NOT NULL,
14
+ "title" TEXT NOT NULL,
15
+ "position" INTEGER NOT NULL,
16
+ "unlockAt" TIMESTAMP(3),
17
+ "requireSequential" BOOLEAN NOT NULL DEFAULT false,
18
+ "published" BOOLEAN NOT NULL DEFAULT false,
19
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
20
+ "updatedAt" TIMESTAMP(3) NOT NULL,
21
+
22
+ CONSTRAINT "Module_pkey" PRIMARY KEY ("id")
23
+ );
24
+
25
+ -- CreateTable
26
+ CREATE TABLE "ModulePrerequisite" (
27
+ "id" TEXT NOT NULL,
28
+ "moduleId" TEXT NOT NULL,
29
+ "prerequisiteId" TEXT NOT NULL,
30
+
31
+ CONSTRAINT "ModulePrerequisite_pkey" PRIMARY KEY ("id")
32
+ );
33
+
34
+ -- CreateTable
35
+ CREATE TABLE "CourseModuleItem" (
36
+ "id" TEXT NOT NULL,
37
+ "moduleId" TEXT NOT NULL,
38
+ "position" INTEGER NOT NULL,
39
+ "indent" INTEGER NOT NULL DEFAULT 0,
40
+ "itemType" "CourseModuleItemType" NOT NULL,
41
+ "itemId" TEXT,
42
+ "externalUrl" TEXT,
43
+ "title" TEXT,
44
+ "requirementType" "ModuleItemRequirementType",
45
+ "requiredScore" DOUBLE PRECISION,
46
+
47
+ CONSTRAINT "CourseModuleItem_pkey" PRIMARY KEY ("id")
48
+ );
49
+
50
+ -- CreateTable
51
+ CREATE TABLE "ModuleItemProgress" (
52
+ "id" TEXT NOT NULL,
53
+ "moduleItemId" TEXT NOT NULL,
54
+ "studentId" TEXT NOT NULL,
55
+ "status" "ModuleItemProgressStatus" NOT NULL DEFAULT 'INCOMPLETE',
56
+ "completedAt" TIMESTAMP(3),
57
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
58
+ "updatedAt" TIMESTAMP(3) NOT NULL,
59
+
60
+ CONSTRAINT "ModuleItemProgress_pkey" PRIMARY KEY ("id")
61
+ );
62
+
63
+ -- CreateIndex
64
+ CREATE INDEX "Module_classId_position_idx" ON "Module"("classId", "position");
65
+
66
+ -- CreateIndex
67
+ CREATE INDEX "ModulePrerequisite_prerequisiteId_idx" ON "ModulePrerequisite"("prerequisiteId");
68
+
69
+ -- CreateIndex
70
+ CREATE UNIQUE INDEX "ModulePrerequisite_moduleId_prerequisiteId_key" ON "ModulePrerequisite"("moduleId", "prerequisiteId");
71
+
72
+ -- CreateIndex
73
+ CREATE INDEX "CourseModuleItem_moduleId_position_idx" ON "CourseModuleItem"("moduleId", "position");
74
+
75
+ -- CreateIndex
76
+ CREATE INDEX "CourseModuleItem_itemType_itemId_idx" ON "CourseModuleItem"("itemType", "itemId");
77
+
78
+ -- CreateIndex
79
+ CREATE INDEX "ModuleItemProgress_studentId_idx" ON "ModuleItemProgress"("studentId");
80
+
81
+ -- CreateIndex
82
+ CREATE UNIQUE INDEX "ModuleItemProgress_moduleItemId_studentId_key" ON "ModuleItemProgress"("moduleItemId", "studentId");
83
+
84
+ -- AddForeignKey
85
+ ALTER TABLE "Module" ADD CONSTRAINT "Module_classId_fkey" FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE CASCADE ON UPDATE CASCADE;
86
+
87
+ -- AddForeignKey
88
+ ALTER TABLE "ModulePrerequisite" ADD CONSTRAINT "ModulePrerequisite_moduleId_fkey" FOREIGN KEY ("moduleId") REFERENCES "Module"("id") ON DELETE CASCADE ON UPDATE CASCADE;
89
+
90
+ -- AddForeignKey
91
+ ALTER TABLE "ModulePrerequisite" ADD CONSTRAINT "ModulePrerequisite_prerequisiteId_fkey" FOREIGN KEY ("prerequisiteId") REFERENCES "Module"("id") ON DELETE CASCADE ON UPDATE CASCADE;
92
+
93
+ -- AddForeignKey
94
+ ALTER TABLE "CourseModuleItem" ADD CONSTRAINT "CourseModuleItem_moduleId_fkey" FOREIGN KEY ("moduleId") REFERENCES "Module"("id") ON DELETE CASCADE ON UPDATE CASCADE;
95
+
96
+ -- AddForeignKey
97
+ ALTER TABLE "ModuleItemProgress" ADD CONSTRAINT "ModuleItemProgress_moduleItemId_fkey" FOREIGN KEY ("moduleItemId") REFERENCES "CourseModuleItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
98
+
99
+ -- AddForeignKey
100
+ ALTER TABLE "ModuleItemProgress" ADD CONSTRAINT "ModuleItemProgress_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,7 @@
1
+ -- Per-message thumbs up/down feedback on AI (Newton) responses.
2
+
3
+ -- CreateEnum
4
+ CREATE TYPE "AiMessageFeedback" AS ENUM ('UP', 'DOWN');
5
+
6
+ -- AlterTable
7
+ ALTER TABLE "Message" ADD COLUMN "aiFeedback" "AiMessageFeedback";
@@ -328,6 +328,7 @@ model User {
328
328
  discussions Discussion[]
329
329
  discussionReplies DiscussionReply[]
330
330
  discussionReads DiscussionRead[]
331
+ moduleItemProgress ModuleItemProgress[]
331
332
  customGradeEntries CustomGradeEntry[] @relation("UserCustomGradeEntries")
332
333
  fileAnnotations FileAnnotation[] @relation("AuthoredFileAnnotations")
333
334
  marketplaceListings MarketplaceListing[] @relation("MarketplaceListingAuthor")
@@ -428,6 +429,8 @@ model Class {
428
429
  studentsCanCreateDiscussions Boolean @default(true)
429
430
  studentsCanReply Boolean @default(true)
430
431
 
432
+ modules Module[]
433
+
431
434
  /// Teacher-controlled ClassSidebar visibility. Keys match nav hrefs
432
435
  /// without leading slash (e.g. "assignments", "quizzes"). Null = all defaults on.
433
436
  sidebarFeatures Json?
@@ -1115,6 +1118,11 @@ model ConversationMember {
1115
1118
  @@unique([userId, conversationId])
1116
1119
  }
1117
1120
 
1121
+ enum AiMessageFeedback {
1122
+ UP
1123
+ DOWN
1124
+ }
1125
+
1118
1126
  model Message {
1119
1127
  id String @id @default(uuid())
1120
1128
  content String
@@ -1124,6 +1132,9 @@ model Message {
1124
1132
  /// UI-shaped status. NULL for messages with no AI lifecycle (normal user DMs).
1125
1133
  /// Operational/queue state (retries, errors, attempts) lives on `runJob`.
1126
1134
  status JobStatus?
1135
+ /// Thumbs up/down left by the conversation member on an AI response.
1136
+ /// NULL for non-AI messages and unrated AI messages.
1137
+ aiFeedback AiMessageFeedback?
1127
1138
  /// Link to the background job that's generating / generated the AI response
1128
1139
  /// for this message. NULL for non-AI messages.
1129
1140
  runJobId String?
@@ -1478,6 +1489,96 @@ model Permission {
1478
1489
  @@index([schoolId])
1479
1490
  }
1480
1491
 
1492
+ enum CourseModuleItemType {
1493
+ ASSIGNMENT
1494
+ WORKSHEET
1495
+ FILE
1496
+ ANNOUNCEMENT
1497
+ DISCUSSION
1498
+ EXTERNAL_URL
1499
+ SUB_HEADER
1500
+ }
1501
+
1502
+ enum ModuleItemRequirementType {
1503
+ VIEW
1504
+ SUBMIT
1505
+ SCORE_AT_LEAST
1506
+ }
1507
+
1508
+ enum ModuleItemProgressStatus {
1509
+ INCOMPLETE
1510
+ COMPLETE
1511
+ }
1512
+
1513
+ /// Canvas-style course module: an ordered group of content items with
1514
+ /// optional prerequisites, unlock date and sequential progression.
1515
+ model Module {
1516
+ id String @id @default(uuid())
1517
+ classId String
1518
+ class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
1519
+ title String
1520
+ position Int
1521
+ unlockAt DateTime?
1522
+ requireSequential Boolean @default(false)
1523
+ published Boolean @default(false)
1524
+ createdAt DateTime @default(now())
1525
+ updatedAt DateTime @updatedAt
1526
+
1527
+ items CourseModuleItem[]
1528
+ prerequisites ModulePrerequisite[] @relation("ModulePrerequisites")
1529
+ requiredBy ModulePrerequisite[] @relation("ModuleRequiredBy")
1530
+
1531
+ @@index([classId, position])
1532
+ }
1533
+
1534
+ model ModulePrerequisite {
1535
+ id String @id @default(uuid())
1536
+ moduleId String
1537
+ module Module @relation("ModulePrerequisites", fields: [moduleId], references: [id], onDelete: Cascade)
1538
+ prerequisiteId String
1539
+ prerequisite Module @relation("ModuleRequiredBy", fields: [prerequisiteId], references: [id], onDelete: Cascade)
1540
+
1541
+ @@unique([moduleId, prerequisiteId])
1542
+ @@index([prerequisiteId])
1543
+ }
1544
+
1545
+ model CourseModuleItem {
1546
+ id String @id @default(uuid())
1547
+ moduleId String
1548
+ module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade)
1549
+ position Int
1550
+ indent Int @default(0)
1551
+ itemType CourseModuleItemType
1552
+ /// ID of the referenced assignment / worksheet / file / announcement /
1553
+ /// discussion. Null for EXTERNAL_URL and SUB_HEADER items.
1554
+ itemId String?
1555
+ externalUrl String?
1556
+ /// Display title override; required for EXTERNAL_URL and SUB_HEADER.
1557
+ title String?
1558
+ requirementType ModuleItemRequirementType?
1559
+ requiredScore Float?
1560
+
1561
+ progress ModuleItemProgress[]
1562
+
1563
+ @@index([moduleId, position])
1564
+ @@index([itemType, itemId])
1565
+ }
1566
+
1567
+ model ModuleItemProgress {
1568
+ id String @id @default(uuid())
1569
+ moduleItemId String
1570
+ moduleItem CourseModuleItem @relation(fields: [moduleItemId], references: [id], onDelete: Cascade)
1571
+ studentId String
1572
+ student User @relation(fields: [studentId], references: [id], onDelete: Cascade)
1573
+ status ModuleItemProgressStatus @default(INCOMPLETE)
1574
+ completedAt DateTime?
1575
+ createdAt DateTime @default(now())
1576
+ updatedAt DateTime @updatedAt
1577
+
1578
+ @@unique([moduleItemId, studentId])
1579
+ @@index([studentId])
1580
+ }
1581
+
1481
1582
  // ---------------------------------------------------------------------------
1482
1583
  // A2 — Monthly student fees + manual payment ledger (Nepal pilot)
1483
1584
  // Billing period = FeeInvoice.year + FeeInvoice.month (no separate term table;
@@ -77,6 +77,25 @@ export async function findOrCreateScopeRoot(scope: CloudScope) {
77
77
  }
78
78
  }
79
79
 
80
+ /** Find a direct child folder by name, creating it if missing. */
81
+ export async function findOrCreateChildFolderByName(
82
+ parentFolderId: string,
83
+ name: string
84
+ ) {
85
+ const existing = await prisma.folder.findFirst({
86
+ where: { parentFolderId, name },
87
+ select: folderSelect,
88
+ });
89
+ if (existing) return existing;
90
+ return prisma.folder.create({
91
+ data: {
92
+ name,
93
+ parentFolder: { connect: { id: parentFolderId } },
94
+ },
95
+ select: folderSelect,
96
+ });
97
+ }
98
+
80
99
  /**
81
100
  * Walk up the folder tree until we reach a root with a scope owner. Used to
82
101
  * permission-check sub-folders (which only carry a parentFolderId).
@@ -122,6 +122,40 @@ export function findLabChatById(labChatId: string) {
122
122
  });
123
123
  }
124
124
 
125
+ /** @returns Class context for starter prompt suggestions (teacher only). */
126
+ export function findClassContextForStarterPrompts(
127
+ classId: string,
128
+ userId: string
129
+ ) {
130
+ return prisma.class.findFirst({
131
+ where: {
132
+ id: classId,
133
+ ...classTeacherOrSchoolAdminWhere(userId),
134
+ },
135
+ select: {
136
+ id: true,
137
+ name: true,
138
+ subject: true,
139
+ sections: {
140
+ select: { name: true },
141
+ orderBy: { order: "asc" as const },
142
+ take: 5,
143
+ },
144
+ assignments: {
145
+ where: { template: false },
146
+ select: {
147
+ title: true,
148
+ type: true,
149
+ dueDate: true,
150
+ inProgress: true,
151
+ },
152
+ orderBy: { createdAt: "desc" as const },
153
+ take: 10,
154
+ },
155
+ },
156
+ });
157
+ }
158
+
125
159
  /** @returns Class if user is student, teacher, or school admin. */
126
160
  export function findClassMembership(classId: string, userId: string) {
127
161
  return prisma.class.findFirst({
@@ -135,6 +135,22 @@ export async function searchMarketplaceListings(input: {
135
135
  return { items: rows, nextCursor };
136
136
  }
137
137
 
138
+ export async function updateMarketplaceListing(
139
+ id: string,
140
+ data: {
141
+ title?: string;
142
+ description?: string | null;
143
+ visibility?: MarketplaceVisibility;
144
+ tags?: string[];
145
+ }
146
+ ) {
147
+ return prisma.marketplaceListing.update({
148
+ where: { id },
149
+ data,
150
+ include: marketplaceListingInclude,
151
+ });
152
+ }
153
+
138
154
  export async function incrementMarketplaceReportCount(id: string) {
139
155
  return prisma.marketplaceListing.update({
140
156
  where: { id },