@studious-lms/server 1.2.45 → 1.2.46
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/.env.example +45 -0
- package/.env.test.example +37 -0
- package/README.md +34 -7
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +12110 -0
- package/coverage/coverage-final.json +44 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +221 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/server/index.html +116 -0
- package/coverage/server/src/exportType.ts.html +109 -0
- package/coverage/server/src/index.html +161 -0
- package/coverage/server/src/index.ts.html +1702 -0
- package/coverage/server/src/instrument.ts.html +130 -0
- package/coverage/server/src/lib/config/env.ts.html +448 -0
- package/coverage/server/src/lib/config/index.html +116 -0
- package/coverage/server/src/lib/fileUpload.ts.html +1138 -0
- package/coverage/server/src/lib/googleCloudStorage.ts.html +334 -0
- package/coverage/server/src/lib/index.html +206 -0
- package/coverage/server/src/lib/jsonConversion.ts.html +2323 -0
- package/coverage/server/src/lib/jsonStyles.ts.html +193 -0
- package/coverage/server/src/lib/notificationHandler.ts.html +193 -0
- package/coverage/server/src/lib/pusher.ts.html +121 -0
- package/coverage/server/src/lib/thumbnailGenerator.ts.html +592 -0
- package/coverage/server/src/middleware/auth.ts.html +646 -0
- package/coverage/server/src/middleware/index.html +146 -0
- package/coverage/server/src/middleware/logging.ts.html +244 -0
- package/coverage/server/src/middleware/security.ts.html +271 -0
- package/coverage/server/src/routers/_app.ts.html +232 -0
- package/coverage/server/src/routers/agenda.ts.html +319 -0
- package/coverage/server/src/routers/announcement.ts.html +3481 -0
- package/coverage/server/src/routers/assignment.ts.html +7633 -0
- package/coverage/server/src/routers/attendance.ts.html +1030 -0
- package/coverage/server/src/routers/auth.ts.html +1081 -0
- package/coverage/server/src/routers/class.ts.html +3535 -0
- package/coverage/server/src/routers/comment.ts.html +991 -0
- package/coverage/server/src/routers/conversation.ts.html +982 -0
- package/coverage/server/src/routers/event.ts.html +1609 -0
- package/coverage/server/src/routers/file.ts.html +1144 -0
- package/coverage/server/src/routers/folder.ts.html +2797 -0
- package/coverage/server/src/routers/index.html +386 -0
- package/coverage/server/src/routers/labChat.ts.html +3073 -0
- package/coverage/server/src/routers/marketing.ts.html +340 -0
- package/coverage/server/src/routers/message.ts.html +1912 -0
- package/coverage/server/src/routers/notifications.ts.html +364 -0
- package/coverage/server/src/routers/section.ts.html +1120 -0
- package/coverage/server/src/routers/user.ts.html +862 -0
- package/coverage/server/src/routers/worksheet.ts.html +1729 -0
- package/coverage/server/src/trpc.ts.html +397 -0
- package/coverage/server/src/types/index.html +116 -0
- package/coverage/server/src/types/trpc.ts.html +127 -0
- package/coverage/server/src/utils/aiUser.ts.html +280 -0
- package/coverage/server/src/utils/email.ts.html +121 -0
- package/coverage/server/src/utils/generateInviteCode.ts.html +106 -0
- package/coverage/server/src/utils/index.html +206 -0
- package/coverage/server/src/utils/inference.ts.html +709 -0
- package/coverage/server/src/utils/logger.ts.html +664 -0
- package/coverage/server/src/utils/prismaErrorHandler.ts.html +907 -0
- package/coverage/server/src/utils/prismaWrapper.ts.html +355 -0
- package/coverage/server/vitest.config.ts.html +196 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +83 -52
- package/dist/index.js.map +1 -1
- package/dist/instrument.js +15 -8
- package/dist/instrument.js.map +1 -1
- package/dist/lib/config/env.d.ts +169 -0
- package/dist/lib/config/env.d.ts.map +1 -0
- package/dist/lib/config/env.js +115 -0
- package/dist/lib/config/env.js.map +1 -0
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +5 -4
- package/dist/lib/fileUpload.js.map +1 -1
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +7 -8
- package/dist/lib/googleCloudStorage.js.map +1 -1
- package/dist/lib/jsonConversion.d.ts.map +1 -1
- package/dist/lib/jsonConversion.js +14 -16
- package/dist/lib/jsonConversion.js.map +1 -1
- package/dist/lib/notificationHandler.d.ts +2 -2
- package/dist/lib/prisma.d.ts +2 -2
- package/dist/lib/prisma.d.ts.map +1 -1
- package/dist/lib/prisma.js +22 -3
- package/dist/lib/prisma.js.map +1 -1
- package/dist/lib/pusher.d.ts.map +1 -1
- package/dist/lib/pusher.js +8 -7
- package/dist/lib/pusher.js.map +1 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +6 -5
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/security.d.ts +5 -0
- package/dist/middleware/security.d.ts.map +1 -0
- package/dist/middleware/security.js +77 -0
- package/dist/middleware/security.js.map +1 -0
- package/dist/routers/_app.d.ts +294 -98
- 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/agenda.d.ts.map +1 -1
- package/dist/routers/agenda.js +12 -9
- package/dist/routers/agenda.js.map +1 -1
- package/dist/routers/announcement.d.ts +8 -0
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +6 -4
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +7 -4
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +35 -18
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/attendance.d.ts +1 -0
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +4 -4
- package/dist/routers/attendance.js.map +1 -1
- package/dist/routers/auth.d.ts +20 -0
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +132 -15
- package/dist/routers/auth.js.map +1 -1
- package/dist/routers/class.d.ts +10 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +49 -5
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/comment.d.ts +2 -0
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/conversation.d.ts +1 -0
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +46 -31
- package/dist/routers/conversation.js.map +1 -1
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +30 -7
- package/dist/routers/file.js.map +1 -1
- package/dist/routers/labChat.d.ts +1 -0
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +2 -3
- package/dist/routers/labChat.js.map +1 -1
- package/dist/routers/marketing.d.ts +1 -1
- package/dist/routers/newtonChat.d.ts +55 -0
- package/dist/routers/newtonChat.d.ts.map +1 -0
- package/dist/routers/newtonChat.js +438 -0
- package/dist/routers/newtonChat.js.map +1 -0
- package/dist/routers/notifications.d.ts +4 -4
- package/dist/routers/section.d.ts +9 -4
- package/dist/routers/section.d.ts.map +1 -1
- package/dist/routers/section.js +8 -8
- package/dist/routers/section.js.map +1 -1
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +5 -4
- package/dist/routers/user.js.map +1 -1
- package/dist/routers/worksheet.d.ts +30 -36
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +11 -33
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/seedDatabase.d.ts +1 -1
- package/dist/seedDatabase.js +275 -284
- package/dist/seedDatabase.js.map +1 -1
- package/dist/server/pipelines/aiLabChat.d.ts +10 -0
- package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
- package/dist/server/pipelines/aiLabChat.js +83 -0
- package/dist/server/pipelines/aiLabChat.js.map +1 -0
- package/dist/server/pipelines/gradeWorksheet.d.ts +2 -0
- package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
- package/dist/server/pipelines/gradeWorksheet.js +138 -0
- package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +2 -2
- package/dist/trpc.js.map +1 -1
- package/dist/utils/email.d.ts +9 -1
- package/dist/utils/email.d.ts.map +1 -1
- package/dist/utils/email.js +20 -5
- package/dist/utils/email.js.map +1 -1
- package/dist/utils/inference.d.ts +3 -0
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +41 -7
- package/dist/utils/inference.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +3 -3
- package/dist/utils/logger.js.map +1 -1
- package/docker-compose.yml +14 -0
- package/package.json +13 -4
- package/prisma/schema.prisma +32 -5
- package/scripts/test-pre-push.ts +14 -0
- package/src/index.ts +98 -54
- package/src/instrument.ts +13 -6
- package/src/lib/config/env.ts +126 -0
- package/src/lib/fileUpload.ts +3 -2
- package/src/lib/googleCloudStorage.ts +6 -6
- package/src/lib/jsonConversion.ts +12 -14
- package/src/lib/prisma.ts +23 -2
- package/src/lib/pusher.ts +6 -5
- package/src/middleware/auth.ts +4 -3
- package/src/middleware/security.ts +80 -0
- package/src/routers/_app.ts +2 -0
- package/src/routers/agenda.ts +10 -7
- package/src/routers/announcement.ts +4 -2
- package/src/routers/assignment.ts +58 -40
- package/src/routers/attendance.ts +2 -2
- package/src/routers/auth.ts +143 -14
- package/src/routers/class.ts +52 -3
- package/src/routers/conversation.ts +49 -29
- package/src/routers/file.ts +29 -5
- package/src/routers/labChat.ts +0 -1
- package/src/routers/newtonChat.ts +520 -0
- package/src/routers/section.ts +6 -6
- package/src/routers/user.ts +3 -2
- package/src/routers/worksheet.ts +9 -37
- package/src/seedDatabase.ts +290 -283
- package/src/server/pipelines/aiLabChat.ts +92 -0
- package/src/server/pipelines/gradeWorksheet.ts +152 -0
- package/src/trpc.ts +2 -0
- package/src/utils/email.ts +30 -3
- package/src/utils/inference.ts +50 -5
- package/src/utils/logger.ts +2 -1
- package/tests/announcement.test.ts +164 -0
- package/tests/assignment.test.ts +296 -0
- package/tests/attendance.test.ts +168 -0
- package/tests/auth.test.ts +33 -10
- package/tests/class.test.ts +34 -9
- package/tests/event.test.ts +228 -0
- package/tests/section.test.ts +216 -0
- package/tests/setup.ts +70 -16
- package/tests/user.test.ts +158 -0
- package/vitest.config.ts +26 -0
- package/API_SPECIFICATION.md +0 -1597
- package/BASE64_REMOVAL_SUMMARY.md +0 -164
- package/CHAT_API_SPEC.md +0 -579
- package/LAB_CHAT_API_SPEC.md +0 -518
- package/dist/routers/school.d.ts +0 -208
- package/dist/routers/school.d.ts.map +0 -1
- package/dist/routers/school.js +0 -483
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { getAIUserId } from "../../utils/aiUser";
|
|
2
|
+
import { prisma } from "../../lib/prisma.js";
|
|
3
|
+
import { Assignment, Class, File, GenerationStatus, User } from "@prisma/client";
|
|
4
|
+
import { inference } from "../../utils/inference.js";
|
|
5
|
+
import z from "zod";
|
|
6
|
+
|
|
7
|
+
const aiLabChatResponseSchema = z.object({
|
|
8
|
+
content: z.string(),
|
|
9
|
+
attachments: z.array(z.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
name: z.string(),
|
|
12
|
+
path: z.string(),
|
|
13
|
+
type: z.string(),
|
|
14
|
+
size: z.number(),
|
|
15
|
+
})),
|
|
16
|
+
assignmentsToCreate: z.array(z.object({
|
|
17
|
+
title: z.string(),
|
|
18
|
+
instructions: z.string(),
|
|
19
|
+
dueDate: z.date(),
|
|
20
|
+
acceptFiles: z.boolean(),
|
|
21
|
+
acceptExtendedResponse: z.boolean(),
|
|
22
|
+
acceptWorksheet: z.boolean(),
|
|
23
|
+
maxGrade: z.number(),
|
|
24
|
+
gradingBoundaryId: z.string(),
|
|
25
|
+
markschemeId: z.string(),
|
|
26
|
+
worksheetIds: z.array(z.string()),
|
|
27
|
+
studentIds: z.array(z.string()),
|
|
28
|
+
sectionId: z.string(),
|
|
29
|
+
type: z.enum(['HOMEWORK', 'QUIZ', 'TEST', 'PROJECT', 'ESSAY', 'DISCUSSION', 'PRESENTATION', 'LAB', 'OTHER']),
|
|
30
|
+
attachments: z.array(z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
})),
|
|
33
|
+
})),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
const getBaseSystemPrompt = (context: Class & { members: User[] , assignments: Assignment[], files: File[] }) => {
|
|
38
|
+
const systemPrompt = `
|
|
39
|
+
# Basic Information
|
|
40
|
+
You are a helpful assistant that helps teachers create course materials for their students.
|
|
41
|
+
You are provided with the following context:
|
|
42
|
+
|
|
43
|
+
Class information: ${context.name} - ${context.subject}
|
|
44
|
+
Students: ${JSON.stringify(context.members)}
|
|
45
|
+
Assignments: ${JSON.stringify(context.assignments)}
|
|
46
|
+
Files: ${JSON.stringify(context.files)}
|
|
47
|
+
|
|
48
|
+
You are to generate a response to the user's message.
|
|
49
|
+
If contextually they would like a file, you are to generate a file.
|
|
50
|
+
And so on... same for assignments, worksheets, etc.
|
|
51
|
+
|
|
52
|
+
You are to generate a response in the following format:
|
|
53
|
+
{
|
|
54
|
+
content: string,
|
|
55
|
+
attachments: File[],
|
|
56
|
+
assignmentsToCreate: Assignment[],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
NOTE:
|
|
60
|
+
- for attachments in Assignment, you may only attach to existing files, based on the file ids provided. if you need to create files and assignments, let the user know that this will take two operations.
|
|
61
|
+
- the user must accept your changes before they are applied. do know this.
|
|
62
|
+
-
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate labchat responses
|
|
70
|
+
* Allow for the generation of the following:
|
|
71
|
+
* - Assignment(s) either individual or bulk as an lesson / course plan.
|
|
72
|
+
* - Worksheet(s) either individual or bulk as an lesson / course plan.
|
|
73
|
+
* - Files (PDFs)
|
|
74
|
+
* @param labChatId
|
|
75
|
+
*/
|
|
76
|
+
// export const sendAiLabChatResponsePipeline = async (labChatId: string) => {
|
|
77
|
+
// const message = await prisma?.message.create({
|
|
78
|
+
// data: {
|
|
79
|
+
// content: "GENERATING_CONTENT",
|
|
80
|
+
// senderId: getAIUserId(),
|
|
81
|
+
// conversationId: labChatId,
|
|
82
|
+
// status: GenerationStatus.PENDING,
|
|
83
|
+
// },
|
|
84
|
+
// });
|
|
85
|
+
|
|
86
|
+
// try {
|
|
87
|
+
|
|
88
|
+
// inference(`
|
|
89
|
+
// `)
|
|
90
|
+
// }
|
|
91
|
+
|
|
92
|
+
// };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { GenerationStatus, WorksheetQuestionType } from "@prisma/client";
|
|
2
|
+
import { prisma } from "../../lib/prisma.js";
|
|
3
|
+
import { logger } from "../../utils/logger.js";
|
|
4
|
+
import z from "zod";
|
|
5
|
+
import { inference } from "../../utils/inference.js";
|
|
6
|
+
import { getAIUserId } from "../../utils/aiUser.js";
|
|
7
|
+
import { pusher } from "../../lib/pusher.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Grades and regrades worksheet (can fixed failed responses)
|
|
11
|
+
* @param worksheetResponseId worksheet response id
|
|
12
|
+
* @returns updated worksheet response
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const DO_NOT_INFERENCE_STATUSES = [GenerationStatus.CANCELLED, GenerationStatus.PENDING, GenerationStatus.COMPLETED];
|
|
16
|
+
|
|
17
|
+
export const gradeWorksheetPipeline = async (worksheetResponseId: string) => {
|
|
18
|
+
logger.info('Grading worksheet response', { worksheetResponseId });
|
|
19
|
+
const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
|
|
20
|
+
where: { id: worksheetResponseId },
|
|
21
|
+
include: {
|
|
22
|
+
worksheet: true,
|
|
23
|
+
responses: {
|
|
24
|
+
where: {
|
|
25
|
+
status: {
|
|
26
|
+
not: {
|
|
27
|
+
in: DO_NOT_INFERENCE_STATUSES,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
question: {
|
|
31
|
+
type: {
|
|
32
|
+
not: {
|
|
33
|
+
in: [WorksheetQuestionType.MULTIPLE_CHOICE, WorksheetQuestionType.TRUE_FALSE],
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
include: {
|
|
39
|
+
question: true,
|
|
40
|
+
comments: true,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
console.log(worksheetResponse);
|
|
47
|
+
|
|
48
|
+
if (!worksheetResponse) {
|
|
49
|
+
logger.error('Worksheet response not found');
|
|
50
|
+
throw new Error('Worksheet response not found');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
worksheetResponse.responses.forEach(async (response) => {
|
|
54
|
+
logger.info('Grading question', { questionId: response.questionId });
|
|
55
|
+
const question = response.question;
|
|
56
|
+
const comments = response.comments;
|
|
57
|
+
const responseText = response.response;
|
|
58
|
+
|
|
59
|
+
const studentQuestionProgress = await prisma.studentQuestionProgress.update({
|
|
60
|
+
where: { id: response.id, status: {
|
|
61
|
+
not: {
|
|
62
|
+
in: DO_NOT_INFERENCE_STATUSES,
|
|
63
|
+
}
|
|
64
|
+
} },
|
|
65
|
+
data: { status: GenerationStatus.PENDING },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (studentQuestionProgress.status !== GenerationStatus.PENDING) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const apiResponse = await inference(
|
|
74
|
+
`Grade the following worksheet response:
|
|
75
|
+
|
|
76
|
+
Question: ${question.question}
|
|
77
|
+
Response: ${responseText}
|
|
78
|
+
|
|
79
|
+
Comments: ${comments.map((comment) => comment.content).join('\n')}
|
|
80
|
+
Mark Scheme: ${JSON.stringify(question.markScheme)}
|
|
81
|
+
|
|
82
|
+
Justify your reasoning by including comment(s) and mark the question please.
|
|
83
|
+
Return ONLY JSON in the following format (fill in the values as per the question):
|
|
84
|
+
{
|
|
85
|
+
"isCorrect": <boolean>,
|
|
86
|
+
"points": <number>,
|
|
87
|
+
"markschemeState": [
|
|
88
|
+
{ "id": <string>, "correct": <boolean> }
|
|
89
|
+
],
|
|
90
|
+
"comments": [<string>, ...]
|
|
91
|
+
}
|
|
92
|
+
`,
|
|
93
|
+
z.object({
|
|
94
|
+
isCorrect: z.boolean(),
|
|
95
|
+
points: z.number(),
|
|
96
|
+
markschemeState: z.array(z.object({
|
|
97
|
+
id: z.string(),
|
|
98
|
+
correct: z.boolean(),
|
|
99
|
+
})), // @note: this has to be converted to [id: string]: correct boolean
|
|
100
|
+
comments: z.array(z.string()),
|
|
101
|
+
}),
|
|
102
|
+
).catch((error) => {
|
|
103
|
+
logger.error('Failed to grade worksheet response', { error });
|
|
104
|
+
throw error;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
console.log(apiResponse);
|
|
108
|
+
|
|
109
|
+
const updatedStudentQuestionProgress = await prisma.studentQuestionProgress.update({
|
|
110
|
+
where: { id: studentQuestionProgress.id, status: {
|
|
111
|
+
not: {
|
|
112
|
+
in: ['CANCELLED'],
|
|
113
|
+
},
|
|
114
|
+
} },
|
|
115
|
+
data: {
|
|
116
|
+
status: GenerationStatus.COMPLETED,
|
|
117
|
+
isCorrect: (apiResponse as { isCorrect: boolean }).isCorrect,
|
|
118
|
+
points: (apiResponse as { points: number }).points,
|
|
119
|
+
markschemeState: (apiResponse as {
|
|
120
|
+
markschemeState: { id: string; correct: boolean }[];
|
|
121
|
+
}).markschemeState.reduce((acc, curr) => {
|
|
122
|
+
acc["item-" + curr.id] = curr.correct;
|
|
123
|
+
return acc;
|
|
124
|
+
}, {} as Record<string, boolean>),
|
|
125
|
+
comments: {
|
|
126
|
+
create: (apiResponse as {
|
|
127
|
+
comments: string[];
|
|
128
|
+
}).comments.map((commentContent) => ({
|
|
129
|
+
content: commentContent,
|
|
130
|
+
authorId: getAIUserId(),
|
|
131
|
+
})),
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
pusher.trigger(`class-${worksheetResponse.worksheet.classId}`, `ai-worksheet-updated-${worksheetResponse.id}`, {
|
|
136
|
+
success: true,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return updatedStudentQuestionProgress;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
logger.error('Failed to grade worksheet response', { error, worksheetResponseId });
|
|
142
|
+
pusher.trigger(`class-${worksheetResponse.worksheet.classId}`, `ai-worksheet-updated-${worksheetResponse.id}`, {
|
|
143
|
+
success: false,
|
|
144
|
+
});
|
|
145
|
+
await prisma.studentQuestionProgress.update({
|
|
146
|
+
where: { id: studentQuestionProgress.id },
|
|
147
|
+
data: { status: GenerationStatus.FAILED },
|
|
148
|
+
});
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
};
|
package/src/trpc.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { createAuthMiddleware } from './middleware/auth.js';
|
|
|
7
7
|
import { Request, Response } from 'express';
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import { handlePrismaError, PrismaErrorInfo } from './utils/prismaErrorHandler.js';
|
|
10
|
+
import { generalLimiter } from './middleware/security.js';
|
|
10
11
|
|
|
11
12
|
interface CreateContextOptions {
|
|
12
13
|
req: Request;
|
|
@@ -89,6 +90,7 @@ const { isAuthed, isMemberInClass, isTeacherInClass } = createAuthMiddleware(t);
|
|
|
89
90
|
export const createTRPCRouter = t.router;
|
|
90
91
|
export const publicProcedure = t.procedure.use(loggingMiddleware);
|
|
91
92
|
|
|
93
|
+
|
|
92
94
|
// Protected procedures
|
|
93
95
|
export const protectedProcedure = publicProcedure.use(isAuthed);
|
|
94
96
|
|
package/src/utils/email.ts
CHANGED
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
import nodemailer from 'nodemailer';
|
|
2
|
+
import { env } from '../lib/config/env.js';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
type sendMailProps = {
|
|
7
|
+
from: string;
|
|
8
|
+
to: string;
|
|
9
|
+
subject: string;
|
|
10
|
+
text: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
2
13
|
|
|
3
14
|
export const transport = nodemailer.createTransport({
|
|
4
|
-
host:
|
|
15
|
+
host: env.EMAIL_HOST,
|
|
5
16
|
port: 587,
|
|
6
17
|
secure: false,
|
|
7
18
|
auth: {
|
|
8
|
-
user:
|
|
9
|
-
pass:
|
|
19
|
+
user: env.EMAIL_USER,
|
|
20
|
+
pass: env.EMAIL_PASS,
|
|
10
21
|
},
|
|
11
22
|
});
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
export const sendMail = async ({ from, to, subject, text }: sendMailProps) => {
|
|
26
|
+
// Wrapper function for sending emails
|
|
27
|
+
if (env.EMAIL_DRY_RUN == "true") {
|
|
28
|
+
logger.info(`Email dry run enabled. Would have sent email to ${to} from ${from} with subject ${subject} and text ${text}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await transport.sendMail({
|
|
33
|
+
from,
|
|
34
|
+
to,
|
|
35
|
+
subject,
|
|
36
|
+
text,
|
|
37
|
+
});
|
|
38
|
+
};
|
package/src/utils/inference.ts
CHANGED
|
@@ -3,13 +3,19 @@ import { logger } from './logger.js';
|
|
|
3
3
|
import { prisma } from '../lib/prisma.js';
|
|
4
4
|
import { pusher } from '../lib/pusher.js';
|
|
5
5
|
import { ensureAIUserExists, getAIUserId } from './aiUser.js';
|
|
6
|
+
import { env } from '../lib/config/env.js';
|
|
7
|
+
import { ZodSchema } from 'zod';
|
|
8
|
+
import { zodTextFormat } from "openai/helpers/zod";
|
|
9
|
+
|
|
6
10
|
|
|
7
11
|
|
|
8
12
|
export const inferenceClient = new OpenAI({
|
|
9
|
-
apiKey:
|
|
10
|
-
baseURL:
|
|
13
|
+
apiKey: env.INFERENCE_API_KEY,
|
|
14
|
+
baseURL: env.INFERENCE_API_BASE_URL,
|
|
11
15
|
});
|
|
12
16
|
|
|
17
|
+
export const openAIClient = new OpenAI();
|
|
18
|
+
|
|
13
19
|
// Types for lab chat context
|
|
14
20
|
export interface LabChatContext {
|
|
15
21
|
subject: string;
|
|
@@ -122,6 +128,45 @@ export async function sendAIMessage(
|
|
|
122
128
|
};
|
|
123
129
|
}
|
|
124
130
|
|
|
131
|
+
export async function inference(
|
|
132
|
+
content: string,
|
|
133
|
+
format?: ZodSchema
|
|
134
|
+
): Promise<string | object> {
|
|
135
|
+
try {
|
|
136
|
+
|
|
137
|
+
const completion = await openAIClient.responses.parse({
|
|
138
|
+
model: 'gpt-5-nano',
|
|
139
|
+
input: [
|
|
140
|
+
{
|
|
141
|
+
role: 'user',
|
|
142
|
+
content: content,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
...(format ? { text: {
|
|
146
|
+
format: zodTextFormat(format, "newton_response_format"),
|
|
147
|
+
},
|
|
148
|
+
} : {}),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if (!completion) {
|
|
153
|
+
throw new Error('No response generated from inference API');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// if (format) {
|
|
157
|
+
// if (typeof completion.output === 'string') {
|
|
158
|
+
// return JSON.parse(completion.output);
|
|
159
|
+
// }
|
|
160
|
+
// return JSON.parse(completion.output);
|
|
161
|
+
// }
|
|
162
|
+
|
|
163
|
+
return completion.output_parsed;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
logger.error('Failed to generate inference response', { error });
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
125
170
|
/**
|
|
126
171
|
* Simple inference function for general use
|
|
127
172
|
*/
|
|
@@ -133,10 +178,10 @@ export async function generateInferenceResponse(
|
|
|
133
178
|
maxTokens?: number;
|
|
134
179
|
} = {}
|
|
135
180
|
): Promise<InferenceResponse> {
|
|
136
|
-
const { model = '
|
|
181
|
+
const { model = 'gpt-5-nano', maxTokens = 500 } = options;
|
|
137
182
|
|
|
138
183
|
try {
|
|
139
|
-
const completion = await
|
|
184
|
+
const completion = await openAIClient.chat.completions.create({
|
|
140
185
|
model,
|
|
141
186
|
messages: [
|
|
142
187
|
{
|
|
@@ -176,7 +221,7 @@ export async function generateInferenceResponse(
|
|
|
176
221
|
* Validate inference configuration
|
|
177
222
|
*/
|
|
178
223
|
export function validateInferenceConfig(): boolean {
|
|
179
|
-
if (!
|
|
224
|
+
if (!env.INFERENCE_API_KEY) {
|
|
180
225
|
logger.error('Inference API key not configured for Cohere');
|
|
181
226
|
return false;
|
|
182
227
|
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
export enum LogLevel {
|
|
2
3
|
INFO = 'info',
|
|
3
4
|
WARN = 'warn',
|
|
@@ -59,7 +60,7 @@ class Logger {
|
|
|
59
60
|
// this.isDevelopment = process.env.NODE_ENV === 'development';
|
|
60
61
|
this.isDevelopment = true;
|
|
61
62
|
|
|
62
|
-
this.mode = (process.env.LOG_MODE as LogMode) || 'normal';
|
|
63
|
+
this.mode = (process.env.NODE_ENV === 'test' ? 'silent' : ((process.env.LOG_MODE as LogMode) || 'normal'));
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
this.levelColors = {
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach } from 'vitest';
|
|
2
|
+
import { user1Caller, user2Caller } from './setup';
|
|
3
|
+
import { createTRPCContext } from '../src/trpc';
|
|
4
|
+
import { appRouter } from '../src/routers/_app';
|
|
5
|
+
|
|
6
|
+
describe('Announcement Router', () => {
|
|
7
|
+
let testClass: any;
|
|
8
|
+
let testAnnouncement: any;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
// Create a test class
|
|
12
|
+
testClass = await user1Caller.class.create({
|
|
13
|
+
name: 'Test Class for Announcements',
|
|
14
|
+
subject: 'Mathematics',
|
|
15
|
+
section: '10th Grade',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Create a test announcement
|
|
19
|
+
testAnnouncement = await user1Caller.announcement.create({
|
|
20
|
+
classId: testClass.id,
|
|
21
|
+
remarks: 'This is a test announcement',
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('create', () => {
|
|
26
|
+
test('should create announcement successfully', async () => {
|
|
27
|
+
expect(testAnnouncement).toBeDefined();
|
|
28
|
+
expect(testAnnouncement.announcement).toBeDefined();
|
|
29
|
+
expect(testAnnouncement.announcement.remarks).toBe('This is a test announcement');
|
|
30
|
+
expect(testAnnouncement.announcement.teacher.id).toBeDefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('should create announcement with attachments', async () => {
|
|
34
|
+
const announcement = await user1Caller.announcement.create({
|
|
35
|
+
classId: testClass.id,
|
|
36
|
+
remarks: 'Announcement with attachments',
|
|
37
|
+
files: [
|
|
38
|
+
{
|
|
39
|
+
name: 'test.pdf',
|
|
40
|
+
type: 'application/pdf',
|
|
41
|
+
size: 1024,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(announcement.announcement).toBeDefined();
|
|
47
|
+
expect(announcement.announcement.attachments).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('should fail to create announcement for class user is not teacher of', async () => {
|
|
51
|
+
await expect(user2Caller.announcement.create({
|
|
52
|
+
classId: testClass.id,
|
|
53
|
+
remarks: 'Unauthorized announcement',
|
|
54
|
+
})).rejects.toThrow();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('should fail without authentication', async () => {
|
|
58
|
+
const invalidCaller = await createTRPCContext({
|
|
59
|
+
req: { headers: {} } as any,
|
|
60
|
+
res: {} as any,
|
|
61
|
+
});
|
|
62
|
+
const router = appRouter.createCaller(invalidCaller);
|
|
63
|
+
|
|
64
|
+
await expect(router.announcement.create({
|
|
65
|
+
classId: testClass.id,
|
|
66
|
+
remarks: 'Test',
|
|
67
|
+
})).rejects.toThrow();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('getAll', () => {
|
|
72
|
+
test('should get all announcements for class', async () => {
|
|
73
|
+
const result = await user1Caller.announcement.getAll({
|
|
74
|
+
classId: testClass.id,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(result.announcements).toBeDefined();
|
|
78
|
+
expect(Array.isArray(result.announcements)).toBe(true);
|
|
79
|
+
expect(result.announcements.length).toBeGreaterThanOrEqual(1);
|
|
80
|
+
expect(result.announcements.some((a: any) => a.id === testAnnouncement.announcement.id)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should fail to get announcements for class user is not member of', async () => {
|
|
84
|
+
await expect(user2Caller.announcement.getAll({
|
|
85
|
+
classId: testClass.id,
|
|
86
|
+
})).rejects.toThrow();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('get', () => {
|
|
91
|
+
test('should get single announcement successfully', async () => {
|
|
92
|
+
const announcement = await user1Caller.announcement.get({
|
|
93
|
+
id: testAnnouncement.announcement.id,
|
|
94
|
+
classId: testClass.id,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(announcement.announcement).toBeDefined();
|
|
98
|
+
expect(announcement.announcement.id).toBe(testAnnouncement.announcement.id);
|
|
99
|
+
expect(announcement.announcement.remarks).toBe('This is a test announcement');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('should fail to get announcement for class user is not member of', async () => {
|
|
103
|
+
await expect(user2Caller.announcement.get({
|
|
104
|
+
id: testAnnouncement.announcement.id,
|
|
105
|
+
classId: testClass.id,
|
|
106
|
+
})).rejects.toThrow();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('update', () => {
|
|
111
|
+
test('should update announcement successfully', async () => {
|
|
112
|
+
const updated = await user1Caller.announcement.update({
|
|
113
|
+
id: testAnnouncement.announcement.id,
|
|
114
|
+
classId: testClass.id,
|
|
115
|
+
data: {
|
|
116
|
+
remarks: 'Updated announcement remarks',
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(updated.announcement.remarks).toBe('Updated announcement remarks');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('should fail to update announcement user is not teacher of', async () => {
|
|
124
|
+
await expect(user2Caller.announcement.update({
|
|
125
|
+
id: testAnnouncement.announcement.id,
|
|
126
|
+
classId: testClass.id,
|
|
127
|
+
data: {
|
|
128
|
+
remarks: 'Unauthorized update',
|
|
129
|
+
},
|
|
130
|
+
})).rejects.toThrow();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('delete', () => {
|
|
135
|
+
test('should delete announcement successfully', async () => {
|
|
136
|
+
// Create a new announcement to delete
|
|
137
|
+
const newAnnouncement = await user1Caller.announcement.create({
|
|
138
|
+
classId: testClass.id,
|
|
139
|
+
remarks: 'To be deleted',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const result = await user1Caller.announcement.delete({
|
|
143
|
+
id: newAnnouncement.announcement.id,
|
|
144
|
+
classId: testClass.id,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(result.success).toBe(true);
|
|
148
|
+
|
|
149
|
+
// Verify announcement is deleted
|
|
150
|
+
await expect(user1Caller.announcement.get({
|
|
151
|
+
id: newAnnouncement.announcement.id,
|
|
152
|
+
classId: testClass.id,
|
|
153
|
+
})).rejects.toThrow();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('should fail to delete announcement user is not teacher of', async () => {
|
|
157
|
+
await expect(user2Caller.announcement.delete({
|
|
158
|
+
id: testAnnouncement.announcement.id,
|
|
159
|
+
classId: testClass.id,
|
|
160
|
+
})).rejects.toThrow();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|