@studious-lms/server 1.12.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/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 +177 -69
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/assignment.d.ts +551 -353
- 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 +348 -114
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/class.d.ts +324 -188
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +9 -3
- 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 +269 -110
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/comment.js +3 -2
- package/dist/models/comment.js.map +1 -1
- package/dist/models/conversation.d.ts +206 -63
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/conversation.js +13 -2
- package/dist/models/conversation.js.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 +287 -125
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/file.d.ts +389 -263
- 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 +209 -83
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/marketing.d.ts +80 -26
- package/dist/models/marketing.d.ts.map +1 -1
- package/dist/models/message.d.ts +95 -41
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/newtonChat.d.ts +83 -29
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/notification.d.ts +61 -25
- 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 +1023 -410
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +19 -7
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts +0 -8
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +50 -67
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +393 -388
- package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
- package/dist/pipelines/aiLabChatContract.js +63 -69
- 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/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +59 -14
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/pipelines/labChatPrompt.d.ts +10 -8
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
- package/dist/pipelines/labChatPrompt.js +47 -67
- package/dist/pipelines/labChatPrompt.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +2 -2
- package/dist/routers/_app.d.ts +2268 -1898
- 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 +32 -28
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +6 -5
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +335 -327
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +4 -4
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/attendance.d.ts +10 -10
- package/dist/routers/auth.d.ts +2 -2
- package/dist/routers/class.d.ts +170 -147
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/comment.d.ts +40 -16
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/comment.js +6 -3
- package/dist/routers/comment.js.map +1 -1
- package/dist/routers/conversation.d.ts +17 -17
- package/dist/routers/department.d.ts +34 -34
- package/dist/routers/discussion.d.ts +65 -53
- 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 +69 -0
- package/dist/routers/embedding.d.ts.map +1 -0
- package/dist/routers/embedding.js +143 -0
- package/dist/routers/embedding.js.map +1 -0
- package/dist/routers/event.d.ts +39 -39
- package/dist/routers/file.d.ts +12 -12
- package/dist/routers/folder.d.ts +38 -38
- package/dist/routers/labChat.d.ts +19 -19
- package/dist/routers/marketing.d.ts +2 -2
- package/dist/routers/message.d.ts +13 -13
- package/dist/routers/newtonChat.d.ts +4 -4
- package/dist/routers/notifications.d.ts +9 -9
- package/dist/routers/permissions.d.ts +14 -14
- package/dist/routers/school.d.ts +55 -55
- 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 +10 -10
- package/dist/routers/worksheet.d.ts +97 -56
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +28 -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 +696 -0
- package/dist/routes/aiStream.js.map +1 -0
- package/dist/seedDatabase.d.ts +2 -2
- 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 +68 -0
- package/dist/services/aiChatStream.d.ts.map +1 -0
- package/dist/services/aiChatStream.js +157 -0
- package/dist/services/aiChatStream.js.map +1 -0
- package/dist/services/aiModifierTools.d.ts +32 -0
- package/dist/services/aiModifierTools.d.ts.map +1 -0
- package/dist/services/aiModifierTools.js +165 -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 +12 -12
- 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 +293 -293
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +36 -15
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts +10 -10
- package/dist/services/auth.d.ts +2 -2
- package/dist/services/class.d.ts +161 -138
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +25 -4
- package/dist/services/class.js.map +1 -1
- package/dist/services/comment.d.ts +34 -16
- package/dist/services/comment.d.ts.map +1 -1
- package/dist/services/comment.js +23 -2
- package/dist/services/comment.js.map +1 -1
- package/dist/services/conversation.d.ts +17 -17
- package/dist/services/conversation.d.ts.map +1 -1
- package/dist/services/conversation.js +9 -7
- package/dist/services/conversation.js.map +1 -1
- 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 +8 -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 +67 -0
- package/dist/services/embedding.d.ts.map +1 -0
- package/dist/services/embedding.js +226 -0
- package/dist/services/embedding.js.map +1 -0
- package/dist/services/event.d.ts +37 -37
- package/dist/services/file.d.ts +24 -24
- package/dist/services/folder.d.ts +36 -36
- package/dist/services/labChat.d.ts +19 -19
- package/dist/services/marketing.d.ts +2 -2
- 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 +9 -9
- package/dist/services/notification.d.ts.map +1 -1
- package/dist/services/permissions.d.ts +14 -14
- package/dist/services/school.d.ts +50 -50
- package/dist/services/search.d.ts +12 -0
- package/dist/services/search.d.ts.map +1 -1
- package/dist/services/search.js +120 -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/worksheet.d.ts +62 -47
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +11 -4
- package/dist/services/worksheet.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 +76 -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/schema.prisma +39 -7
- package/src/index.ts +4 -0
- 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/prisma.ts +40 -1
- package/src/models/class.ts +10 -3
- package/src/models/comment.ts +1 -0
- package/src/models/conversation.ts +12 -0
- package/src/models/worksheet.ts +16 -7
- package/src/pipelines/aiLabChat.ts +54 -79
- package/src/pipelines/aiLabChatContract.ts +69 -69
- package/src/pipelines/aiNewtonChat.ts +46 -43
- package/src/pipelines/gradeWorksheet.ts +66 -15
- package/src/pipelines/labChatPrompt.ts +63 -88
- package/src/routers/_app.ts +2 -0
- package/src/routers/announcement.ts +4 -3
- package/src/routers/assignment.ts +2 -2
- package/src/routers/comment.ts +5 -0
- package/src/routers/discussion.ts +7 -0
- package/src/routers/embedding.ts +159 -0
- package/src/routers/search.ts +10 -1
- package/src/routers/worksheet.ts +29 -0
- package/src/routes/aiStream.ts +789 -0
- package/src/seedDatabase.ts +4 -4
- package/src/services/aiChatStream.ts +219 -0
- package/src/services/aiModifierTools.ts +188 -0
- package/src/services/aiProposalTools.ts +272 -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 +36 -14
- package/src/services/class.ts +30 -2
- package/src/services/comment.ts +22 -0
- package/src/services/conversation.ts +9 -4
- package/src/services/discussion.ts +9 -8
- package/src/services/discussionSummary.ts +226 -0
- package/src/services/embedding.ts +285 -0
- package/src/services/message.ts +2 -2
- package/src/services/search.ts +138 -0
- package/src/services/user.ts +9 -17
- package/src/services/worksheet.ts +10 -2
- 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 +90 -0
- package/src/workers/queues.ts +5 -1
- package/tests/integration/routers/conversation.test.ts +9 -7
- package/tests/unit/pipelines/aiLabChatContract.test.ts +38 -21
- package/tests/unit/pipelines/labChatPrompt.test.ts +23 -36
- 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,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lab-chat "proposal" tools.
|
|
3
|
+
*
|
|
4
|
+
* In the agentic Lab pipeline the model produces create-actions by *calling
|
|
5
|
+
* tools* (e.g. `propose_assignment`) instead of returning a JSON blob. Each
|
|
6
|
+
* tool records a structured payload into a shared accumulator and returns a
|
|
7
|
+
* short confirmation so the model can keep narrating. After the stream ends
|
|
8
|
+
* the pipeline turns the accumulator into the `message.meta.*ToCreate` shape
|
|
9
|
+
* the frontend accept-cards already consume — so the data contract is
|
|
10
|
+
* preserved while the generation becomes genuine inline tool calls.
|
|
11
|
+
*
|
|
12
|
+
* NOTE: these tools intentionally use `jsonSchema()` rather than the zod
|
|
13
|
+
* `tool()` helper. Zod-typed `tool()` definitions with deeply nested schemas
|
|
14
|
+
* make the TS compiler instantiate excessively deep types (TS2589) and OOM,
|
|
15
|
+
* so we mirror the existing `aiTools.ts` pattern (plain JSON Schema + manually
|
|
16
|
+
* typed execute args).
|
|
17
|
+
*/
|
|
18
|
+
import { jsonSchema, type ToolSet } from 'ai';
|
|
19
|
+
import { v4 } from 'uuid';
|
|
20
|
+
import type {
|
|
21
|
+
AssignmentToCreate,
|
|
22
|
+
WorksheetToCreate,
|
|
23
|
+
SectionToCreate,
|
|
24
|
+
DocToCreate,
|
|
25
|
+
} from '../pipelines/aiLabChatContract.js';
|
|
26
|
+
import type { DocumentBlock } from '../lib/jsonStyles.js';
|
|
27
|
+
|
|
28
|
+
export interface LabProposals {
|
|
29
|
+
assignments: AssignmentToCreate[];
|
|
30
|
+
worksheets: WorksheetToCreate[];
|
|
31
|
+
sections: SectionToCreate[];
|
|
32
|
+
docs: DocToCreate[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createLabProposals(): LabProposals {
|
|
36
|
+
return { assignments: [], worksheets: [], sections: [], docs: [] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeDueDate(input?: string | null): string {
|
|
40
|
+
if (input) {
|
|
41
|
+
const parsed = new Date(input);
|
|
42
|
+
if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
|
|
43
|
+
}
|
|
44
|
+
// Default: one week out.
|
|
45
|
+
return new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ASSIGNMENT_TYPES = [
|
|
49
|
+
'HOMEWORK', 'QUIZ', 'TEST', 'PROJECT', 'ESSAY', 'DISCUSSION', 'PRESENTATION', 'LAB', 'OTHER',
|
|
50
|
+
] as const;
|
|
51
|
+
type AssignmentType = (typeof ASSIGNMENT_TYPES)[number];
|
|
52
|
+
|
|
53
|
+
const QUESTION_TYPES = [
|
|
54
|
+
'MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'LONG_ANSWER', 'MATH_EXPRESSION', 'ESSAY',
|
|
55
|
+
] as const;
|
|
56
|
+
type QuestionType = (typeof QUESTION_TYPES)[number];
|
|
57
|
+
|
|
58
|
+
interface ProposeSectionArgs {
|
|
59
|
+
name: string;
|
|
60
|
+
color?: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ProposeWorksheetArgs {
|
|
64
|
+
title: string;
|
|
65
|
+
questions: Array<{
|
|
66
|
+
type: QuestionType;
|
|
67
|
+
question: string;
|
|
68
|
+
answer?: string;
|
|
69
|
+
options?: Array<{ text: string; isCorrect: boolean }>;
|
|
70
|
+
markScheme?: Array<{ points: number; description: string }>;
|
|
71
|
+
points?: number;
|
|
72
|
+
}>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ProposeAssignmentArgs {
|
|
76
|
+
title: string;
|
|
77
|
+
instructions: string;
|
|
78
|
+
dueDate?: string;
|
|
79
|
+
type?: AssignmentType;
|
|
80
|
+
maxGrade?: number;
|
|
81
|
+
acceptFiles?: boolean;
|
|
82
|
+
acceptExtendedResponse?: boolean;
|
|
83
|
+
acceptWorksheet?: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface GenerateDocumentArgs {
|
|
87
|
+
title: string;
|
|
88
|
+
blocks: DocumentBlock[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build the set of proposal tools backed by the given accumulator.
|
|
93
|
+
* Merge these with the read-only RAG tools from `aiTools(classId)`.
|
|
94
|
+
*/
|
|
95
|
+
export function labProposalTools(proposals: LabProposals): ToolSet {
|
|
96
|
+
return {
|
|
97
|
+
propose_section: {
|
|
98
|
+
description:
|
|
99
|
+
'Propose a new section (folder) to organise class content. Call once per section you want to create.',
|
|
100
|
+
inputSchema: jsonSchema({
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
name: { type: 'string', description: 'Section name' },
|
|
104
|
+
color: { type: 'string', description: 'Optional hex color like #3b82f6' },
|
|
105
|
+
},
|
|
106
|
+
required: ['name'],
|
|
107
|
+
}),
|
|
108
|
+
execute: async ({ name, color }: ProposeSectionArgs) => {
|
|
109
|
+
proposals.sections.push({ name, color: color ?? null });
|
|
110
|
+
return `Section "${name}" proposed.`;
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
propose_worksheet: {
|
|
115
|
+
description:
|
|
116
|
+
'Propose a worksheet of questions for the teacher to review and create. Include all questions in a single call.',
|
|
117
|
+
inputSchema: jsonSchema({
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties: {
|
|
120
|
+
title: { type: 'string', description: 'Worksheet title' },
|
|
121
|
+
questions: {
|
|
122
|
+
type: 'array',
|
|
123
|
+
description: 'The list of questions',
|
|
124
|
+
items: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
type: { type: 'string', enum: QUESTION_TYPES as unknown as string[] },
|
|
128
|
+
question: { type: 'string' },
|
|
129
|
+
answer: { type: 'string' },
|
|
130
|
+
options: {
|
|
131
|
+
type: 'array',
|
|
132
|
+
description: 'For MULTIPLE_CHOICE/TRUE_FALSE questions',
|
|
133
|
+
items: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
text: { type: 'string' },
|
|
137
|
+
isCorrect: { type: 'boolean' },
|
|
138
|
+
},
|
|
139
|
+
required: ['text', 'isCorrect'],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
markScheme: {
|
|
143
|
+
type: 'array',
|
|
144
|
+
items: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
points: { type: 'number' },
|
|
148
|
+
description: { type: 'string' },
|
|
149
|
+
},
|
|
150
|
+
required: ['points', 'description'],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
points: { type: 'number' },
|
|
154
|
+
},
|
|
155
|
+
required: ['type', 'question'],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
required: ['title', 'questions'],
|
|
160
|
+
}),
|
|
161
|
+
execute: async ({ title, questions }: ProposeWorksheetArgs) => {
|
|
162
|
+
const worksheet: WorksheetToCreate = {
|
|
163
|
+
title,
|
|
164
|
+
questions: (questions ?? []).map((q, index) => ({
|
|
165
|
+
type: q.type,
|
|
166
|
+
question: q.question,
|
|
167
|
+
answer: q.answer ?? '',
|
|
168
|
+
options: (q.options ?? []).map((o) => ({
|
|
169
|
+
id: v4(),
|
|
170
|
+
text: o.text,
|
|
171
|
+
isCorrect: o.isCorrect,
|
|
172
|
+
})),
|
|
173
|
+
markScheme: (q.markScheme ?? []).map((m) => ({
|
|
174
|
+
id: v4(),
|
|
175
|
+
points: m.points,
|
|
176
|
+
description: m.description,
|
|
177
|
+
})),
|
|
178
|
+
points: q.points ?? 0,
|
|
179
|
+
order: index,
|
|
180
|
+
})),
|
|
181
|
+
};
|
|
182
|
+
proposals.worksheets.push(worksheet);
|
|
183
|
+
return `Worksheet "${title}" with ${worksheet.questions.length} question(s) proposed.`;
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
propose_assignment: {
|
|
188
|
+
description:
|
|
189
|
+
'Propose an assignment for the teacher to review and create. Call once per assignment.',
|
|
190
|
+
inputSchema: jsonSchema({
|
|
191
|
+
type: 'object',
|
|
192
|
+
properties: {
|
|
193
|
+
title: { type: 'string' },
|
|
194
|
+
instructions: { type: 'string' },
|
|
195
|
+
dueDate: {
|
|
196
|
+
type: 'string',
|
|
197
|
+
description: 'ISO 8601 datetime. Defaults to one week from now if omitted.',
|
|
198
|
+
},
|
|
199
|
+
type: { type: 'string', enum: ASSIGNMENT_TYPES as unknown as string[] },
|
|
200
|
+
maxGrade: { type: 'number' },
|
|
201
|
+
acceptFiles: { type: 'boolean' },
|
|
202
|
+
acceptExtendedResponse: { type: 'boolean' },
|
|
203
|
+
acceptWorksheet: { type: 'boolean' },
|
|
204
|
+
},
|
|
205
|
+
required: ['title', 'instructions'],
|
|
206
|
+
}),
|
|
207
|
+
execute: async ({
|
|
208
|
+
title,
|
|
209
|
+
instructions,
|
|
210
|
+
dueDate,
|
|
211
|
+
type,
|
|
212
|
+
maxGrade,
|
|
213
|
+
acceptFiles,
|
|
214
|
+
acceptExtendedResponse,
|
|
215
|
+
acceptWorksheet,
|
|
216
|
+
}: ProposeAssignmentArgs) => {
|
|
217
|
+
const assignment: AssignmentToCreate = {
|
|
218
|
+
title,
|
|
219
|
+
instructions,
|
|
220
|
+
dueDate: normalizeDueDate(dueDate),
|
|
221
|
+
type: type ?? 'HOMEWORK',
|
|
222
|
+
maxGrade: maxGrade ?? 100,
|
|
223
|
+
acceptFiles: acceptFiles ?? true,
|
|
224
|
+
acceptExtendedResponse: acceptExtendedResponse ?? false,
|
|
225
|
+
acceptWorksheet: acceptWorksheet ?? false,
|
|
226
|
+
gradingBoundaryId: null,
|
|
227
|
+
markSchemeId: null,
|
|
228
|
+
worksheetIds: [],
|
|
229
|
+
studentIds: [],
|
|
230
|
+
sectionId: null,
|
|
231
|
+
attachments: [],
|
|
232
|
+
};
|
|
233
|
+
proposals.assignments.push(assignment);
|
|
234
|
+
return `Assignment "${title}" proposed.`;
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
generate_document: {
|
|
239
|
+
description:
|
|
240
|
+
'Generate a document (rendered to a PDF and attached to the message). Provide the document title and an ordered list of formatting blocks.',
|
|
241
|
+
inputSchema: jsonSchema({
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties: {
|
|
244
|
+
title: { type: 'string' },
|
|
245
|
+
blocks: {
|
|
246
|
+
type: 'array',
|
|
247
|
+
description: 'Ordered list of document formatting blocks',
|
|
248
|
+
items: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
properties: {
|
|
251
|
+
format: { type: 'number', description: 'Block format id (0-12)' },
|
|
252
|
+
content: {
|
|
253
|
+
description: 'String, or array of strings for list blocks',
|
|
254
|
+
},
|
|
255
|
+
metadata: {
|
|
256
|
+
type: 'object',
|
|
257
|
+
description: 'Optional styling metadata',
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
required: ['format', 'content'],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
required: ['title', 'blocks'],
|
|
265
|
+
}),
|
|
266
|
+
execute: async ({ title, blocks }: GenerateDocumentArgs) => {
|
|
267
|
+
proposals.docs.push({ title, blocks } as unknown as DocToCreate);
|
|
268
|
+
return `Document "${title}" queued for generation.`;
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
@@ -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
|
+
}
|