@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared chat attachment handling for Newton + Lab pipelines.
|
|
3
|
+
*
|
|
4
|
+
* - Images + PDFs: inlined as multimodal parts (bytes from GCS).
|
|
5
|
+
* - Office docs, spreadsheets, slides, text, csv, json, html: text extracted
|
|
6
|
+
* via officeparser / UTF-8 decode and appended to the user message.
|
|
7
|
+
* - Audio/video/zip: filename mention only (no extraction).
|
|
8
|
+
*/
|
|
9
|
+
import type { ModelMessage, UserContent } from 'ai';
|
|
10
|
+
/** Model used for AI chats that may include document attachments. */
|
|
11
|
+
export declare const AI_CHAT_MODEL = "gpt-5.5";
|
|
12
|
+
export type ChatAttachmentRef = {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
path: string;
|
|
17
|
+
};
|
|
18
|
+
export type LoadedChatAttachment = ChatAttachmentRef & {
|
|
19
|
+
data?: Uint8Array;
|
|
20
|
+
extractedText?: string;
|
|
21
|
+
};
|
|
22
|
+
export type ChatHistoryMessage = {
|
|
23
|
+
id?: string;
|
|
24
|
+
content: string;
|
|
25
|
+
senderId: string;
|
|
26
|
+
createdAt?: Date | string | number;
|
|
27
|
+
sender?: {
|
|
28
|
+
username?: string | null;
|
|
29
|
+
profile?: {
|
|
30
|
+
displayName?: string | null;
|
|
31
|
+
} | null;
|
|
32
|
+
} | null;
|
|
33
|
+
attachments?: ChatAttachmentRef[];
|
|
34
|
+
};
|
|
35
|
+
/** Download bytes from GCS and extract text for every supported attachment type. */
|
|
36
|
+
export declare function loadChatAttachments(attachments: ChatAttachmentRef[]): Promise<LoadedChatAttachment[]>;
|
|
37
|
+
/** @deprecated Use {@link loadChatAttachments} */
|
|
38
|
+
export declare const loadPromptAttachments: typeof loadChatAttachments;
|
|
39
|
+
export declare function buildUserMessageContentWithAttachments(textContent: string, attachments: LoadedChatAttachment[]): UserContent;
|
|
40
|
+
export declare function sortChatMessagesChronologically<T extends ChatHistoryMessage>(messages: T[]): T[];
|
|
41
|
+
/** Build ModelMessage[] with shared attachment loading for Newton + Lab. */
|
|
42
|
+
export declare function buildChatModelMessages({ messages, isAIUser, defaultSenderLabel, }: {
|
|
43
|
+
messages: ChatHistoryMessage[];
|
|
44
|
+
isAIUser: (senderId: string) => boolean;
|
|
45
|
+
defaultSenderLabel: string;
|
|
46
|
+
}): Promise<ModelMessage[]>;
|
|
47
|
+
//# sourceMappingURL=chatAttachmentPrompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatAttachmentPrompt.d.ts","sourceRoot":"/","sources":["lib/chatAttachmentPrompt.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAuB,YAAY,EAAY,WAAW,EAAE,MAAM,IAAI,CAAC;AAKnF,qEAAqE;AACrE,eAAO,MAAM,aAAa,YAAY,CAAC;AAKvC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,iBAAiB,GAAG;IACrD,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;IACnC,MAAM,CAAC,EAAE;QACP,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,OAAO,CAAC,EAAE;YAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,GAAG,IAAI,CAAC;KAClD,GAAG,IAAI,CAAC;IACT,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;CACnC,CAAC;AAyEF,oFAAoF;AACpF,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,iBAAiB,EAAE,GAC/B,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAyBjC;AAED,kDAAkD;AAClD,eAAO,MAAM,qBAAqB,4BAAsB,CAAC;AAEzD,wBAAgB,sCAAsC,CACpD,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,oBAAoB,EAAE,GAClC,WAAW,CA4Db;AAED,wBAAgB,+BAA+B,CAAC,CAAC,SAAS,kBAAkB,EAC1E,QAAQ,EAAE,CAAC,EAAE,GACZ,CAAC,EAAE,CAmBL;AAED,4EAA4E;AAC5E,wBAAsB,sBAAsB,CAAC,EAC3C,QAAQ,EACR,QAAQ,EACR,kBAAkB,GACnB,EAAE;IACD,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,kBAAkB,EAAE,MAAM,CAAC;CAC5B,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAyB1B"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b2f1ec36-e878-51c6-b49c-0473d9a71c3e")}catch(e){}}();
|
|
3
|
+
import { OfficeParser } from 'officeparser';
|
|
4
|
+
import { bucket } from './googleCloudStorage.js';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
/** Model used for AI chats that may include document attachments. */
|
|
7
|
+
export const AI_CHAT_MODEL = 'gpt-5.5';
|
|
8
|
+
const MAX_EXTRACTED_CHARS = 80000;
|
|
9
|
+
const IMAGE_TYPE_PREFIX = 'image/';
|
|
10
|
+
const TEXT_DECODE_TYPES = new Set([
|
|
11
|
+
'text/plain',
|
|
12
|
+
'text/csv',
|
|
13
|
+
'text/markdown',
|
|
14
|
+
'text/html',
|
|
15
|
+
'application/json',
|
|
16
|
+
]);
|
|
17
|
+
const MENTION_ONLY_PREFIXES = ['audio/', 'video/'];
|
|
18
|
+
const MENTION_ONLY_TYPES = new Set(['application/zip']);
|
|
19
|
+
function extension(name) {
|
|
20
|
+
return name.split('.').pop()?.toLowerCase() ?? '';
|
|
21
|
+
}
|
|
22
|
+
function isPdf(attachment) {
|
|
23
|
+
return attachment.type === 'application/pdf' || extension(attachment.name) === 'pdf';
|
|
24
|
+
}
|
|
25
|
+
function isImage(attachment) {
|
|
26
|
+
return attachment.type.startsWith(IMAGE_TYPE_PREFIX);
|
|
27
|
+
}
|
|
28
|
+
function isMentionOnly(attachment) {
|
|
29
|
+
if (MENTION_ONLY_TYPES.has(attachment.type))
|
|
30
|
+
return true;
|
|
31
|
+
return MENTION_ONLY_PREFIXES.some((prefix) => attachment.type.startsWith(prefix));
|
|
32
|
+
}
|
|
33
|
+
function isUtf8Text(attachment) {
|
|
34
|
+
if (TEXT_DECODE_TYPES.has(attachment.type))
|
|
35
|
+
return true;
|
|
36
|
+
const ext = extension(attachment.name);
|
|
37
|
+
return ['txt', 'csv', 'md', 'html', 'json'].includes(ext);
|
|
38
|
+
}
|
|
39
|
+
function shouldDownload(attachment) {
|
|
40
|
+
return Boolean(attachment.path) && !isMentionOnly(attachment);
|
|
41
|
+
}
|
|
42
|
+
async function extractTextFromBytes(bytes, name, type) {
|
|
43
|
+
if (isUtf8Text({ type, name })) {
|
|
44
|
+
return new TextDecoder('utf-8', { fatal: false })
|
|
45
|
+
.decode(bytes)
|
|
46
|
+
.slice(0, MAX_EXTRACTED_CHARS);
|
|
47
|
+
}
|
|
48
|
+
if (isImage({ type }) || isPdf({ type, name })) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (isMentionOnly({ type, name })) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const ast = await OfficeParser.parseOffice(Buffer.from(bytes), {
|
|
56
|
+
includeRawContent: false,
|
|
57
|
+
});
|
|
58
|
+
const text = ast.toText().trim();
|
|
59
|
+
return text ? text.slice(0, MAX_EXTRACTED_CHARS) : null;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
logger.warn('Failed to extract chat attachment text', { name, type, error });
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Download bytes from GCS and extract text for every supported attachment type. */
|
|
67
|
+
export async function loadChatAttachments(attachments) {
|
|
68
|
+
return Promise.all(attachments.map(async (attachment) => {
|
|
69
|
+
if (!shouldDownload(attachment)) {
|
|
70
|
+
return { ...attachment };
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const [bytes] = await bucket.file(attachment.path).download();
|
|
74
|
+
const data = new Uint8Array(bytes);
|
|
75
|
+
const extractedText = (await extractTextFromBytes(data, attachment.name, attachment.type)) ??
|
|
76
|
+
undefined;
|
|
77
|
+
return { ...attachment, data, extractedText };
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
logger.warn('Failed to load chat attachment from storage', {
|
|
81
|
+
attachmentId: attachment.id,
|
|
82
|
+
path: attachment.path,
|
|
83
|
+
type: attachment.type,
|
|
84
|
+
error,
|
|
85
|
+
});
|
|
86
|
+
return { ...attachment };
|
|
87
|
+
}
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
/** @deprecated Use {@link loadChatAttachments} */
|
|
91
|
+
export const loadPromptAttachments = loadChatAttachments;
|
|
92
|
+
export function buildUserMessageContentWithAttachments(textContent, attachments) {
|
|
93
|
+
if (attachments.length === 0) {
|
|
94
|
+
return textContent;
|
|
95
|
+
}
|
|
96
|
+
const parts = [];
|
|
97
|
+
let fullText = textContent;
|
|
98
|
+
for (const attachment of attachments) {
|
|
99
|
+
if (attachment.extractedText) {
|
|
100
|
+
fullText += `\n\n[Content of ${attachment.name}]:\n${attachment.extractedText}`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const unhandled = attachments.filter((a) => {
|
|
104
|
+
if (a.extractedText)
|
|
105
|
+
return false;
|
|
106
|
+
if (isImage(a) && a.data)
|
|
107
|
+
return false;
|
|
108
|
+
if (isPdf(a) && a.data)
|
|
109
|
+
return false;
|
|
110
|
+
return true;
|
|
111
|
+
});
|
|
112
|
+
if (unhandled.length > 0) {
|
|
113
|
+
const fileList = unhandled
|
|
114
|
+
.map((a) => `${a.name} (${a.type})`)
|
|
115
|
+
.join(', ');
|
|
116
|
+
fullText = fullText
|
|
117
|
+
? `${fullText}\n[Attached files (content not extracted): ${fileList}]`
|
|
118
|
+
: `[Attached files (content not extracted): ${fileList}]`;
|
|
119
|
+
}
|
|
120
|
+
if (fullText.trim()) {
|
|
121
|
+
parts.push({ type: 'text', text: fullText });
|
|
122
|
+
}
|
|
123
|
+
for (const image of attachments) {
|
|
124
|
+
if (isImage(image) && image.data) {
|
|
125
|
+
parts.push({ type: 'image', image: image.data });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const file of attachments) {
|
|
129
|
+
if (isPdf(file) && file.data) {
|
|
130
|
+
parts.push({
|
|
131
|
+
type: 'file',
|
|
132
|
+
data: file.data,
|
|
133
|
+
mediaType: 'application/pdf',
|
|
134
|
+
filename: file.name,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (parts.length === 0) {
|
|
139
|
+
return textContent || '[Attached files]';
|
|
140
|
+
}
|
|
141
|
+
if (parts.length === 1 && parts[0].type === 'text') {
|
|
142
|
+
return parts[0].text;
|
|
143
|
+
}
|
|
144
|
+
return parts;
|
|
145
|
+
}
|
|
146
|
+
export function sortChatMessagesChronologically(messages) {
|
|
147
|
+
return messages
|
|
148
|
+
.map((message, index) => ({ message, index }))
|
|
149
|
+
.sort((left, right) => {
|
|
150
|
+
const leftTime = left.message.createdAt
|
|
151
|
+
? new Date(left.message.createdAt).getTime()
|
|
152
|
+
: null;
|
|
153
|
+
const rightTime = right.message.createdAt
|
|
154
|
+
? new Date(right.message.createdAt).getTime()
|
|
155
|
+
: null;
|
|
156
|
+
if (leftTime != null && rightTime != null) {
|
|
157
|
+
return leftTime - rightTime;
|
|
158
|
+
}
|
|
159
|
+
if (leftTime != null)
|
|
160
|
+
return -1;
|
|
161
|
+
if (rightTime != null)
|
|
162
|
+
return 1;
|
|
163
|
+
return left.index - right.index;
|
|
164
|
+
})
|
|
165
|
+
.map(({ message }) => message);
|
|
166
|
+
}
|
|
167
|
+
/** Build ModelMessage[] with shared attachment loading for Newton + Lab. */
|
|
168
|
+
export async function buildChatModelMessages({ messages, isAIUser, defaultSenderLabel, }) {
|
|
169
|
+
const modelMessages = [];
|
|
170
|
+
for (const message of sortChatMessagesChronologically(messages)) {
|
|
171
|
+
if (isAIUser(message.senderId)) {
|
|
172
|
+
modelMessages.push({ role: 'assistant', content: message.content });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const senderName = message.sender?.profile?.displayName ||
|
|
176
|
+
message.sender?.username ||
|
|
177
|
+
defaultSenderLabel;
|
|
178
|
+
const label = message.content.trim()
|
|
179
|
+
? `${senderName}: ${message.content}`
|
|
180
|
+
: senderName;
|
|
181
|
+
const attachments = await loadChatAttachments(message.attachments ?? []);
|
|
182
|
+
modelMessages.push({
|
|
183
|
+
role: 'user',
|
|
184
|
+
content: buildUserMessageContentWithAttachments(label, attachments),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return modelMessages;
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=chatAttachmentPrompt.js.map
|
|
190
|
+
//# debugId=b2f1ec36-e878-51c6-b49c-0473d9a71c3e
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatAttachmentPrompt.js","sources":["lib/chatAttachmentPrompt.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Shared chat attachment handling for Newton + Lab pipelines.\n *\n * - Images + PDFs: inlined as multimodal parts (bytes from GCS).\n * - Office docs, spreadsheets, slides, text, csv, json, html: text extracted\n * via officeparser / UTF-8 decode and appended to the user message.\n * - Audio/video/zip: filename mention only (no extraction).\n */\nimport type { FilePart, ImagePart, ModelMessage, TextPart, UserContent } from 'ai';\nimport { OfficeParser } from 'officeparser';\nimport { bucket } from './googleCloudStorage.js';\nimport { logger } from '../utils/logger.js';\n\n/** Model used for AI chats that may include document attachments. */\nexport const AI_CHAT_MODEL = 'gpt-5.5';\n\nconst MAX_EXTRACTED_CHARS = 80_000;\nconst IMAGE_TYPE_PREFIX = 'image/';\n\nexport type ChatAttachmentRef = {\n id: string;\n name: string;\n type: string;\n path: string;\n};\n\nexport type LoadedChatAttachment = ChatAttachmentRef & {\n data?: Uint8Array;\n extractedText?: string;\n};\n\nexport type ChatHistoryMessage = {\n id?: string;\n content: string;\n senderId: string;\n createdAt?: Date | string | number;\n sender?: {\n username?: string | null;\n profile?: { displayName?: string | null } | null;\n } | null;\n attachments?: ChatAttachmentRef[];\n};\n\ntype UserContentPart = TextPart | ImagePart | FilePart;\n\nconst TEXT_DECODE_TYPES = new Set([\n 'text/plain',\n 'text/csv',\n 'text/markdown',\n 'text/html',\n 'application/json',\n]);\n\nconst MENTION_ONLY_PREFIXES = ['audio/', 'video/'];\nconst MENTION_ONLY_TYPES = new Set(['application/zip']);\n\nfunction extension(name: string): string {\n return name.split('.').pop()?.toLowerCase() ?? '';\n}\n\nfunction isPdf(attachment: { type: string; name: string }): boolean {\n return attachment.type === 'application/pdf' || extension(attachment.name) === 'pdf';\n}\n\nfunction isImage(attachment: { type: string }): boolean {\n return attachment.type.startsWith(IMAGE_TYPE_PREFIX);\n}\n\nfunction isMentionOnly(attachment: { type: string; name: string }): boolean {\n if (MENTION_ONLY_TYPES.has(attachment.type)) return true;\n return MENTION_ONLY_PREFIXES.some((prefix) => attachment.type.startsWith(prefix));\n}\n\nfunction isUtf8Text(attachment: { type: string; name: string }): boolean {\n if (TEXT_DECODE_TYPES.has(attachment.type)) return true;\n const ext = extension(attachment.name);\n return ['txt', 'csv', 'md', 'html', 'json'].includes(ext);\n}\n\nfunction shouldDownload(attachment: ChatAttachmentRef): boolean {\n return Boolean(attachment.path) && !isMentionOnly(attachment);\n}\n\nasync function extractTextFromBytes(\n bytes: Uint8Array,\n name: string,\n type: string,\n): Promise<string | null> {\n if (isUtf8Text({ type, name })) {\n return new TextDecoder('utf-8', { fatal: false })\n .decode(bytes)\n .slice(0, MAX_EXTRACTED_CHARS);\n }\n\n if (isImage({ type }) || isPdf({ type, name })) {\n return null;\n }\n\n if (isMentionOnly({ type, name })) {\n return null;\n }\n\n try {\n const ast = await OfficeParser.parseOffice(Buffer.from(bytes), {\n includeRawContent: false,\n });\n const text = ast.toText().trim();\n return text ? text.slice(0, MAX_EXTRACTED_CHARS) : null;\n } catch (error) {\n logger.warn('Failed to extract chat attachment text', { name, type, error });\n return null;\n }\n}\n\n/** Download bytes from GCS and extract text for every supported attachment type. */\nexport async function loadChatAttachments(\n attachments: ChatAttachmentRef[],\n): Promise<LoadedChatAttachment[]> {\n return Promise.all(\n attachments.map(async (attachment) => {\n if (!shouldDownload(attachment)) {\n return { ...attachment };\n }\n\n try {\n const [bytes] = await bucket.file(attachment.path).download();\n const data = new Uint8Array(bytes);\n const extractedText =\n (await extractTextFromBytes(data, attachment.name, attachment.type)) ??\n undefined;\n return { ...attachment, data, extractedText };\n } catch (error) {\n logger.warn('Failed to load chat attachment from storage', {\n attachmentId: attachment.id,\n path: attachment.path,\n type: attachment.type,\n error,\n });\n return { ...attachment };\n }\n }),\n );\n}\n\n/** @deprecated Use {@link loadChatAttachments} */\nexport const loadPromptAttachments = loadChatAttachments;\n\nexport function buildUserMessageContentWithAttachments(\n textContent: string,\n attachments: LoadedChatAttachment[],\n): UserContent {\n if (attachments.length === 0) {\n return textContent;\n }\n\n const parts: UserContentPart[] = [];\n let fullText = textContent;\n\n for (const attachment of attachments) {\n if (attachment.extractedText) {\n fullText += `\\n\\n[Content of ${attachment.name}]:\\n${attachment.extractedText}`;\n }\n }\n\n const unhandled = attachments.filter((a) => {\n if (a.extractedText) return false;\n if (isImage(a) && a.data) return false;\n if (isPdf(a) && a.data) return false;\n return true;\n });\n\n if (unhandled.length > 0) {\n const fileList = unhandled\n .map((a) => `${a.name} (${a.type})`)\n .join(', ');\n fullText = fullText\n ? `${fullText}\\n[Attached files (content not extracted): ${fileList}]`\n : `[Attached files (content not extracted): ${fileList}]`;\n }\n\n if (fullText.trim()) {\n parts.push({ type: 'text', text: fullText });\n }\n\n for (const image of attachments) {\n if (isImage(image) && image.data) {\n parts.push({ type: 'image', image: image.data });\n }\n }\n\n for (const file of attachments) {\n if (isPdf(file) && file.data) {\n parts.push({\n type: 'file',\n data: file.data,\n mediaType: 'application/pdf',\n filename: file.name,\n });\n }\n }\n\n if (parts.length === 0) {\n return textContent || '[Attached files]';\n }\n\n if (parts.length === 1 && parts[0].type === 'text') {\n return parts[0].text;\n }\n\n return parts;\n}\n\nexport function sortChatMessagesChronologically<T extends ChatHistoryMessage>(\n messages: T[],\n): T[] {\n return messages\n .map((message, index) => ({ message, index }))\n .sort((left, right) => {\n const leftTime = left.message.createdAt\n ? new Date(left.message.createdAt).getTime()\n : null;\n const rightTime = right.message.createdAt\n ? new Date(right.message.createdAt).getTime()\n : null;\n\n if (leftTime != null && rightTime != null) {\n return leftTime - rightTime;\n }\n if (leftTime != null) return -1;\n if (rightTime != null) return 1;\n return left.index - right.index;\n })\n .map(({ message }) => message);\n}\n\n/** Build ModelMessage[] with shared attachment loading for Newton + Lab. */\nexport async function buildChatModelMessages({\n messages,\n isAIUser,\n defaultSenderLabel,\n}: {\n messages: ChatHistoryMessage[];\n isAIUser: (senderId: string) => boolean;\n defaultSenderLabel: string;\n}): Promise<ModelMessage[]> {\n const modelMessages: ModelMessage[] = [];\n\n for (const message of sortChatMessagesChronologically(messages)) {\n if (isAIUser(message.senderId)) {\n modelMessages.push({ role: 'assistant', content: message.content });\n continue;\n }\n\n const senderName =\n message.sender?.profile?.displayName ||\n message.sender?.username ||\n defaultSenderLabel;\n const label = message.content.trim()\n ? `${senderName}: ${message.content}`\n : senderName;\n const attachments = await loadChatAttachments(message.attachments ?? []);\n\n modelMessages.push({\n role: 'user',\n content: buildUserMessageContentWithAttachments(label, attachments),\n });\n }\n\n return modelMessages;\n}\n"],"names":[],"mappings":"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","debug_id":"b2f1ec36-e878-51c6-b49c-0473d9a71c3e"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="64850cb0-dd7d-5df3-8b52-d0a2d8e27737")}catch(e){}}();
|
|
3
3
|
import { normalizeDocumentFormat } from '../services/openaiDocumentGen.js';
|
|
4
4
|
/** Ordered most-specific first. */
|
|
5
5
|
const FORMAT_RULES = [
|
|
@@ -7,7 +7,7 @@ const FORMAT_RULES = [
|
|
|
7
7
|
{ format: 'xlsx', pattern: /\b(spreadsheet|excel|workbook|xlsx|gradebook|grade book|\.xlsx)\b/i },
|
|
8
8
|
{ format: 'csv', pattern: /\b(csv|comma[- ]separated|\.csv)\b/i },
|
|
9
9
|
{ format: 'docx', pattern: /\b(word doc(ument)?|docx|editable handout|\.docx)\b/i },
|
|
10
|
-
{ format: 'png', pattern: /\b(diagram|chart|graph|plot|figure|infographic|\.png)\b/i },
|
|
10
|
+
{ format: 'png', pattern: /\b(diagram|flowchart|chart|graph|plot|figure|infographic|visual|illustration|mind ?map|timeline|\.png)\b/i },
|
|
11
11
|
{ format: 'md', pattern: /\b(markdown|readme|\.md)\b/i },
|
|
12
12
|
{ format: 'html', pattern: /\b(html|web ?page|\.html)\b/i },
|
|
13
13
|
{ format: 'txt', pattern: /\b(plain text|text file|\.txt)\b/i },
|
|
@@ -45,4 +45,4 @@ export function inferDocumentFormat(title, content, explicit, userRequest) {
|
|
|
45
45
|
return explicitNorm ?? 'pdf';
|
|
46
46
|
}
|
|
47
47
|
//# sourceMappingURL=inferDocumentFormat.js.map
|
|
48
|
-
//# debugId=
|
|
48
|
+
//# debugId=64850cb0-dd7d-5df3-8b52-d0a2d8e27737
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inferDocumentFormat.js","sources":["lib/inferDocumentFormat.ts"],"sourceRoot":"/","sourcesContent":["import type { DocumentFormat } from '../services/openaiDocumentGen.js';\nimport { normalizeDocumentFormat } from '../services/openaiDocumentGen.js';\n\ntype FormatRule = { format: DocumentFormat; pattern: RegExp };\n\n/** Ordered most-specific first. */\nconst FORMAT_RULES: FormatRule[] = [\n { format: 'pptx', pattern: /\\b(presentation|powerpoint|power point|slides?|slide deck|deck|pptx|\\.pptx)\\b/i },\n { format: 'xlsx', pattern: /\\b(spreadsheet|excel|workbook|xlsx|gradebook|grade book|\\.xlsx)\\b/i },\n { format: 'csv', pattern: /\\b(csv|comma[- ]separated|\\.csv)\\b/i },\n { format: 'docx', pattern: /\\b(word doc(ument)?|docx|editable handout|\\.docx)\\b/i },\n { format: 'png', pattern: /\\b(diagram|chart|graph|plot|figure|infographic|\\.png)\\b/i },\n { format: 'md', pattern: /\\b(markdown|readme|\\.md)\\b/i },\n { format: 'html', pattern: /\\b(html|web ?page|\\.html)\\b/i },\n { format: 'txt', pattern: /\\b(plain text|text file|\\.txt)\\b/i },\n];\n\nfunction inferFromText(...parts: Array<string | null | undefined>): DocumentFormat | null {\n const hay = parts.filter(Boolean).join('\\n');\n if (!hay.trim()) return null;\n\n for (const { format, pattern } of FORMAT_RULES) {\n if (pattern.test(hay)) return format;\n }\n return null;\n}\n\n/**\n * Resolve the output format for a generated document.\n *\n * The lab model often passes `format: \"pdf\"` even for presentations/spreadsheets.\n * Keyword signals from the teacher request, title, and body take precedence over\n * a generic pdf default; a non-pdf explicit format is always respected.\n */\nexport function inferDocumentFormat(\n title: string,\n content: string,\n explicit?: string | null,\n userRequest?: string | null,\n): DocumentFormat {\n const explicitNorm = explicit?.trim()\n ? normalizeDocumentFormat(explicit)\n : null;\n\n const fromKeywords = inferFromText(userRequest, title, content);\n\n // Non-pdf explicit choice wins (model or tool args).\n if (explicitNorm && explicitNorm !== 'pdf') {\n return explicitNorm;\n }\n\n // Override pdf when the request clearly asks for slides, spreadsheets, etc.\n if (fromKeywords && fromKeywords !== 'pdf') {\n return fromKeywords;\n }\n\n return explicitNorm ?? 'pdf';\n}\n"],"names":[],"mappings":";;AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAI3E,mCAAmC;AACnC,MAAM,YAAY,GAAiB;IACjC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,gFAAgF,EAAE;IAC7G,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,oEAAoE,EAAE;IACjG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,qCAAqC,EAAE;IACjE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,sDAAsD,EAAE;IACnF,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"inferDocumentFormat.js","sources":["lib/inferDocumentFormat.ts"],"sourceRoot":"/","sourcesContent":["import type { DocumentFormat } from '../services/openaiDocumentGen.js';\nimport { normalizeDocumentFormat } from '../services/openaiDocumentGen.js';\n\ntype FormatRule = { format: DocumentFormat; pattern: RegExp };\n\n/** Ordered most-specific first. */\nconst FORMAT_RULES: FormatRule[] = [\n { format: 'pptx', pattern: /\\b(presentation|powerpoint|power point|slides?|slide deck|deck|pptx|\\.pptx)\\b/i },\n { format: 'xlsx', pattern: /\\b(spreadsheet|excel|workbook|xlsx|gradebook|grade book|\\.xlsx)\\b/i },\n { format: 'csv', pattern: /\\b(csv|comma[- ]separated|\\.csv)\\b/i },\n { format: 'docx', pattern: /\\b(word doc(ument)?|docx|editable handout|\\.docx)\\b/i },\n { format: 'png', pattern: /\\b(diagram|flowchart|chart|graph|plot|figure|infographic|visual|illustration|mind ?map|timeline|\\.png)\\b/i },\n { format: 'md', pattern: /\\b(markdown|readme|\\.md)\\b/i },\n { format: 'html', pattern: /\\b(html|web ?page|\\.html)\\b/i },\n { format: 'txt', pattern: /\\b(plain text|text file|\\.txt)\\b/i },\n];\n\nfunction inferFromText(...parts: Array<string | null | undefined>): DocumentFormat | null {\n const hay = parts.filter(Boolean).join('\\n');\n if (!hay.trim()) return null;\n\n for (const { format, pattern } of FORMAT_RULES) {\n if (pattern.test(hay)) return format;\n }\n return null;\n}\n\n/**\n * Resolve the output format for a generated document.\n *\n * The lab model often passes `format: \"pdf\"` even for presentations/spreadsheets.\n * Keyword signals from the teacher request, title, and body take precedence over\n * a generic pdf default; a non-pdf explicit format is always respected.\n */\nexport function inferDocumentFormat(\n title: string,\n content: string,\n explicit?: string | null,\n userRequest?: string | null,\n): DocumentFormat {\n const explicitNorm = explicit?.trim()\n ? normalizeDocumentFormat(explicit)\n : null;\n\n const fromKeywords = inferFromText(userRequest, title, content);\n\n // Non-pdf explicit choice wins (model or tool args).\n if (explicitNorm && explicitNorm !== 'pdf') {\n return explicitNorm;\n }\n\n // Override pdf when the request clearly asks for slides, spreadsheets, etc.\n if (fromKeywords && fromKeywords !== 'pdf') {\n return fromKeywords;\n }\n\n return explicitNorm ?? 'pdf';\n}\n"],"names":[],"mappings":";;AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAI3E,mCAAmC;AACnC,MAAM,YAAY,GAAiB;IACjC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,gFAAgF,EAAE;IAC7G,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,oEAAoE,EAAE;IACjG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,qCAAqC,EAAE;IACjE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,sDAAsD,EAAE;IACnF,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,2GAA2G,EAAE;IACvI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,6BAA6B,EAAE;IACxD,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,8BAA8B,EAAE;IAC3D,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,mCAAmC,EAAE;CAChE,CAAC;AAEF,SAAS,aAAa,CAAC,GAAG,KAAuC;IAC/D,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7C,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAAE,OAAO,IAAI,CAAC;IAE7B,KAAK,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,YAAY,EAAE,CAAC;QAC/C,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,MAAM,CAAC;IACvC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAa,EACb,OAAe,EACf,QAAwB,EACxB,WAA2B;IAE3B,MAAM,YAAY,GAAG,QAAQ,EAAE,IAAI,EAAE;QACnC,CAAC,CAAC,uBAAuB,CAAC,QAAQ,CAAC;QACnC,CAAC,CAAC,IAAI,CAAC;IAET,MAAM,YAAY,GAAG,aAAa,CAAC,WAAW,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IAEhE,qDAAqD;IACrD,IAAI,YAAY,IAAI,YAAY,KAAK,KAAK,EAAE,CAAC;QAC3C,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,4EAA4E;IAC5E,IAAI,YAAY,IAAI,YAAY,KAAK,KAAK,EAAE,CAAC;QAC3C,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,OAAO,YAAY,IAAI,KAAK,CAAC;AAC/B,CAAC","debug_id":"64850cb0-dd7d-5df3-8b52-d0a2d8e27737"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aiLabChat.d.ts","sourceRoot":"/","sources":["pipelines/aiLabChat.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAmD7E,wDAAwD;AACxD,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,WAAW,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAClD,iBAAiB,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACxD,UAAU,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAClE,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE;YAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,GAAG,IAAI,CAAA;KAAE,EAAE,CAAC;IAC9F,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE;YAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,GAAG,IAAI,CAAA;KAAE,EAAE,CAAC;IAC9F,WAAW,EAAE,CAAC,UAAU,GAAG;QACzB,OAAO,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,GAAG,IAAI,CAAC;QACrE,UAAU,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAC;QACnC,eAAe,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAC;KACzC,CAAC,EAAE,CAAC;CACN,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,MAAM,EAAE;QACN,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,OAAO,EAAE;YACP,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;SAC5B,GAAG,IAAI,CAAC;KACV,GAAG,IAAI,CAAC;IACT,WAAW,EAAE;QACX,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;KACd,EAAE,CAAC;CACL,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,GACrC,UAAU,oBAAoB,EAAE,EAChC,kBAAkB,MAAM,EACxB,cAAU,2BAYX,CAAC;
|
|
1
|
+
{"version":3,"file":"aiLabChat.d.ts","sourceRoot":"/","sources":["pipelines/aiLabChat.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,17 +3,18 @@
|
|
|
3
3
|
* Can create worksheets, sections, assignments, and generated files from AI output.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
6
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="34f3e5c6-5811-5644-b688-13b9a35600d2")}catch(e){}}();
|
|
7
7
|
import { isAIUser } from "../utils/aiUser.js";
|
|
8
8
|
import { prisma } from "../lib/prisma.js";
|
|
9
9
|
import { pusher, chatChannel, teacherChannel } from "../lib/pusher.js";
|
|
10
10
|
import { completePendingAIMessage, failPendingAIMessage, } from "../utils/inference.js";
|
|
11
11
|
import { logger } from "../utils/logger.js";
|
|
12
12
|
import { v4 } from "uuid";
|
|
13
|
-
import { bucket
|
|
13
|
+
import { bucket } from "../lib/googleCloudStorage.js";
|
|
14
14
|
import { generateDocumentWithOpenAI, normalizeDocumentFormat, } from "../services/openaiDocumentGen.js";
|
|
15
15
|
import { inferDocumentFormat } from "../lib/inferDocumentFormat.js";
|
|
16
16
|
import { broadcastTextChunks, broadcastStreamComplete } from "../utils/streamBroadcast.js";
|
|
17
|
+
import { AI_CHAT_MODEL } from "../lib/chatAttachmentPrompt.js";
|
|
17
18
|
import { buildLabChatAgentMessages } from "./labChatPrompt.js";
|
|
18
19
|
import { applyResolvedFormatsToStreamParts, enrichStreamPartsWithGeneratedFiles, finalizeDeferredToolParts, streamChatResponse, } from "../services/aiChatStream.js";
|
|
19
20
|
import { aiTools } from "../services/aiTools.js";
|
|
@@ -135,34 +136,6 @@ const loadRecentLabChatMessages = async (conversationId, anchorMessageId) => {
|
|
|
135
136
|
})
|
|
136
137
|
.slice(0, limit);
|
|
137
138
|
};
|
|
138
|
-
/**
|
|
139
|
-
* Resolves signed URLs for image attachments in messages so the LLM can see them.
|
|
140
|
-
* Non-image files are passed as text descriptions only.
|
|
141
|
-
*/
|
|
142
|
-
async function resolveAttachmentUrls(messages) {
|
|
143
|
-
const resolved = [];
|
|
144
|
-
for (const msg of messages) {
|
|
145
|
-
const attachments = msg.attachments ?? [];
|
|
146
|
-
if (attachments.length === 0) {
|
|
147
|
-
resolved.push({ ...msg, attachments: [] });
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
const withUrls = await Promise.all(attachments.map(async (a) => {
|
|
151
|
-
if (a.type.startsWith('image/')) {
|
|
152
|
-
try {
|
|
153
|
-
const url = await getSignedUrl(a.path);
|
|
154
|
-
return { id: a.id, name: a.name, type: a.type, url };
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
return { id: a.id, name: a.name, type: a.type };
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return { id: a.id, name: a.name, type: a.type };
|
|
161
|
-
}));
|
|
162
|
-
resolved.push({ ...msg, attachments: withUrls });
|
|
163
|
-
}
|
|
164
|
-
return resolved;
|
|
165
|
-
}
|
|
166
139
|
/**
|
|
167
140
|
* Builds schema-aware context for the AI from class data.
|
|
168
141
|
* Formats entities with IDs so the model can reference them when creating assignments.
|
|
@@ -385,12 +358,10 @@ export const generateAndSendLabResponse = async (labChatId, teacherMessage, emit
|
|
|
385
358
|
teachers: classData.teachers,
|
|
386
359
|
assignments: classData.assignments,
|
|
387
360
|
});
|
|
388
|
-
|
|
389
|
-
const messagesWithAttachments = await resolveAttachmentUrls(recentMessages.reverse());
|
|
390
|
-
const { system, messages } = buildLabChatAgentMessages({
|
|
361
|
+
const { system, messages } = await buildLabChatAgentMessages({
|
|
391
362
|
context: fullLabChat.context,
|
|
392
363
|
classContext,
|
|
393
|
-
recentMessages:
|
|
364
|
+
recentMessages: recentMessages.reverse(),
|
|
394
365
|
isAIUser,
|
|
395
366
|
});
|
|
396
367
|
// Genuine agentic generation: the model streams prose token-by-token and
|
|
@@ -405,6 +376,7 @@ export const generateAndSendLabResponse = async (labChatId, teacherMessage, emit
|
|
|
405
376
|
const { text: streamedText, parts: rawStreamParts } = await withTimeout(streamChatResponse({
|
|
406
377
|
conversationId,
|
|
407
378
|
aiMessageId: emitOptions.aiMessageId,
|
|
379
|
+
model: AI_CHAT_MODEL,
|
|
408
380
|
system,
|
|
409
381
|
messages,
|
|
410
382
|
tools,
|
|
@@ -585,4 +557,4 @@ export const generateAndSendLabResponse = async (labChatId, teacherMessage, emit
|
|
|
585
557
|
}
|
|
586
558
|
};
|
|
587
559
|
//# sourceMappingURL=aiLabChat.js.map
|
|
588
|
-
//# debugId=
|
|
560
|
+
//# debugId=34f3e5c6-5811-5644-b688-13b9a35600d2
|