@studious-lms/server 1.15.0 → 1.16.1
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/dist/lib/chatAttachmentPrompt.d.ts +47 -0
- package/dist/lib/chatAttachmentPrompt.d.ts.map +1 -0
- package/dist/lib/chatAttachmentPrompt.js +190 -0
- package/dist/lib/chatAttachmentPrompt.js.map +1 -0
- package/dist/lib/inferDocumentFormat.js +3 -3
- package/dist/lib/inferDocumentFormat.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +7 -35
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +2 -2
- package/dist/pipelines/aiNewtonChat.d.ts +1 -1
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +20 -19
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +1 -0
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +146 -36
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/pipelines/labChatPrompt.d.ts +4 -20
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
- package/dist/pipelines/labChatPrompt.js +22 -65
- package/dist/pipelines/labChatPrompt.js.map +1 -1
- package/dist/routers/_app.d.ts +14 -0
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/worksheet.d.ts +7 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +8 -3
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/services/aiProposalTools.js +5 -5
- package/dist/services/aiProposalTools.js.map +1 -1
- package/dist/services/newtonChat.d.ts.map +1 -1
- package/dist/services/newtonChat.js +2 -8
- package/dist/services/newtonChat.js.map +1 -1
- package/dist/services/openaiDocumentGen.d.ts.map +1 -1
- package/dist/services/openaiDocumentGen.js +58 -17
- package/dist/services/openaiDocumentGen.js.map +1 -1
- package/dist/services/worksheet.d.ts +1 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +13 -3
- package/dist/services/worksheet.js.map +1 -1
- package/dist/workers/processors/aiNewtonChat.d.ts.map +1 -1
- package/dist/workers/processors/aiNewtonChat.js +3 -3
- package/dist/workers/processors/aiNewtonChat.js.map +1 -1
- package/package.json +2 -1
- package/src/lib/chatAttachmentPrompt.ts +270 -0
- package/src/lib/inferDocumentFormat.ts +1 -1
- package/src/pipelines/aiLabChat.ts +5 -43
- package/src/pipelines/aiNewtonChat.ts +20 -23
- package/src/pipelines/gradeWorksheet.ts +171 -38
- package/src/pipelines/labChatPrompt.ts +27 -99
- package/src/routers/worksheet.ts +11 -0
- package/src/services/aiProposalTools.ts +3 -3
- package/src/services/newtonChat.ts +0 -7
- package/src/services/openaiDocumentGen.ts +57 -15
- package/src/services/worksheet.ts +20 -1
- package/src/workers/processors/aiNewtonChat.ts +0 -1
- package/tests/integration/routers/newtonChat.test.ts +9 -8
- package/tests/unit/lib/chatAttachmentPrompt.test.ts +135 -0
- package/tests/unit/lib/inferDocumentFormat.test.ts +10 -0
- package/tests/unit/pipelines/labChatPrompt.test.ts +6 -6
|
@@ -1,23 +1,10 @@
|
|
|
1
1
|
import type { ModelMessage } from "ai";
|
|
2
|
+
import {
|
|
3
|
+
buildChatModelMessages,
|
|
4
|
+
type ChatHistoryMessage,
|
|
5
|
+
} from "../lib/chatAttachmentPrompt.js";
|
|
2
6
|
|
|
3
|
-
export type ConversationMessage =
|
|
4
|
-
id?: string;
|
|
5
|
-
content: string;
|
|
6
|
-
senderId: string;
|
|
7
|
-
createdAt?: Date | string | number;
|
|
8
|
-
sender?: {
|
|
9
|
-
username?: string | null;
|
|
10
|
-
profile?: {
|
|
11
|
-
displayName?: string | null;
|
|
12
|
-
} | null;
|
|
13
|
-
} | null;
|
|
14
|
-
attachments?: {
|
|
15
|
-
id: string;
|
|
16
|
-
name: string;
|
|
17
|
-
type: string;
|
|
18
|
-
url?: string;
|
|
19
|
-
}[];
|
|
20
|
-
};
|
|
7
|
+
export type ConversationMessage = ChatHistoryMessage;
|
|
21
8
|
|
|
22
9
|
const LAB_CHAT_BASE_INSTRUCTIONS = [
|
|
23
10
|
"IMPORTANT INSTRUCTIONS:",
|
|
@@ -47,20 +34,27 @@ const LAB_CHAT_REFERENCE_RULES = [
|
|
|
47
34
|
|
|
48
35
|
const LAB_CHAT_DOC_RULES = [
|
|
49
36
|
"WHEN CREATING FILES (generate_document tool):",
|
|
50
|
-
"- Call generate_document with title, content
|
|
37
|
+
"- Call generate_document with title, content, and format.",
|
|
51
38
|
"- You can generate MANY file types — not just PDF. Always pass format explicitly.",
|
|
52
39
|
"- Match format to what the teacher asked for:",
|
|
53
|
-
" * slides / presentation / deck → pptx",
|
|
54
|
-
" * spreadsheet / grade table / data → xlsx or csv",
|
|
40
|
+
" * slides / presentation / deck → pptx (separate slides with --- or Slide N: headers in content)",
|
|
41
|
+
" * spreadsheet / grade table / data → xlsx or csv (include markdown tables or row data in content)",
|
|
55
42
|
" * editable handout / letter → docx",
|
|
56
43
|
" * printable worksheet / reading → pdf",
|
|
57
|
-
" * diagram / chart
|
|
44
|
+
" * diagram / chart / flowchart / figure / graph / visual → png (describe the visual in detail: labels, arrows, shapes, data — NOT pdf)",
|
|
58
45
|
" * notes / readme → md or txt",
|
|
46
|
+
" * source code → py/js/ts only when the teacher explicitly wants code files",
|
|
59
47
|
"- Supported: pdf, docx, xlsx, pptx, csv, md, html, txt, json, xml, tex, png, jpeg, gif, zip, py, js, ts, css, sh.",
|
|
60
|
-
"-
|
|
61
|
-
"- Write complete classroom-ready content — no placeholders.",
|
|
48
|
+
"- Write complete classroom-ready content — no placeholders like [diagram here].",
|
|
62
49
|
"- One file per tool call; split long packs into logical separate documents if needed.",
|
|
63
|
-
"- In your spoken reply, name the actual format (e.g. \"PowerPoint\", \"spreadsheet\") — never say you only make PDFs.",
|
|
50
|
+
"- In your spoken reply, name the actual format (e.g. \"PowerPoint\", \"spreadsheet\", \"diagram image\") — never say you only make PDFs.",
|
|
51
|
+
"- Never include download links or URLs; the file appears as an attachment on this message automatically after you call generate_document.",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const LAB_CHAT_ATTACHMENT_RULES = [
|
|
55
|
+
"WHEN THE TEACHER ATTACHES FILES:",
|
|
56
|
+
"- Read attached PDFs, Word docs, PowerPoints, spreadsheets, images, and text files — content is provided inline.",
|
|
57
|
+
"- Reference specifics from the attachment when answering or creating materials.",
|
|
64
58
|
];
|
|
65
59
|
|
|
66
60
|
const LAB_CHAT_WORKSHEET_RULES = [
|
|
@@ -100,33 +94,6 @@ FILE GENERATION: You can attach pdf, docx, xlsx, pptx, csv, images, and more via
|
|
|
100
94
|
|
|
101
95
|
const buildPrompt = (sections: string[]): string => sections.filter(Boolean).join("\n\n");
|
|
102
96
|
|
|
103
|
-
const sortMessagesChronologically = (
|
|
104
|
-
recentMessages: ConversationMessage[],
|
|
105
|
-
): ConversationMessage[] =>
|
|
106
|
-
recentMessages
|
|
107
|
-
.map((message, index) => ({ message, index }))
|
|
108
|
-
.sort((left, right) => {
|
|
109
|
-
const leftTime = left.message.createdAt
|
|
110
|
-
? new Date(left.message.createdAt).getTime()
|
|
111
|
-
: null;
|
|
112
|
-
const rightTime = right.message.createdAt
|
|
113
|
-
? new Date(right.message.createdAt).getTime()
|
|
114
|
-
: null;
|
|
115
|
-
|
|
116
|
-
if (leftTime != null && rightTime != null) {
|
|
117
|
-
return leftTime - rightTime;
|
|
118
|
-
}
|
|
119
|
-
if (leftTime != null) {
|
|
120
|
-
return -1;
|
|
121
|
-
}
|
|
122
|
-
if (rightTime != null) {
|
|
123
|
-
return 1;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return left.index - right.index;
|
|
127
|
-
})
|
|
128
|
-
.map(({ message }) => message);
|
|
129
|
-
|
|
130
97
|
const LAB_CHAT_TOOL_RULES = [
|
|
131
98
|
"HOW TO CREATE CONTENT (TOOLS):",
|
|
132
99
|
"- You have tools to search class materials and to PROPOSE content for the teacher to review and accept.",
|
|
@@ -147,6 +114,7 @@ export const buildLabChatAgentSystemPrompt = (context: string): string =>
|
|
|
147
114
|
LAB_CHAT_REFERENCE_RULES.join("\n"),
|
|
148
115
|
LAB_CHAT_TOOL_RULES.join("\n"),
|
|
149
116
|
LAB_CHAT_DOC_RULES.join("\n"),
|
|
117
|
+
LAB_CHAT_ATTACHMENT_RULES.join("\n"),
|
|
150
118
|
LAB_CHAT_WORKSHEET_RULES.join("\n"),
|
|
151
119
|
LAB_CHAT_SECTION_RULES.join("\n"),
|
|
152
120
|
LAB_CHAT_ASSIGNMENT_RULES.join("\n"),
|
|
@@ -158,7 +126,7 @@ export const buildLabChatAgentSystemPrompt = (context: string): string =>
|
|
|
158
126
|
* Returns a `system` string and AI-SDK `ModelMessage[]` history. Per-request
|
|
159
127
|
* class context (IDs) is folded into the system string.
|
|
160
128
|
*/
|
|
161
|
-
export const buildLabChatAgentMessages = ({
|
|
129
|
+
export const buildLabChatAgentMessages = async ({
|
|
162
130
|
context,
|
|
163
131
|
classContext,
|
|
164
132
|
recentMessages,
|
|
@@ -168,54 +136,14 @@ export const buildLabChatAgentMessages = ({
|
|
|
168
136
|
classContext: string;
|
|
169
137
|
recentMessages: ConversationMessage[];
|
|
170
138
|
isAIUser: (senderId: string) => boolean;
|
|
171
|
-
}): { system: string; messages: ModelMessage[] } => {
|
|
139
|
+
}): Promise<{ system: string; messages: ModelMessage[] }> => {
|
|
172
140
|
const system = `${buildLabChatAgentSystemPrompt(context)}\n\nCLASS CONTEXT (use these IDs when proposing assignments, worksheets, or attaching files):\n${classContext}`;
|
|
173
141
|
|
|
174
|
-
const messages
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (role === "assistant") {
|
|
181
|
-
return { role, content: message.content };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Build user message with attachments
|
|
185
|
-
const textContent = `${senderName}: ${message.content}`;
|
|
186
|
-
const attachments = message.attachments ?? [];
|
|
187
|
-
|
|
188
|
-
if (attachments.length === 0) {
|
|
189
|
-
return { role, content: textContent };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Separate images (multimodal) from other files (text description)
|
|
193
|
-
const imageAttachments = attachments.filter((a) => a.type.startsWith('image/'));
|
|
194
|
-
const otherAttachments = attachments.filter((a) => !a.type.startsWith('image/'));
|
|
195
|
-
|
|
196
|
-
const parts: Array<{ type: string; text?: string; image?: string }> = [];
|
|
197
|
-
|
|
198
|
-
// Add text content with non-image file descriptions
|
|
199
|
-
let fullText = textContent;
|
|
200
|
-
if (otherAttachments.length > 0) {
|
|
201
|
-
const fileList = otherAttachments.map((a) => `${a.name} (${a.type})`).join(', ');
|
|
202
|
-
fullText += `\n[Attached files: ${fileList}]`;
|
|
203
|
-
}
|
|
204
|
-
parts.push({ type: 'text', text: fullText });
|
|
205
|
-
|
|
206
|
-
// Add image attachments as image parts for multimodal
|
|
207
|
-
for (const img of imageAttachments) {
|
|
208
|
-
if (img.url) {
|
|
209
|
-
parts.push({ type: 'image', image: img.url });
|
|
210
|
-
} else {
|
|
211
|
-
// Fallback: mention the image name in text if no URL available
|
|
212
|
-
parts.push({ type: 'text', text: `[Image attached: ${img.name}]` });
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return { role, content: parts } as ModelMessage;
|
|
217
|
-
},
|
|
218
|
-
);
|
|
142
|
+
const messages = await buildChatModelMessages({
|
|
143
|
+
messages: recentMessages,
|
|
144
|
+
isAIUser,
|
|
145
|
+
defaultSenderLabel: 'Teacher',
|
|
146
|
+
});
|
|
219
147
|
|
|
220
148
|
return { system, messages };
|
|
221
149
|
};
|
package/src/routers/worksheet.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
answerWorksheetQuestion,
|
|
31
31
|
cancelGrading,
|
|
32
32
|
regradeQuestion,
|
|
33
|
+
regradeAll,
|
|
33
34
|
gradeAnswer,
|
|
34
35
|
addCommentToResponse,
|
|
35
36
|
} from "../services/worksheet.js";
|
|
@@ -370,6 +371,16 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
370
371
|
regradeQuestion(input.worksheetResponseId, input.progressId, ctx.user!.id)
|
|
371
372
|
),
|
|
372
373
|
|
|
374
|
+
regradeAll: protectedProcedure
|
|
375
|
+
.input(
|
|
376
|
+
z.object({
|
|
377
|
+
worksheetResponseId: z.string(),
|
|
378
|
+
})
|
|
379
|
+
)
|
|
380
|
+
.mutation(({ ctx, input }) =>
|
|
381
|
+
regradeAll(input.worksheetResponseId, ctx.user!.id)
|
|
382
|
+
),
|
|
383
|
+
|
|
373
384
|
gradeAnswer: protectedProcedure
|
|
374
385
|
.input(
|
|
375
386
|
z.object({
|
|
@@ -237,7 +237,7 @@ export function labProposalTools(proposals: LabProposals): ToolSet {
|
|
|
237
237
|
|
|
238
238
|
generate_document: {
|
|
239
239
|
description:
|
|
240
|
-
'Generate a downloadable file and attach it to this message.
|
|
240
|
+
'Generate a downloadable file and attach it to this message. Pick the best format for the request — pptx for slides, xlsx/csv for data, docx for editable handouts, pdf for print-ready docs, png/jpeg for charts/diagrams/figures (NOT pdf for visuals). Use py/js/ts only when the teacher explicitly wants source code. Always set format explicitly.',
|
|
241
241
|
inputSchema: jsonSchema({
|
|
242
242
|
type: 'object',
|
|
243
243
|
properties: {
|
|
@@ -245,12 +245,12 @@ export function labProposalTools(proposals: LabProposals): ToolSet {
|
|
|
245
245
|
content: {
|
|
246
246
|
type: 'string',
|
|
247
247
|
description:
|
|
248
|
-
'Full body — markdown or
|
|
248
|
+
'Full body. Structure by format: pptx — separate slides with "---" or "Slide N:" headers; xlsx/csv — markdown tables or row data; png/jpeg — describe the visual in detail (shapes, labels, arrows, data points, colors); pdf/docx — headings, lists, tables; code formats — complete runnable source.',
|
|
249
249
|
},
|
|
250
250
|
format: {
|
|
251
251
|
type: 'string',
|
|
252
252
|
description:
|
|
253
|
-
'Output file type. REQUIRED. pptx=slides
|
|
253
|
+
'Output file type. REQUIRED. pptx=slides, xlsx/csv=spreadsheet, docx=Word, pdf=printable doc, png/jpeg/gif=chart/diagram/figure image, md/txt/html=text. py/js/ts/css/sh=source code only when requested.',
|
|
254
254
|
enum: [
|
|
255
255
|
'pdf', 'docx', 'doc', 'xlsx', 'pptx', 'csv', 'md', 'html', 'txt',
|
|
256
256
|
'json', 'xml', 'tex', 'png', 'jpeg', 'jpg', 'gif', 'zip', 'tar',
|
|
@@ -159,13 +159,6 @@ export async function postToNewtonChat(
|
|
|
159
159
|
ensureMessageHasContentOrAttachments(content, fileIds);
|
|
160
160
|
const normalizedContent = content.trim();
|
|
161
161
|
|
|
162
|
-
if (!normalizedContent) {
|
|
163
|
-
throw new TRPCError({
|
|
164
|
-
code: "BAD_REQUEST",
|
|
165
|
-
message: "Newton chat messages must include text content",
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
162
|
const result = await prisma.$transaction(async (tx) => {
|
|
170
163
|
const message = await tx.message.create({
|
|
171
164
|
data: {
|
|
@@ -6,7 +6,7 @@ import OpenAI from 'openai';
|
|
|
6
6
|
import type { Response } from 'openai/resources/responses/responses.js';
|
|
7
7
|
import { logger } from '../utils/logger.js';
|
|
8
8
|
|
|
9
|
-
const DOCUMENT_GEN_MODEL = 'gpt-
|
|
9
|
+
const DOCUMENT_GEN_MODEL = 'gpt-5.5';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Extensions OpenAI Code Interpreter can read/write in its sandbox.
|
|
@@ -67,11 +67,11 @@ const FORMAT_MIME: Record<DocumentFormat, string> = {
|
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
const FORMAT_HINTS: Record<DocumentFormat, string> = {
|
|
70
|
-
pdf: 'fpdf2 or reportlab —
|
|
71
|
-
docx: 'python-docx — Word document with
|
|
70
|
+
pdf: 'fpdf2 or reportlab — polished document with headings, body text, and tables',
|
|
71
|
+
docx: 'python-docx — Word document with heading styles, paragraphs, and lists',
|
|
72
72
|
doc: 'python-docx saving as .doc if supported, otherwise .docx',
|
|
73
|
-
xlsx: 'openpyxl or pandas — spreadsheet with labelled columns
|
|
74
|
-
pptx: 'python-pptx —
|
|
73
|
+
xlsx: 'openpyxl or pandas — spreadsheet with labelled columns, formulas where useful',
|
|
74
|
+
pptx: 'python-pptx — full slide deck: title slide, section slides, bullets, diagrams on slides',
|
|
75
75
|
csv: 'pandas or csv module — tabular data with headers',
|
|
76
76
|
md: 'write the markdown file directly',
|
|
77
77
|
html: 'write a complete, styled HTML page',
|
|
@@ -79,19 +79,58 @@ const FORMAT_HINTS: Record<DocumentFormat, string> = {
|
|
|
79
79
|
json: 'json.dumps with readable indentation',
|
|
80
80
|
xml: 'well-formed XML document',
|
|
81
81
|
tex: 'LaTeX source that compiles to the requested content',
|
|
82
|
-
png: 'matplotlib or Pillow — chart
|
|
83
|
-
jpeg: 'matplotlib or Pillow —
|
|
84
|
-
jpg: 'matplotlib or Pillow —
|
|
82
|
+
png: 'matplotlib, networkx, or Pillow — render the described chart/diagram as a finished image',
|
|
83
|
+
jpeg: 'matplotlib or Pillow — high-quality visual saved as JPEG',
|
|
84
|
+
jpg: 'matplotlib or Pillow — high-quality visual saved as JPEG',
|
|
85
85
|
gif: 'matplotlib or Pillow — save as GIF',
|
|
86
86
|
zip: 'zipfile — bundle multiple related files if needed',
|
|
87
87
|
tar: 'tarfile — archive if multiple files are required',
|
|
88
|
-
py: 'valid Python source
|
|
89
|
-
js: 'valid JavaScript source
|
|
90
|
-
ts: 'valid TypeScript source
|
|
88
|
+
py: 'valid, runnable Python source with comments',
|
|
89
|
+
js: 'valid JavaScript source with comments',
|
|
90
|
+
ts: 'valid TypeScript source with comments',
|
|
91
91
|
css: 'stylesheet file',
|
|
92
92
|
sh: 'shell script file',
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
+
/** Extra quality rules appended for formats that need more than a one-line library hint. */
|
|
96
|
+
const FORMAT_QUALITY_RULES: Partial<Record<DocumentFormat, string[]>> = {
|
|
97
|
+
png: [
|
|
98
|
+
'Interpret the source content as a visual spec — draw it, do not dump text into the image.',
|
|
99
|
+
'Charts: labelled axes, legend, title, readable font sizes, 150+ DPI.',
|
|
100
|
+
'Diagrams/flowcharts: use matplotlib patches, networkx, or Pillow drawing — boxes, arrows, labels.',
|
|
101
|
+
'Geometry/maps: accurate relative positions; label every important part.',
|
|
102
|
+
'Use color meaningfully; avoid blank or placeholder images.',
|
|
103
|
+
],
|
|
104
|
+
jpeg: [
|
|
105
|
+
'Same visual quality rules as PNG — polished chart or diagram, not a screenshot of text.',
|
|
106
|
+
],
|
|
107
|
+
jpg: [
|
|
108
|
+
'Same visual quality rules as PNG — polished chart or diagram, not a screenshot of text.',
|
|
109
|
+
],
|
|
110
|
+
gif: [
|
|
111
|
+
'Render a clear educational visual; static GIF is fine unless animation is described.',
|
|
112
|
+
],
|
|
113
|
+
pptx: [
|
|
114
|
+
'Split slides on "---" lines or "Slide N:" / "# " headings in the source content.',
|
|
115
|
+
'Title slide first, then content slides with concise bullets (not walls of text).',
|
|
116
|
+
'If the content describes a diagram, draw it on a slide (shapes/arrows) or embed a generated chart.',
|
|
117
|
+
'Use distinct slide layouts where appropriate (title, title+content).',
|
|
118
|
+
],
|
|
119
|
+
xlsx: [
|
|
120
|
+
'Parse markdown tables or tabular lists in the source into real rows/columns.',
|
|
121
|
+
'Bold header row; auto-fit column widths where possible.',
|
|
122
|
+
],
|
|
123
|
+
pdf: [
|
|
124
|
+
'If the source describes a figure/chart, generate and embed it — do not leave a "[diagram]" placeholder.',
|
|
125
|
+
],
|
|
126
|
+
docx: [
|
|
127
|
+
'Use Heading 1/2 styles for section titles; bullets and numbered lists for enumerations.',
|
|
128
|
+
],
|
|
129
|
+
html: [
|
|
130
|
+
'Include embedded CSS for readable typography; use tables or simple layout for structured content.',
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
|
|
95
134
|
export interface GenerateDocumentInput {
|
|
96
135
|
title: string;
|
|
97
136
|
/** Markdown or plain-text source material for the output file. */
|
|
@@ -218,6 +257,7 @@ function safeBasename(title: string): string {
|
|
|
218
257
|
}
|
|
219
258
|
|
|
220
259
|
function buildDocumentGenInstructions(format: DocumentFormat): string {
|
|
260
|
+
const qualityRules = FORMAT_QUALITY_RULES[format] ?? [];
|
|
221
261
|
return [
|
|
222
262
|
'You are a document generator for K-12 and university teachers.',
|
|
223
263
|
'When given a title, output format, and source content:',
|
|
@@ -225,14 +265,17 @@ function buildDocumentGenInstructions(format: DocumentFormat): string {
|
|
|
225
265
|
`2. For this format use: ${FORMAT_HINTS[format]}.`,
|
|
226
266
|
`3. You MUST output a ${format.toUpperCase()} file — not any other format.`,
|
|
227
267
|
`4. The saved filename MUST end with .${extensionForFormat(format)}.`,
|
|
228
|
-
'5.
|
|
268
|
+
'5. Faithfully adapt ALL source content to the output medium — do not summarize away detail.',
|
|
229
269
|
'6. Include the document title prominently where the format allows.',
|
|
230
270
|
'7. Put ONLY the educational content in the file — no meta commentary.',
|
|
231
271
|
'8. You MUST save the file to disk in the working directory before you finish.',
|
|
232
272
|
...(format !== 'pdf'
|
|
233
273
|
? [`9. Do NOT create a PDF when ${format.toUpperCase()} was requested.`]
|
|
234
274
|
: []),
|
|
235
|
-
|
|
275
|
+
...(qualityRules.length > 0
|
|
276
|
+
? ['', 'Format-specific quality:', ...qualityRules.map((r, i) => `${i + 1}. ${r}`)]
|
|
277
|
+
: []),
|
|
278
|
+
].join('\n');
|
|
236
279
|
}
|
|
237
280
|
|
|
238
281
|
/** Generate a file via OpenAI Code Interpreter. */
|
|
@@ -260,8 +303,7 @@ export async function generateDocumentWithOpenAI(
|
|
|
260
303
|
`Output filename (exact): ${filename}`,
|
|
261
304
|
`Output format: ${ext}`,
|
|
262
305
|
`Document title: ${input.title}`,
|
|
263
|
-
'Source content
|
|
264
|
-
input.content,
|
|
306
|
+
['Source content — adapt fully to the output medium (do not paste raw markdown into slides/images):', input.content].join('\n'),
|
|
265
307
|
].join('\n\n'),
|
|
266
308
|
});
|
|
267
309
|
|
|
@@ -6,7 +6,7 @@ import { TRPCError } from "@trpc/server";
|
|
|
6
6
|
import { JobStatus, Prisma, type WorksheetQuestionType } from "@prisma/client";
|
|
7
7
|
import { prisma } from "../lib/prisma.js";
|
|
8
8
|
import { getFileUrl } from "../lib/fileUpload.js";
|
|
9
|
-
import { cancelGradePipeline, regradeWorksheetPipeline } from "../pipelines/gradeWorksheet.js";
|
|
9
|
+
import { cancelGradePipeline, regradeWorksheetPipeline, regradeAllPipeline } from "../pipelines/gradeWorksheet.js";
|
|
10
10
|
import { enqueueRunJob } from "../workers/queues.js";
|
|
11
11
|
import { logger } from "../utils/logger.js";
|
|
12
12
|
import {
|
|
@@ -629,6 +629,25 @@ export async function regradeQuestion(
|
|
|
629
629
|
return regradeWorksheetPipeline(worksheetResponseId, progressId);
|
|
630
630
|
}
|
|
631
631
|
|
|
632
|
+
export async function regradeAll(
|
|
633
|
+
worksheetResponseId: string,
|
|
634
|
+
userId: string
|
|
635
|
+
) {
|
|
636
|
+
const worksheetResponse = await findWorksheetResponseByIdForTeacher(
|
|
637
|
+
worksheetResponseId,
|
|
638
|
+
userId
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
if (!worksheetResponse) {
|
|
642
|
+
throw new TRPCError({
|
|
643
|
+
code: "FORBIDDEN",
|
|
644
|
+
message: "Only class teachers can regrade all worksheet answers",
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
await regradeAllPipeline(worksheetResponseId);
|
|
649
|
+
}
|
|
650
|
+
|
|
632
651
|
export async function gradeAnswer(
|
|
633
652
|
questionId: string,
|
|
634
653
|
studentWorksheetResponseId: string,
|
|
@@ -50,7 +50,7 @@ describe('Newton Chat Router', () => {
|
|
|
50
50
|
conversationId = tutorConversation.conversationId;
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
test('
|
|
53
|
+
test('allows attachment-only Newton chat posts', async () => {
|
|
54
54
|
const [uploadTarget] = await user2Caller.message.getUploadUrls({
|
|
55
55
|
conversationId,
|
|
56
56
|
files: [
|
|
@@ -70,12 +70,13 @@ describe('Newton Chat Router', () => {
|
|
|
70
70
|
},
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
await
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
).
|
|
73
|
+
const message = await user2Caller.newtonChat.postToNewtonChat({
|
|
74
|
+
newtonChatId,
|
|
75
|
+
content: ' ',
|
|
76
|
+
fileIds: [uploadTarget!.id],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(message.attachments).toHaveLength(1);
|
|
80
|
+
expect(message.attachments[0]?.name).toBe('newton-attachment-only.pdf');
|
|
80
81
|
});
|
|
81
82
|
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
buildUserMessageContentWithAttachments,
|
|
4
|
+
loadChatAttachments,
|
|
5
|
+
} from '../../../src/lib/chatAttachmentPrompt.js';
|
|
6
|
+
|
|
7
|
+
const pdfBytes = new Uint8Array([0x25, 0x50, 0x44, 0x46]);
|
|
8
|
+
const docxText = 'Unit 3 lesson plan content';
|
|
9
|
+
|
|
10
|
+
vi.mock('officeparser', () => ({
|
|
11
|
+
OfficeParser: {
|
|
12
|
+
parseOffice: vi.fn(async () => ({
|
|
13
|
+
toText: () => docxText,
|
|
14
|
+
})),
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock('../../../src/lib/googleCloudStorage.js', () => ({
|
|
19
|
+
bucket: {
|
|
20
|
+
file: vi.fn((path: string) => ({
|
|
21
|
+
download: vi.fn(async () => {
|
|
22
|
+
if (path.includes('missing')) {
|
|
23
|
+
throw new Error('not found');
|
|
24
|
+
}
|
|
25
|
+
if (path.endsWith('.docx')) {
|
|
26
|
+
return [Buffer.from('fake-docx-bytes')];
|
|
27
|
+
}
|
|
28
|
+
return [Buffer.from(pdfBytes)];
|
|
29
|
+
}),
|
|
30
|
+
})),
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
describe('buildUserMessageContentWithAttachments', () => {
|
|
35
|
+
it('returns plain text when there are no attachments', () => {
|
|
36
|
+
expect(buildUserMessageContentWithAttachments('Hello', [])).toBe('Hello');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('includes PDFs as inline file parts with bytes', () => {
|
|
40
|
+
const content = buildUserMessageContentWithAttachments('Review this', [
|
|
41
|
+
{
|
|
42
|
+
id: '1',
|
|
43
|
+
name: 'essay.pdf',
|
|
44
|
+
type: 'application/pdf',
|
|
45
|
+
path: 'essay.pdf',
|
|
46
|
+
data: pdfBytes,
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
expect(content).toEqual([
|
|
51
|
+
{ type: 'text', text: 'Review this' },
|
|
52
|
+
{
|
|
53
|
+
type: 'file',
|
|
54
|
+
data: pdfBytes,
|
|
55
|
+
mediaType: 'application/pdf',
|
|
56
|
+
filename: 'essay.pdf',
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('includes extracted docx text in the message body', () => {
|
|
62
|
+
const content = buildUserMessageContentWithAttachments('Summarize', [
|
|
63
|
+
{
|
|
64
|
+
id: '2',
|
|
65
|
+
name: 'plan.docx',
|
|
66
|
+
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
67
|
+
path: 'plan.docx',
|
|
68
|
+
extractedText: docxText,
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
expect(content).toBe(
|
|
73
|
+
`Summarize\n\n[Content of plan.docx]:\n${docxText}`,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('lists unsupported file types when extraction failed', () => {
|
|
78
|
+
const content = buildUserMessageContentWithAttachments('Here', [
|
|
79
|
+
{
|
|
80
|
+
id: '3',
|
|
81
|
+
name: 'clip.mp4',
|
|
82
|
+
type: 'video/mp4',
|
|
83
|
+
path: 'clip.mp4',
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
expect(content).toBe(
|
|
88
|
+
'Here\n[Attached files (content not extracted): clip.mp4 (video/mp4)]',
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('loadChatAttachments', () => {
|
|
94
|
+
it('loads bytes and extracts office document text', async () => {
|
|
95
|
+
const resolved = await loadChatAttachments([
|
|
96
|
+
{
|
|
97
|
+
id: '1',
|
|
98
|
+
name: 'plan.docx',
|
|
99
|
+
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
100
|
+
path: 'uploads/plan.docx',
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
expect(resolved[0]?.data).toBeDefined();
|
|
105
|
+
expect(resolved[0]?.extractedText).toBe(docxText);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('loads pdf bytes without extracted text', async () => {
|
|
109
|
+
const resolved = await loadChatAttachments([
|
|
110
|
+
{
|
|
111
|
+
id: '2',
|
|
112
|
+
name: 'scan.pdf',
|
|
113
|
+
type: 'application/pdf',
|
|
114
|
+
path: 'uploads/scan.pdf',
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
expect(resolved[0]?.data).toEqual(pdfBytes);
|
|
119
|
+
expect(resolved[0]?.extractedText).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('skips download for video attachments', async () => {
|
|
123
|
+
const resolved = await loadChatAttachments([
|
|
124
|
+
{
|
|
125
|
+
id: '3',
|
|
126
|
+
name: 'clip.mp4',
|
|
127
|
+
type: 'video/mp4',
|
|
128
|
+
path: 'uploads/clip.mp4',
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
expect(resolved[0]?.data).toBeUndefined();
|
|
133
|
+
expect(resolved[0]?.extractedText).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -30,4 +30,14 @@ describe('inferDocumentFormat', () => {
|
|
|
30
30
|
it('defaults to pdf for generic handouts', () => {
|
|
31
31
|
expect(inferDocumentFormat('Reading Guide', 'Chapter summary', 'pdf')).toBe('pdf');
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
it('overrides pdf when content mentions a flowchart', () => {
|
|
35
|
+
expect(
|
|
36
|
+
inferDocumentFormat(
|
|
37
|
+
'Photosynthesis Overview',
|
|
38
|
+
'Include a flowchart of the light reactions.',
|
|
39
|
+
'pdf',
|
|
40
|
+
),
|
|
41
|
+
).toBe('png');
|
|
42
|
+
});
|
|
33
43
|
});
|
|
@@ -17,8 +17,8 @@ describe("buildLabChatAgentSystemPrompt", () => {
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
describe("buildLabChatAgentMessages", () => {
|
|
20
|
-
test("folds the class context into the system prompt, not the message list", () => {
|
|
21
|
-
const { system, messages } = buildLabChatAgentMessages({
|
|
20
|
+
test("folds the class context into the system prompt, not the message list", async () => {
|
|
21
|
+
const { system, messages } = await buildLabChatAgentMessages({
|
|
22
22
|
context: "Topic context",
|
|
23
23
|
classContext: "CLASS: Biology",
|
|
24
24
|
recentMessages: [
|
|
@@ -41,8 +41,8 @@ describe("buildLabChatAgentMessages", () => {
|
|
|
41
41
|
]);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
test("sorts timestamped history into chronological order before prompt assembly", () => {
|
|
45
|
-
const { messages } = buildLabChatAgentMessages({
|
|
44
|
+
test("sorts timestamped history into chronological order before prompt assembly", async () => {
|
|
45
|
+
const { messages } = await buildLabChatAgentMessages({
|
|
46
46
|
context: "Topic context",
|
|
47
47
|
classContext: "CLASS: Biology",
|
|
48
48
|
recentMessages: [
|
|
@@ -76,8 +76,8 @@ describe("buildLabChatAgentMessages", () => {
|
|
|
76
76
|
});
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
test("keeps original order among messages without timestamps", () => {
|
|
80
|
-
const { messages } = buildLabChatAgentMessages({
|
|
79
|
+
test("keeps original order among messages without timestamps", async () => {
|
|
80
|
+
const { messages } = await buildLabChatAgentMessages({
|
|
81
81
|
context: "Topic context",
|
|
82
82
|
classContext: "CLASS: Biology",
|
|
83
83
|
recentMessages: [
|