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