@studious-lms/server 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +28 -0
- package/.env.example +5 -0
- package/.github/workflows/deploy.yml +123 -0
- package/Dockerfile +54 -0
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/instrument.js +3 -3
- package/dist/instrument.js.map +1 -1
- package/dist/lib/config/cors.d.ts.map +1 -1
- package/dist/lib/config/cors.js +31 -11
- package/dist/lib/config/cors.js.map +1 -1
- package/dist/lib/config/env.d.ts +17 -0
- package/dist/lib/config/env.d.ts.map +1 -1
- package/dist/lib/config/env.js +8 -2
- package/dist/lib/config/env.js.map +1 -1
- package/dist/lib/googleCloudStorage.d.ts +10 -0
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +22 -2
- package/dist/lib/googleCloudStorage.js.map +1 -1
- package/dist/lib/inferDocumentFormat.d.ts +10 -0
- package/dist/lib/inferDocumentFormat.d.ts.map +1 -0
- package/dist/lib/inferDocumentFormat.js +48 -0
- package/dist/lib/inferDocumentFormat.js.map +1 -0
- package/dist/lib/legal.d.ts +3 -0
- package/dist/lib/legal.d.ts.map +1 -0
- package/dist/lib/legal.js +6 -0
- package/dist/lib/legal.js.map +1 -0
- package/dist/lib/prisma.d.ts +104 -5
- package/dist/lib/prisma.d.ts.map +1 -1
- package/dist/lib/prisma.js +33 -2
- package/dist/lib/prisma.js.map +1 -1
- package/dist/models/agenda.d.ts +20 -20
- package/dist/models/announcement.d.ts +211 -101
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/assignment.d.ts +560 -362
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/attendance.d.ts +197 -71
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/auth.d.ts +356 -114
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +4 -2
- package/dist/models/auth.js.map +1 -1
- package/dist/models/class.d.ts +327 -201
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +2 -2
- package/dist/models/class.js.map +1 -1
- package/dist/models/cloudStorage.d.ts +146 -56
- package/dist/models/cloudStorage.d.ts.map +1 -1
- package/dist/models/comment.d.ts +262 -110
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/conversation.d.ts +206 -80
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/department.d.ts +196 -70
- package/dist/models/department.d.ts.map +1 -1
- package/dist/models/event.d.ts +305 -141
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/file.d.ts +422 -272
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/folder.d.ts +433 -181
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +205 -79
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/marketing.d.ts +79 -38
- package/dist/models/marketing.d.ts.map +1 -1
- package/dist/models/marketing.js +12 -4
- package/dist/models/marketing.js.map +1 -1
- package/dist/models/message.d.ts +97 -43
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/newtonChat.d.ts +84 -30
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/notification.d.ts +57 -21
- package/dist/models/notification.d.ts.map +1 -1
- package/dist/models/school.d.ts +349 -133
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/section.d.ts +216 -72
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/tag.d.ts +156 -48
- package/dist/models/tag.d.ts.map +1 -1
- package/dist/models/user.d.ts +82 -28
- package/dist/models/user.d.ts.map +1 -1
- package/dist/models/worksheet.d.ts +1032 -423
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +8 -2
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts +7 -9
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +194 -125
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +233 -389
- package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
- package/dist/pipelines/aiLabChatContract.js +54 -70
- package/dist/pipelines/aiLabChatContract.js.map +1 -1
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +41 -41
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +8 -8
- package/dist/pipelines/labChatPrompt.d.ts +18 -11
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
- package/dist/pipelines/labChatPrompt.js +93 -75
- package/dist/pipelines/labChatPrompt.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +1 -1
- package/dist/routers/_app.d.ts +2601 -2151
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/admin.d.ts +13 -13
- package/dist/routers/agenda.d.ts +18 -18
- package/dist/routers/announcement.d.ts +60 -60
- package/dist/routers/assignment.d.ts +393 -383
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/attendance.d.ts +10 -10
- package/dist/routers/auth.d.ts +4 -2
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +15 -2
- package/dist/routers/auth.js.map +1 -1
- package/dist/routers/class.d.ts +157 -153
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/comment.d.ts +18 -18
- package/dist/routers/conversation.d.ts +11 -11
- package/dist/routers/department.d.ts +34 -34
- package/dist/routers/discussion.d.ts +68 -56
- package/dist/routers/discussion.d.ts.map +1 -1
- package/dist/routers/discussion.js +6 -2
- package/dist/routers/discussion.js.map +1 -1
- package/dist/routers/embedding.d.ts +73 -0
- package/dist/routers/embedding.d.ts.map +1 -0
- package/dist/routers/embedding.js +151 -0
- package/dist/routers/embedding.js.map +1 -0
- package/dist/routers/event.d.ts +57 -55
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/file.d.ts +14 -14
- package/dist/routers/folder.d.ts +38 -38
- package/dist/routers/labChat.d.ts +16 -16
- package/dist/routers/marketing.d.ts +1 -14
- package/dist/routers/marketing.d.ts.map +1 -1
- package/dist/routers/message.d.ts +13 -13
- package/dist/routers/newtonChat.d.ts +4 -4
- package/dist/routers/notifications.d.ts +5 -5
- package/dist/routers/permissions.d.ts +14 -14
- package/dist/routers/school.d.ts +59 -55
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/search.d.ts +22 -1
- package/dist/routers/search.d.ts.map +1 -1
- package/dist/routers/search.js +9 -3
- package/dist/routers/search.js.map +1 -1
- package/dist/routers/section.d.ts +4 -4
- package/dist/routers/studentProgress.d.ts +1 -1
- package/dist/routers/tag.d.ts +7 -7
- package/dist/routers/user.d.ts +99 -10
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +19 -2
- package/dist/routers/user.js.map +1 -1
- package/dist/routers/worksheet.d.ts +93 -55
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +26 -2
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/routes/aiStream.d.ts +2 -0
- package/dist/routes/aiStream.d.ts.map +1 -0
- package/dist/routes/aiStream.js +726 -0
- package/dist/routes/aiStream.js.map +1 -0
- package/dist/seedDatabase.d.ts +3 -1
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/seedDatabase.js +6 -6
- package/dist/seedDatabase.js.map +1 -1
- package/dist/services/adminInstitution.d.ts +8 -8
- package/dist/services/adminRunJob.d.ts +4 -4
- package/dist/services/agenda.d.ts +18 -18
- package/dist/services/aiChatStream.d.ts +88 -0
- package/dist/services/aiChatStream.d.ts.map +1 -0
- package/dist/services/aiChatStream.js +254 -0
- package/dist/services/aiChatStream.js.map +1 -0
- package/dist/services/aiModifierTools.d.ts +16 -0
- package/dist/services/aiModifierTools.d.ts.map +1 -0
- package/dist/services/aiModifierTools.js +274 -0
- package/dist/services/aiModifierTools.js.map +1 -0
- package/dist/services/aiProposalTools.d.ts +32 -0
- package/dist/services/aiProposalTools.d.ts.map +1 -0
- package/dist/services/aiProposalTools.js +207 -0
- package/dist/services/aiProposalTools.js.map +1 -0
- package/dist/services/aiStream.d.ts +22 -0
- package/dist/services/aiStream.d.ts.map +1 -0
- package/dist/services/aiStream.js +142 -0
- package/dist/services/aiStream.js.map +1 -0
- package/dist/services/aiTools.d.ts +16 -0
- package/dist/services/aiTools.d.ts.map +1 -0
- package/dist/services/aiTools.js +271 -0
- package/dist/services/aiTools.js.map +1 -0
- package/dist/services/announcement.d.ts +42 -42
- package/dist/services/announcement.d.ts.map +1 -1
- package/dist/services/announcement.js +7 -2
- package/dist/services/announcement.js.map +1 -1
- package/dist/services/assignment.d.ts +355 -345
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +30 -9
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts +10 -10
- package/dist/services/auth.d.ts +4 -2
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/auth.js +13 -4
- package/dist/services/auth.js.map +1 -1
- package/dist/services/class.d.ts +146 -142
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +24 -4
- package/dist/services/class.js.map +1 -1
- package/dist/services/comment.d.ts +18 -18
- package/dist/services/conversation.d.ts +11 -11
- package/dist/services/department.d.ts +33 -33
- package/dist/services/discussion.d.ts +58 -58
- package/dist/services/discussion.d.ts.map +1 -1
- package/dist/services/discussion.js +15 -8
- package/dist/services/discussion.js.map +1 -1
- package/dist/services/discussionSummary.d.ts +21 -0
- package/dist/services/discussionSummary.d.ts.map +1 -0
- package/dist/services/discussionSummary.js +179 -0
- package/dist/services/discussionSummary.js.map +1 -0
- package/dist/services/embedding.d.ts +85 -0
- package/dist/services/embedding.d.ts.map +1 -0
- package/dist/services/embedding.js +372 -0
- package/dist/services/embedding.js.map +1 -0
- package/dist/services/event.d.ts +55 -53
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/file.d.ts +28 -28
- package/dist/services/folder.d.ts +36 -36
- package/dist/services/folder.d.ts.map +1 -1
- package/dist/services/folder.js +32 -2
- package/dist/services/folder.js.map +1 -1
- package/dist/services/labChat.d.ts +16 -16
- package/dist/services/marketing.d.ts +2 -15
- package/dist/services/marketing.d.ts.map +1 -1
- package/dist/services/marketing.js +5 -5
- package/dist/services/marketing.js.map +1 -1
- package/dist/services/message.d.ts +11 -11
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +2 -2
- package/dist/services/message.js.map +1 -1
- package/dist/services/newtonChat.d.ts +4 -4
- package/dist/services/notification.d.ts +5 -5
- package/dist/services/notification.d.ts.map +1 -1
- package/dist/services/openaiDocumentGen.d.ts +37 -0
- package/dist/services/openaiDocumentGen.d.ts.map +1 -0
- package/dist/services/openaiDocumentGen.js +262 -0
- package/dist/services/openaiDocumentGen.js.map +1 -0
- package/dist/services/permissions.d.ts +14 -14
- package/dist/services/school.d.ts +54 -50
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/search.d.ts +13 -1
- package/dist/services/search.d.ts.map +1 -1
- package/dist/services/search.js +145 -2
- package/dist/services/search.js.map +1 -1
- package/dist/services/section.d.ts +3 -3
- package/dist/services/tag.d.ts +4 -4
- package/dist/services/user.d.ts +4 -4
- package/dist/services/user.d.ts.map +1 -1
- package/dist/services/user.js +10 -15
- package/dist/services/user.js.map +1 -1
- package/dist/services/userPrivacy.d.ts +86 -0
- package/dist/services/userPrivacy.d.ts.map +1 -0
- package/dist/services/userPrivacy.js +184 -0
- package/dist/services/userPrivacy.js.map +1 -0
- package/dist/services/worksheet.d.ts +86 -57
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +57 -5
- package/dist/services/worksheet.js.map +1 -1
- package/dist/utils/email.d.ts.map +1 -1
- package/dist/utils/email.js +7 -3
- package/dist/utils/email.js.map +1 -1
- package/dist/utils/streamBroadcast.d.ts +11 -0
- package/dist/utils/streamBroadcast.d.ts.map +1 -0
- package/dist/utils/streamBroadcast.js +32 -0
- package/dist/utils/streamBroadcast.js.map +1 -0
- package/dist/workers/connection.d.ts +15 -0
- package/dist/workers/connection.d.ts.map +1 -1
- package/dist/workers/connection.js +20 -2
- package/dist/workers/connection.js.map +1 -1
- package/dist/workers/index.js +4 -2
- package/dist/workers/index.js.map +1 -1
- package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -1
- package/dist/workers/processors/createRunJobWorker.js +31 -8
- package/dist/workers/processors/createRunJobWorker.js.map +1 -1
- package/dist/workers/processors/embedContent.d.ts +8 -0
- package/dist/workers/processors/embedContent.d.ts.map +1 -0
- package/dist/workers/processors/embedContent.js +67 -0
- package/dist/workers/processors/embedContent.js.map +1 -0
- package/dist/workers/queues.d.ts +6 -0
- package/dist/workers/queues.d.ts.map +1 -1
- package/dist/workers/queues.js +5 -3
- package/dist/workers/queues.js.map +1 -1
- package/package.json +5 -1
- package/prisma/migrations/20260609215135_init/migration.sql +1412 -0
- package/prisma/migrations/20260610130000_add_worksheet_tags/migration.sql +13 -0
- package/prisma/migrations/20260610140000_expand_embedding_source_types/migration.sql +4 -0
- package/prisma/migrations/20260611120000_add_user_terms_acceptance/migration.sql +3 -0
- package/prisma/schema.prisma +44 -7
- package/src/index.ts +4 -0
- package/src/instrument.ts +1 -1
- package/src/lib/config/cors.ts +32 -9
- package/src/lib/config/env.ts +6 -0
- package/src/lib/googleCloudStorage.ts +21 -0
- package/src/lib/inferDocumentFormat.ts +58 -0
- package/src/lib/legal.ts +2 -0
- package/src/lib/prisma.ts +40 -1
- package/src/models/auth.ts +4 -0
- package/src/models/class.ts +3 -2
- package/src/models/marketing.ts +10 -2
- package/src/models/worksheet.ts +5 -2
- package/src/pipelines/aiLabChat.ts +245 -146
- package/src/pipelines/aiLabChatContract.ts +59 -69
- package/src/pipelines/aiNewtonChat.ts +46 -43
- package/src/pipelines/labChatPrompt.ts +115 -90
- package/src/routers/_app.ts +2 -0
- package/src/routers/auth.ts +13 -0
- package/src/routers/discussion.ts +7 -0
- package/src/routers/embedding.ts +164 -0
- package/src/routers/search.ts +10 -1
- package/src/routers/user.ts +26 -0
- package/src/routers/worksheet.ts +27 -0
- package/src/routes/aiStream.ts +827 -0
- package/src/seedDatabase.ts +4 -4
- package/src/services/aiChatStream.ts +341 -0
- package/src/services/aiModifierTools.ts +341 -0
- package/src/services/aiProposalTools.ts +274 -0
- package/src/services/aiStream.ts +175 -0
- package/src/services/aiTools.ts +290 -0
- package/src/services/announcement.ts +7 -0
- package/src/services/assignment.ts +31 -7
- package/src/services/auth.ts +14 -2
- package/src/services/class.ts +27 -2
- package/src/services/discussion.ts +22 -8
- package/src/services/discussionSummary.ts +226 -0
- package/src/services/embedding.ts +446 -0
- package/src/services/folder.ts +31 -0
- package/src/services/marketing.ts +3 -3
- package/src/services/message.ts +2 -2
- package/src/services/openaiDocumentGen.ts +319 -0
- package/src/services/search.ts +163 -1
- package/src/services/user.ts +9 -17
- package/src/services/userPrivacy.ts +196 -0
- package/src/services/worksheet.ts +77 -3
- package/src/utils/email.ts +5 -1
- package/src/utils/streamBroadcast.ts +48 -0
- package/src/workers/connection.ts +19 -0
- package/src/workers/index.ts +2 -0
- package/src/workers/processors/createRunJobWorker.ts +32 -6
- package/src/workers/processors/embedContent.ts +86 -0
- package/src/workers/queues.ts +5 -1
- package/tests/helpers/registerInput.ts +20 -0
- package/tests/integration/globalSetup.ts +10 -18
- package/tests/integration/routers/auth.test.ts +13 -18
- package/tests/unit/lib/inferDocumentFormat.test.ts +33 -0
- package/tests/unit/pipelines/aiLabChatContract.test.ts +38 -21
- package/tests/unit/pipelines/labChatPrompt.test.ts +23 -36
- package/tests/unit/services/aiChatStream.test.ts +92 -0
- package/tests/unit/services/openaiDocumentGen.test.ts +65 -0
- package/prisma/migrations/20250807062924_init/migration.sql +0 -436
- package/prisma/migrations/20250919063704_init2/migration.sql +0 -24
- package/prisma/migrations/20250920143543_add_profile_fields/migration.sql +0 -15
- package/prisma/migrations/20250922165147_add_chat_models/migration.sql +0 -68
- package/prisma/migrations/20250922165314_add_chat_roles/migration.sql +0 -5
- package/prisma/migrations/20250922171242_refactor_notification_system/migration.sql +0 -20
- package/prisma/migrations/20250922172208_add_mentions_system/migration.sql +0 -21
- package/prisma/migrations/20250925072732_add_lab_chat_system/migration.sql +0 -25
- package/prisma/migrations/20251020151505_add_upload_tracking_fields/migration.sql +0 -57
- package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +0 -30
- package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +0 -35
- package/prisma/migrations/20260410124000_add_submission_recommendation_state/migration.sql +0 -14
- package/prisma/migrations/20260415103500_add_session_type/migration.sql +0 -33
- package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +0 -19
- package/prisma/migrations/20260417083000_add_conversation_member_hidden_at/migration.sql +0 -2
- package/prisma/migrations/20260502230500_assignment_submission_comments/migration.sql +0 -60
- package/prisma/migrations/20260504071500_school_memberships/migration.sql +0 -81
- package/prisma/migrations/20260506162500_consolidate_run_job_and_unify_job_status/migration.sql +0 -166
- package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +0 -184
- package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +0 -93
- package/prisma/migrations/20260510183000_add_discussions/migration.sql +0 -151
- /package/.github/workflows/{npm.yml → npm-publish.yml} +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
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 { streamText, stepCountIs, type ModelMessage } from 'ai';
|
|
6
|
+
import { openai } from '@ai-sdk/openai';
|
|
7
|
+
import { getRedis } from '../lib/redis.js';
|
|
8
|
+
import { prisma } from '../lib/prisma.js';
|
|
9
|
+
import { pusher, chatChannel } from '../lib/pusher.js';
|
|
10
|
+
import { getAIUserId, ensureAIUserExists } from '../utils/aiUser.js';
|
|
11
|
+
import { logger } from '../utils/logger.js';
|
|
12
|
+
import { JobStatus } from '@prisma/client';
|
|
13
|
+
import { aiTools, type ToolCallResult } from './aiTools.js';
|
|
14
|
+
|
|
15
|
+
const STREAM_REDIS_TTL = 600; // 10 minutes TTL for stream keys
|
|
16
|
+
const STREAM_KEY_PREFIX = 'stream:';
|
|
17
|
+
|
|
18
|
+
export interface StreamOptions {
|
|
19
|
+
conversationId: string;
|
|
20
|
+
aiMessageId: string;
|
|
21
|
+
messages: ModelMessage[];
|
|
22
|
+
systemPrompt: string;
|
|
23
|
+
classId?: string;
|
|
24
|
+
enableTools?: boolean;
|
|
25
|
+
model?: string;
|
|
26
|
+
temperature?: number;
|
|
27
|
+
maxOutputTokens?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Stream an AI response, broadcasting chunks via Pusher and caching in Redis.
|
|
32
|
+
* On completion, persists the full response to the database.
|
|
33
|
+
*/
|
|
34
|
+
export async function streamAIResponse(options: StreamOptions): Promise<void> {
|
|
35
|
+
const {
|
|
36
|
+
conversationId,
|
|
37
|
+
aiMessageId,
|
|
38
|
+
messages,
|
|
39
|
+
systemPrompt,
|
|
40
|
+
classId,
|
|
41
|
+
enableTools = true,
|
|
42
|
+
model = 'gpt-4o-mini',
|
|
43
|
+
temperature = 0.7,
|
|
44
|
+
maxOutputTokens = 4096,
|
|
45
|
+
} = options;
|
|
46
|
+
|
|
47
|
+
await ensureAIUserExists();
|
|
48
|
+
|
|
49
|
+
const redis = getRedis();
|
|
50
|
+
const streamKey = `${STREAM_KEY_PREFIX}${aiMessageId}`;
|
|
51
|
+
|
|
52
|
+
// Initialize Redis stream cache
|
|
53
|
+
if (redis) {
|
|
54
|
+
await redis.set(streamKey, '', 'EX', STREAM_REDIS_TTL);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let fullContent = '';
|
|
58
|
+
const toolCallResults: ToolCallResult[] = [];
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const toolSet = enableTools && classId ? aiTools(classId) : undefined;
|
|
62
|
+
|
|
63
|
+
const result = streamText({
|
|
64
|
+
model: openai(model),
|
|
65
|
+
system: systemPrompt,
|
|
66
|
+
messages,
|
|
67
|
+
tools: toolSet,
|
|
68
|
+
temperature,
|
|
69
|
+
maxOutputTokens,
|
|
70
|
+
stopWhen: stepCountIs(5),
|
|
71
|
+
onStepFinish: async ({ toolCalls, toolResults }) => {
|
|
72
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
73
|
+
for (const tc of toolCalls) {
|
|
74
|
+
try {
|
|
75
|
+
await pusher.trigger(chatChannel(conversationId), 'message-tool-call', {
|
|
76
|
+
messageId: aiMessageId,
|
|
77
|
+
toolName: tc.toolName,
|
|
78
|
+
status: 'executing',
|
|
79
|
+
});
|
|
80
|
+
} catch (err) {
|
|
81
|
+
logger.error('Failed to broadcast tool call event', { err, aiMessageId });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (toolResults) {
|
|
87
|
+
for (const tr of toolResults) {
|
|
88
|
+
toolCallResults.push({
|
|
89
|
+
toolName: tr.toolName,
|
|
90
|
+
result: tr.output,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Stream text chunks to client
|
|
98
|
+
for await (const chunk of result.textStream) {
|
|
99
|
+
fullContent += chunk;
|
|
100
|
+
|
|
101
|
+
// Cache in Redis
|
|
102
|
+
if (redis) {
|
|
103
|
+
await redis.append(streamKey, chunk);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Broadcast chunk to client via Pusher
|
|
107
|
+
try {
|
|
108
|
+
await pusher.trigger(chatChannel(conversationId), 'message-stream-chunk', {
|
|
109
|
+
messageId: aiMessageId,
|
|
110
|
+
chunk,
|
|
111
|
+
accumulated: fullContent.length,
|
|
112
|
+
});
|
|
113
|
+
} catch (err) {
|
|
114
|
+
logger.warn('Failed to broadcast stream chunk', { err, aiMessageId });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Stream complete — persist to DB
|
|
119
|
+
const meta = toolCallResults.length > 0
|
|
120
|
+
? JSON.stringify({ toolCalls: toolCallResults })
|
|
121
|
+
: undefined;
|
|
122
|
+
|
|
123
|
+
await prisma.message.update({
|
|
124
|
+
where: { id: aiMessageId },
|
|
125
|
+
data: {
|
|
126
|
+
content: fullContent,
|
|
127
|
+
status: JobStatus.COMPLETED,
|
|
128
|
+
...(meta ? { meta: JSON.parse(meta) } : {}),
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Broadcast final message-updated event
|
|
133
|
+
await pusher.trigger(chatChannel(conversationId), 'message-updated', {
|
|
134
|
+
id: aiMessageId,
|
|
135
|
+
content: fullContent,
|
|
136
|
+
senderId: getAIUserId(),
|
|
137
|
+
conversationId,
|
|
138
|
+
status: JobStatus.COMPLETED,
|
|
139
|
+
sender: {
|
|
140
|
+
id: getAIUserId(),
|
|
141
|
+
username: 'Newton_AI',
|
|
142
|
+
profile: { displayName: 'Newton AI', profilePicture: null },
|
|
143
|
+
},
|
|
144
|
+
mentionedUserIds: [],
|
|
145
|
+
meta: meta ? JSON.parse(meta) : {},
|
|
146
|
+
attachments: [],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
logger.info('AI stream completed', {
|
|
150
|
+
aiMessageId,
|
|
151
|
+
conversationId,
|
|
152
|
+
contentLength: fullContent.length,
|
|
153
|
+
toolCalls: toolCallResults.length,
|
|
154
|
+
});
|
|
155
|
+
} catch (error) {
|
|
156
|
+
logger.error('AI stream failed', { error, aiMessageId, conversationId });
|
|
157
|
+
|
|
158
|
+
await prisma.message.update({
|
|
159
|
+
where: { id: aiMessageId },
|
|
160
|
+
data: { status: JobStatus.FAILED },
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await pusher.trigger(chatChannel(conversationId), 'message-updated', {
|
|
164
|
+
id: aiMessageId,
|
|
165
|
+
conversationId,
|
|
166
|
+
status: JobStatus.FAILED,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
throw error;
|
|
170
|
+
} finally {
|
|
171
|
+
if (redis) {
|
|
172
|
+
await redis.del(streamKey);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
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 { jsonSchema, type ToolSet } from 'ai';
|
|
7
|
+
import OpenAI from 'openai';
|
|
8
|
+
import { searchEmbeddings } from './embedding.js';
|
|
9
|
+
import { prisma } from '../lib/prisma.js';
|
|
10
|
+
import { logger } from '../utils/logger.js';
|
|
11
|
+
|
|
12
|
+
export interface ToolCallResult {
|
|
13
|
+
toolName: string;
|
|
14
|
+
result: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build the tool set for a given class context.
|
|
19
|
+
* Tools are scoped to a specific class for security.
|
|
20
|
+
*/
|
|
21
|
+
export function aiTools(classId: string): ToolSet {
|
|
22
|
+
return {
|
|
23
|
+
search_materials: {
|
|
24
|
+
description: 'Search class materials, files, assignments, worksheets, and announcements using semantic similarity. Use this to find relevant context for answering questions.',
|
|
25
|
+
inputSchema: jsonSchema({
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
query: { type: 'string', description: 'The search query to find relevant materials' },
|
|
29
|
+
limit: { type: 'number', description: 'Maximum results (1-10)', default: 5 },
|
|
30
|
+
},
|
|
31
|
+
required: ['query'],
|
|
32
|
+
}),
|
|
33
|
+
execute: async ({ query, limit = 5 }: { query: string; limit?: number }) => {
|
|
34
|
+
try {
|
|
35
|
+
const results = await searchEmbeddings({ classId, query, limit });
|
|
36
|
+
return results.map((r) => ({
|
|
37
|
+
sourceType: r.sourceType,
|
|
38
|
+
sourceId: r.sourceId,
|
|
39
|
+
content: r.content.slice(0, 500),
|
|
40
|
+
similarity: Math.round(r.similarity * 100) / 100,
|
|
41
|
+
metadata: r.metadata,
|
|
42
|
+
}));
|
|
43
|
+
} catch (error) {
|
|
44
|
+
logger.error('search_materials tool failed', { error, classId, query });
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
search_assignments: {
|
|
51
|
+
description: 'Search existing assignments by title, instructions, or type. Returns assignment details including due dates and submission types.',
|
|
52
|
+
inputSchema: jsonSchema({
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
query: { type: 'string', description: 'Search query for assignments' },
|
|
56
|
+
limit: { type: 'number', description: 'Maximum results (1-10)', default: 5 },
|
|
57
|
+
},
|
|
58
|
+
required: ['query'],
|
|
59
|
+
}),
|
|
60
|
+
execute: async ({ query, limit = 5 }: { query: string; limit?: number }) => {
|
|
61
|
+
try {
|
|
62
|
+
const results = await searchEmbeddings({
|
|
63
|
+
classId,
|
|
64
|
+
query,
|
|
65
|
+
limit,
|
|
66
|
+
sourceTypes: ['ASSIGNMENT'],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const assignmentIds = results.map((r) => r.sourceId);
|
|
70
|
+
const assignments = await prisma.assignment.findMany({
|
|
71
|
+
where: { id: { in: assignmentIds } },
|
|
72
|
+
select: {
|
|
73
|
+
id: true,
|
|
74
|
+
title: true,
|
|
75
|
+
instructions: true,
|
|
76
|
+
type: true,
|
|
77
|
+
dueDate: true,
|
|
78
|
+
maxGrade: true,
|
|
79
|
+
section: { select: { name: true } },
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return assignments.map((a) => ({
|
|
84
|
+
id: a.id,
|
|
85
|
+
title: a.title,
|
|
86
|
+
instructions: a.instructions?.slice(0, 200) ?? null,
|
|
87
|
+
type: a.type,
|
|
88
|
+
dueDate: a.dueDate.toISOString(),
|
|
89
|
+
maxGrade: a.maxGrade,
|
|
90
|
+
section: a.section?.name ?? null,
|
|
91
|
+
}));
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logger.error('search_assignments tool failed', { error, classId, query });
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
get_student_progress: {
|
|
100
|
+
description: 'Get progress summary for a specific student in this class, including grades and submission status.',
|
|
101
|
+
inputSchema: jsonSchema({
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
studentId: { type: 'string', description: 'The student user ID' },
|
|
105
|
+
},
|
|
106
|
+
required: ['studentId'],
|
|
107
|
+
}),
|
|
108
|
+
execute: async ({ studentId }: { studentId: string }) => {
|
|
109
|
+
try {
|
|
110
|
+
const submissions = await prisma.submission.findMany({
|
|
111
|
+
where: {
|
|
112
|
+
studentId,
|
|
113
|
+
assignment: { classId },
|
|
114
|
+
},
|
|
115
|
+
select: {
|
|
116
|
+
id: true,
|
|
117
|
+
gradeReceived: true,
|
|
118
|
+
submittedAt: true,
|
|
119
|
+
assignment: {
|
|
120
|
+
select: {
|
|
121
|
+
title: true,
|
|
122
|
+
maxGrade: true,
|
|
123
|
+
type: true,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
orderBy: { submittedAt: 'desc' },
|
|
128
|
+
take: 20,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const totalAssignments = await prisma.assignment.count({
|
|
132
|
+
where: { classId },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const submitted = submissions.filter((s) => s.submittedAt);
|
|
136
|
+
const graded = submissions.filter((s) => s.gradeReceived !== null);
|
|
137
|
+
const avgGrade = graded.length > 0
|
|
138
|
+
? graded.reduce((sum, s) => sum + (s.gradeReceived || 0), 0) / graded.length
|
|
139
|
+
: null;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
studentId,
|
|
143
|
+
totalAssignments,
|
|
144
|
+
submittedCount: submitted.length,
|
|
145
|
+
gradedCount: graded.length,
|
|
146
|
+
averageGrade: avgGrade ? Math.round(avgGrade * 10) / 10 : null,
|
|
147
|
+
recentSubmissions: submissions.slice(0, 5).map((s) => ({
|
|
148
|
+
assignment: s.assignment.title,
|
|
149
|
+
gradeReceived: s.gradeReceived,
|
|
150
|
+
maxGrade: s.assignment.maxGrade,
|
|
151
|
+
submittedAt: s.submittedAt?.toISOString() ?? null,
|
|
152
|
+
})),
|
|
153
|
+
};
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.error('get_student_progress tool failed', { error, classId, studentId });
|
|
156
|
+
return { error: 'Failed to fetch student progress' };
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
get_worksheet_details: {
|
|
162
|
+
description: 'Get full details of a worksheet including all questions, answers, and mark schemes.',
|
|
163
|
+
inputSchema: jsonSchema({
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
worksheetId: { type: 'string', description: 'The worksheet ID to look up' },
|
|
167
|
+
},
|
|
168
|
+
required: ['worksheetId'],
|
|
169
|
+
}),
|
|
170
|
+
execute: async ({ worksheetId }: { worksheetId: string }) => {
|
|
171
|
+
try {
|
|
172
|
+
const worksheet = await prisma.worksheet.findUnique({
|
|
173
|
+
where: { id: worksheetId, classId },
|
|
174
|
+
include: {
|
|
175
|
+
questions: {
|
|
176
|
+
orderBy: { order: 'asc' },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!worksheet) {
|
|
182
|
+
return { error: 'Worksheet not found' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
id: worksheet.id,
|
|
187
|
+
name: worksheet.name,
|
|
188
|
+
questions: worksheet.questions.map((q) => ({
|
|
189
|
+
id: q.id,
|
|
190
|
+
type: q.type,
|
|
191
|
+
question: q.question,
|
|
192
|
+
answer: q.answer,
|
|
193
|
+
options: q.options,
|
|
194
|
+
points: q.points,
|
|
195
|
+
order: q.order,
|
|
196
|
+
})),
|
|
197
|
+
};
|
|
198
|
+
} catch (error) {
|
|
199
|
+
logger.error('get_worksheet_details tool failed', { error, classId, worksheetId });
|
|
200
|
+
return { error: 'Failed to fetch worksheet' };
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
get_mark_scheme: {
|
|
206
|
+
description: 'Get a mark scheme (rubric) by ID, including criteria and point allocations.',
|
|
207
|
+
inputSchema: jsonSchema({
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {
|
|
210
|
+
markSchemeId: { type: 'string', description: 'The mark scheme ID' },
|
|
211
|
+
},
|
|
212
|
+
required: ['markSchemeId'],
|
|
213
|
+
}),
|
|
214
|
+
execute: async ({ markSchemeId }: { markSchemeId: string }) => {
|
|
215
|
+
try {
|
|
216
|
+
const markScheme = await prisma.markScheme.findUnique({
|
|
217
|
+
where: { id: markSchemeId },
|
|
218
|
+
select: { id: true, name: true, structured: true },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (!markScheme) {
|
|
222
|
+
return { error: 'Mark scheme not found' };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let parsed: unknown = {};
|
|
226
|
+
try { parsed = JSON.parse(markScheme.structured || '{}'); } catch { /* noop */ }
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
id: markScheme.id,
|
|
230
|
+
name: markScheme.name,
|
|
231
|
+
structured: parsed,
|
|
232
|
+
};
|
|
233
|
+
} catch (error) {
|
|
234
|
+
logger.error('get_mark_scheme tool failed', { error, classId, markSchemeId });
|
|
235
|
+
return { error: 'Failed to fetch mark scheme' };
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
web_search: {
|
|
241
|
+
description: 'Search the web for current information on a topic. Use this when you need up-to-date facts, definitions, explanations, or context beyond the class materials.',
|
|
242
|
+
inputSchema: jsonSchema({
|
|
243
|
+
type: 'object',
|
|
244
|
+
properties: {
|
|
245
|
+
query: { type: 'string', description: 'The search query to look up on the web' },
|
|
246
|
+
},
|
|
247
|
+
required: ['query'],
|
|
248
|
+
}),
|
|
249
|
+
execute: async ({ query }: { query: string }) => {
|
|
250
|
+
try {
|
|
251
|
+
const client = new OpenAI();
|
|
252
|
+
const response = await client.responses.create({
|
|
253
|
+
model: 'gpt-4o-mini',
|
|
254
|
+
tools: [{ type: 'web_search' }],
|
|
255
|
+
input: query,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const messageOutput = response.output.find(
|
|
259
|
+
(item) => item.type === 'message',
|
|
260
|
+
);
|
|
261
|
+
if (!messageOutput || messageOutput.type !== 'message') {
|
|
262
|
+
return { summary: 'No results found.', sources: [] };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const textContent = messageOutput.content.find(
|
|
266
|
+
(c) => c.type === 'output_text',
|
|
267
|
+
);
|
|
268
|
+
if (!textContent || textContent.type !== 'output_text') {
|
|
269
|
+
return { summary: 'No results found.', sources: [] };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const sources = textContent.annotations
|
|
273
|
+
.filter(
|
|
274
|
+
(a): a is Extract<typeof a, { type: 'url_citation' }> =>
|
|
275
|
+
a.type === 'url_citation',
|
|
276
|
+
)
|
|
277
|
+
.map((a) => ({ url: a.url, title: a.title }));
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
summary: textContent.text.slice(0, 2000),
|
|
281
|
+
sources: sources.slice(0, 5),
|
|
282
|
+
};
|
|
283
|
+
} catch (error) {
|
|
284
|
+
logger.error('web_search tool failed', { error, query });
|
|
285
|
+
return { summary: 'Web search failed. Please try again.', sources: [] };
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
@@ -16,6 +16,7 @@ import { createDirectUploadFiles, type DirectUploadFile } from "../lib/fileUploa
|
|
|
16
16
|
import { deleteFile } from "../lib/googleCloudStorage.js";
|
|
17
17
|
import { sendToMultiple } from "./notification.js";
|
|
18
18
|
import { logger } from "../utils/logger.js";
|
|
19
|
+
import { enqueueEmbedding } from "./embedding.js";
|
|
19
20
|
|
|
20
21
|
export async function getAllAnnouncements(classId: string) {
|
|
21
22
|
const announcements = await findAnnouncementsByClassId(classId);
|
|
@@ -127,6 +128,9 @@ export async function createAnnouncementRecord(
|
|
|
127
128
|
logger.error("Failed to send announcement notifications:", error);
|
|
128
129
|
});
|
|
129
130
|
|
|
131
|
+
// Auto-index announcement for RAG search
|
|
132
|
+
enqueueEmbedding(classId, 'ANNOUNCEMENT', announcement.id);
|
|
133
|
+
|
|
130
134
|
return {
|
|
131
135
|
announcement: announcementWithAttachments || announcement,
|
|
132
136
|
uploadFiles: directUploadFiles.length > 0 ? directUploadFiles : undefined,
|
|
@@ -263,6 +267,9 @@ export async function updateAnnouncementRecord(
|
|
|
263
267
|
},
|
|
264
268
|
});
|
|
265
269
|
|
|
270
|
+
// Re-index announcement for RAG search after update
|
|
271
|
+
enqueueEmbedding(input.classId, 'ANNOUNCEMENT', input.id);
|
|
272
|
+
|
|
266
273
|
return {
|
|
267
274
|
announcement: updatedAnnouncement,
|
|
268
275
|
uploadFiles: directUploadFiles.length > 0 ? directUploadFiles : undefined,
|
|
@@ -27,6 +27,7 @@ import { sendToMultiple } from "./notification.js";
|
|
|
27
27
|
import { enqueueRunJob } from "../workers/queues.js";
|
|
28
28
|
import { logger } from "../utils/logger.js";
|
|
29
29
|
import { invalidateStudentProgressInferenceCache } from "./studentProgressInferenceCache.js";
|
|
30
|
+
import { enqueueEmbedding } from "./embedding.js";
|
|
30
31
|
|
|
31
32
|
export async function assignmentExists(id: string) {
|
|
32
33
|
const assignment = await findAssignmentById(id);
|
|
@@ -311,13 +312,18 @@ export async function createAssignmentRecord(
|
|
|
311
312
|
}
|
|
312
313
|
await Promise.all(fileOperations);
|
|
313
314
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
315
|
+
if (!inProgress) {
|
|
316
|
+
sendToMultiple({
|
|
317
|
+
receiverIds: (studentIds && studentIds.length > 0) ? studentIds : classData.students.map((s) => s.id),
|
|
318
|
+
title: `New assignment in ${classData.name}`,
|
|
319
|
+
content: `"${title}" is due ${new Date(dueDate).toLocaleDateString()}.\n\n[View assignment](/class/${classId}/assignments/${assignment.id})`,
|
|
320
|
+
}).catch((error) => {
|
|
321
|
+
logger.error("Failed to send assignment notifications:", error);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Auto-index assignment for RAG search
|
|
326
|
+
enqueueEmbedding(classId, 'ASSIGNMENT', assignment.id);
|
|
321
327
|
|
|
322
328
|
return assignment;
|
|
323
329
|
}
|
|
@@ -591,6 +597,21 @@ export async function updateAssignmentRecord(
|
|
|
591
597
|
.catch((error) => logger.error("Failed to update max grade from rubric:", error));
|
|
592
598
|
}
|
|
593
599
|
|
|
600
|
+
// Re-index assignment for RAG search after update
|
|
601
|
+
enqueueEmbedding(input.classId, 'ASSIGNMENT', input.id);
|
|
602
|
+
if (assignment.inProgress && input.inProgress === false && classData) {
|
|
603
|
+
const receiverIds = input.studentIds?.length
|
|
604
|
+
? input.studentIds
|
|
605
|
+
: classData.students.map((s) => s.id);
|
|
606
|
+
sendToMultiple({
|
|
607
|
+
receiverIds,
|
|
608
|
+
title: `New assignment in ${classData.name}`,
|
|
609
|
+
content: `"${updatedAssignment.title}" is due ${new Date(updatedAssignment.dueDate).toLocaleDateString()}.\n\n[View assignment](/class/${classData.id}/assignments/${updatedAssignment.id})`,
|
|
610
|
+
}).catch((error) => {
|
|
611
|
+
logger.error("Failed to send assignment publish notifications:", error);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
594
615
|
return updatedAssignment;
|
|
595
616
|
}
|
|
596
617
|
|
|
@@ -916,6 +937,9 @@ export async function updateSubmissionAsTeacherRecord(
|
|
|
916
937
|
);
|
|
917
938
|
}
|
|
918
939
|
|
|
940
|
+
// Index submission for RAG search after teacher grades/feedback
|
|
941
|
+
enqueueEmbedding(input.classId, 'SUBMISSION', submissionId);
|
|
942
|
+
|
|
919
943
|
return updated;
|
|
920
944
|
}
|
|
921
945
|
|
package/src/services/auth.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { sendMail } from "../utils/email.js";
|
|
|
10
10
|
import { prismaWrapper } from "../utils/prismaWrapper.js";
|
|
11
11
|
import { env } from "../lib/config/env.js";
|
|
12
12
|
import { logger } from "../utils/logger.js";
|
|
13
|
+
import { CURRENT_TERMS_VERSION } from "../lib/legal.js";
|
|
13
14
|
import {
|
|
14
15
|
findUserByUsername,
|
|
15
16
|
findUserByUsernameOrEmail,
|
|
@@ -32,8 +33,17 @@ export async function register(input: {
|
|
|
32
33
|
username: string;
|
|
33
34
|
email: string;
|
|
34
35
|
password: string;
|
|
36
|
+
agreeToTerms: true;
|
|
37
|
+
termsVersion: string;
|
|
35
38
|
}) {
|
|
36
|
-
const { username, email, password } = input;
|
|
39
|
+
const { username, email, password, termsVersion } = input;
|
|
40
|
+
|
|
41
|
+
if (termsVersion !== CURRENT_TERMS_VERSION) {
|
|
42
|
+
throw new TRPCError({
|
|
43
|
+
code: "BAD_REQUEST",
|
|
44
|
+
message: "Please accept the current Terms of Service and Privacy Policy.",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
37
47
|
|
|
38
48
|
const existingUser = await prismaWrapper.findFirst(
|
|
39
49
|
() => findUserByUsernameOrEmail(username, email),
|
|
@@ -70,7 +80,9 @@ export async function register(input: {
|
|
|
70
80
|
username,
|
|
71
81
|
email,
|
|
72
82
|
password: await hash(password, 10),
|
|
73
|
-
verified:
|
|
83
|
+
verified: false,
|
|
84
|
+
termsAcceptedAt: new Date(),
|
|
85
|
+
termsVersion,
|
|
74
86
|
}),
|
|
75
87
|
"creating new user during registration"
|
|
76
88
|
);
|
package/src/services/class.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { SessionType } from "@prisma/client";
|
|
|
6
6
|
import { TRPCError } from "@trpc/server";
|
|
7
7
|
import { prisma } from "../lib/prisma.js";
|
|
8
8
|
import { generateInviteCode } from "../utils/generateInviteCode.js";
|
|
9
|
+
import { enqueueEmbedding } from "./embedding.js";
|
|
9
10
|
import {
|
|
10
11
|
findTeacherClasses,
|
|
11
12
|
findStudentClasses,
|
|
@@ -27,6 +28,8 @@ import {
|
|
|
27
28
|
} from "../models/class.js";
|
|
28
29
|
import { Class, Folder } from "@prisma/client";
|
|
29
30
|
import { invalidateStudentProgressInferenceCache } from "./studentProgressInferenceCache.js";
|
|
31
|
+
import { sendToMultiple } from "./notification.js";
|
|
32
|
+
import { logger } from "../utils/logger.js";
|
|
30
33
|
|
|
31
34
|
async function requireActiveSchoolMembers(schoolId: string, userIds: string[]) {
|
|
32
35
|
const uniqueUserIds = [...new Set(userIds)];
|
|
@@ -624,10 +627,15 @@ export async function updateSyllabus(classId: string, contents: string) {
|
|
|
624
627
|
message: "Missing key contents",
|
|
625
628
|
});
|
|
626
629
|
}
|
|
627
|
-
|
|
630
|
+
const updated = await prisma.class.update({
|
|
628
631
|
where: { id: classId },
|
|
629
632
|
data: { syllabus: contents },
|
|
630
633
|
});
|
|
634
|
+
|
|
635
|
+
// Index syllabus for RAG search
|
|
636
|
+
enqueueEmbedding(classId, 'SYLLABUS', classId);
|
|
637
|
+
|
|
638
|
+
return updated;
|
|
631
639
|
}
|
|
632
640
|
|
|
633
641
|
export async function listLabDrafts(classId: string, teacherId: string) {
|
|
@@ -748,7 +756,7 @@ export async function publishLabDraft(
|
|
|
748
756
|
});
|
|
749
757
|
}
|
|
750
758
|
|
|
751
|
-
|
|
759
|
+
const published = await prisma.assignment.update({
|
|
752
760
|
where: { id: draftId },
|
|
753
761
|
data: {
|
|
754
762
|
inProgress: false,
|
|
@@ -759,6 +767,23 @@ export async function publishLabDraft(
|
|
|
759
767
|
modifiedAt: new Date(),
|
|
760
768
|
},
|
|
761
769
|
});
|
|
770
|
+
|
|
771
|
+
const classData = await prisma.class.findUnique({
|
|
772
|
+
where: { id: classId },
|
|
773
|
+
select: { name: true, students: { select: { id: true } } },
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
if (classData) {
|
|
777
|
+
sendToMultiple({
|
|
778
|
+
receiverIds: classData.students.map((s) => s.id),
|
|
779
|
+
title: `New assignment in ${classData.name}`,
|
|
780
|
+
content: `"${published.title}" is due ${new Date(published.dueDate).toLocaleDateString()}.\n\n[View assignment](/class/${classId}/assignments/${published.id})`,
|
|
781
|
+
}).catch((error) => {
|
|
782
|
+
logger.error("Failed to send lab draft publish notifications:", error);
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return published;
|
|
762
787
|
}
|
|
763
788
|
|
|
764
789
|
export async function getFiles(classId: string) {
|