@zhouchangui/math-ati 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.local.example +6 -0
- package/AGENTS.md +273 -0
- package/README.md +34 -0
- package/bin/math-ati.js +194 -0
- package/dist/assets/index-BYFoutza.js +22 -0
- package/dist/assets/index-Bk2WFPoL.css +1 -0
- package/dist/index.html +13 -0
- package/package.json +72 -0
- package/prompts/grading.system.md +129 -0
- package/prompts/knowledge-extract.system.md +123 -0
- package/prompts/knowledge-summarize.system.md +127 -0
- package/prompts/learning-summary.system.md +123 -0
- package/prompts/pdf-grading.system.md +80 -0
- package/prompts/pdf-page-extract.system.md +52 -0
- package/prompts/pdf-recheck.system.md +43 -0
- package/prompts/practice-generate.system.md +161 -0
- package/prompts/practice-review.system.md +65 -0
- package/prompts/practice-revise.system.md +56 -0
- package/server/abilityService.js +259 -0
- package/server/agentClient.js +202 -0
- package/server/env.js +4 -0
- package/server/fileStore.js +726 -0
- package/server/grading.js +116 -0
- package/server/index.js +655 -0
- package/server/jobStore.js +169 -0
- package/server/knowledgeBase.js +30 -0
- package/server/knowledgeExtractor.js +360 -0
- package/server/knowledgeFeedback.js +299 -0
- package/server/llmConfig.js +96 -0
- package/server/mistakeLifecycle.js +251 -0
- package/server/pdfSubmissionGrader.js +846 -0
- package/server/practiceGenerator.js +908 -0
- package/server/practicePaperHtml.js +313 -0
- package/server/practiceReviewer.js +307 -0
- package/server/practiceService.js +331 -0
- package/server/promptStore.js +16 -0
- package/server/submissionService.js +184 -0
- package/templates/workspace/.env.local.example +6 -0
- package/templates/workspace/data/global/ability_index.json +5 -0
- package/templates/workspace/data/global/chapters.json +621 -0
- package/templates/workspace/data/global/mastery_index.json +6 -0
- package/templates/workspace/data/global/mistakes_index.json +7 -0
- package/templates/workspace/data/global/student_profile.json +11 -0
- package/templates/workspace/data/knowledge_points.json +1264 -0
- package/templates/workspace/data/mistakes.json +1 -0
- package/vite.config.js +21 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chapterDataPaths,
|
|
3
|
+
paths,
|
|
4
|
+
readJson,
|
|
5
|
+
writeJson
|
|
6
|
+
} from './fileStore.js';
|
|
7
|
+
|
|
8
|
+
export const ABILITY_CATALOG = [
|
|
9
|
+
{
|
|
10
|
+
id: 'calculation_accuracy',
|
|
11
|
+
title: '计算能力',
|
|
12
|
+
description: '评估本章相关计算的准确性与稳定性。',
|
|
13
|
+
category: 'calculation',
|
|
14
|
+
passThreshold: 0.9,
|
|
15
|
+
requiredConsecutivePasses: 2,
|
|
16
|
+
firstVersionScope: '只评估正确率、错误类型和连续达标;暂不考虑用时。'
|
|
17
|
+
}
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const DEFAULT_ASSESSMENT_FLOWS = {
|
|
21
|
+
knowledge: true,
|
|
22
|
+
mistakeRepair: true,
|
|
23
|
+
abilities: []
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function normalizeAbilityIds(ids = []) {
|
|
27
|
+
const known = new Set(ABILITY_CATALOG.map((ability) => ability.id));
|
|
28
|
+
return [...new Set(ids.filter((id) => known.has(id)))];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function normalizeAssessmentFlows(chapter = {}) {
|
|
32
|
+
const flows = chapter.assessmentFlows || {};
|
|
33
|
+
return {
|
|
34
|
+
knowledge: flows.knowledge !== false,
|
|
35
|
+
mistakeRepair: flows.mistakeRepair !== false,
|
|
36
|
+
abilities: normalizeAbilityIds(Array.isArray(flows.abilities) ? flows.abilities : DEFAULT_ASSESSMENT_FLOWS.abilities)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function initialAbilityState(abilityId) {
|
|
41
|
+
const catalog = ABILITY_CATALOG.find((item) => item.id === abilityId);
|
|
42
|
+
return {
|
|
43
|
+
id: abilityId,
|
|
44
|
+
title: catalog?.title || abilityId,
|
|
45
|
+
status: 'not_started',
|
|
46
|
+
assessmentCount: 0,
|
|
47
|
+
correctCount: 0,
|
|
48
|
+
wrongCount: 0,
|
|
49
|
+
consecutivePasses: 0,
|
|
50
|
+
lastAssessmentPracticeId: null,
|
|
51
|
+
lastSubmissionId: null,
|
|
52
|
+
lastUpdatedAt: null,
|
|
53
|
+
lastAccuracy: null,
|
|
54
|
+
errorTypes: []
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function enabledAbilityIdsForChapter(chapterId) {
|
|
59
|
+
const state = await buildChapterAbilities(chapterId);
|
|
60
|
+
return state.enabledAbilityIds;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function assertChapterAbilitiesEnabled(chapterId, requestedAbilityIds = []) {
|
|
64
|
+
const enabledAbilityIds = await enabledAbilityIdsForChapter(chapterId);
|
|
65
|
+
const enabled = new Set(enabledAbilityIds);
|
|
66
|
+
const requested = Array.isArray(requestedAbilityIds) && requestedAbilityIds.length
|
|
67
|
+
? [...new Set(requestedAbilityIds)]
|
|
68
|
+
: enabledAbilityIds;
|
|
69
|
+
const unknown = requested.filter((id) => !ABILITY_CATALOG.some((ability) => ability.id === id));
|
|
70
|
+
if (unknown.length) {
|
|
71
|
+
const error = new Error(`unknown_ability:${unknown.join(',')}`);
|
|
72
|
+
error.status = 422;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
const disabled = requested.filter((id) => !enabled.has(id));
|
|
76
|
+
if (disabled.length) {
|
|
77
|
+
const error = new Error(`ability_not_enabled:${disabled.join(',')}`);
|
|
78
|
+
error.status = 422;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
if (!requested.length) {
|
|
82
|
+
const error = new Error(`ability_assessment_not_enabled:${chapterId}`);
|
|
83
|
+
error.status = 422;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
return requested;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function plainLabel(value) {
|
|
90
|
+
return String(value ?? '')
|
|
91
|
+
.replace(/\$\$([^$]+)\$\$/g, '$1')
|
|
92
|
+
.replace(/\$([^$]+)\$/g, '$1')
|
|
93
|
+
.replace(/\\\((.*?)\\\)/g, '$1')
|
|
94
|
+
.replace(/\\\[(.*?)\\\]/g, '$1')
|
|
95
|
+
.replace(/\\left|\\right/g, '')
|
|
96
|
+
.replace(/\\frac\{([^{}]+)\}\{([^{}]+)\}/g, '$1/$2')
|
|
97
|
+
.replace(/<[^>]+>/g, '')
|
|
98
|
+
.replace(/\s+/g, ' ')
|
|
99
|
+
.trim();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function abilityIdsForQuestion(question, practiceAbilityIds) {
|
|
103
|
+
const ids = Array.isArray(question.abilityIds) && question.abilityIds.length
|
|
104
|
+
? question.abilityIds
|
|
105
|
+
: practiceAbilityIds;
|
|
106
|
+
return normalizeAbilityIds(ids);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isCorrectGrade(item) {
|
|
110
|
+
if (item?.status === 'correct') return true;
|
|
111
|
+
const maxScore = Number(item?.maxScore || 0);
|
|
112
|
+
const score = Number(item?.score || 0);
|
|
113
|
+
return maxScore > 0 && score >= maxScore;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function mergeUniqueLabels(...groups) {
|
|
117
|
+
return [...new Set(groups.flat().map(plainLabel).filter(Boolean))].slice(0, 12);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function buildChapterAbilities(chapterId) {
|
|
121
|
+
const chapters = await readJson(paths.chapters, []);
|
|
122
|
+
const chapter = chapters.find((item) => item.id === chapterId);
|
|
123
|
+
if (!chapter) {
|
|
124
|
+
const error = new Error(`chapter_not_found:${chapterId}`);
|
|
125
|
+
error.status = 404;
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
const assessmentFlows = normalizeAssessmentFlows(chapter);
|
|
129
|
+
const chapterPaths = chapterDataPaths(chapterId);
|
|
130
|
+
const existing = await readJson(chapterPaths.abilities, null);
|
|
131
|
+
const existingAbilities = existing?.abilities || {};
|
|
132
|
+
const abilities = Object.fromEntries(assessmentFlows.abilities.map((abilityId) => [
|
|
133
|
+
abilityId,
|
|
134
|
+
{
|
|
135
|
+
...initialAbilityState(abilityId),
|
|
136
|
+
...(existingAbilities[abilityId] || {}),
|
|
137
|
+
id: abilityId,
|
|
138
|
+
title: ABILITY_CATALOG.find((item) => item.id === abilityId)?.title || abilityId
|
|
139
|
+
}
|
|
140
|
+
]));
|
|
141
|
+
return {
|
|
142
|
+
chapterId,
|
|
143
|
+
chapterTitle: chapter.fullTitle || chapter.title || chapterId,
|
|
144
|
+
updatedAt: existing?.updatedAt || null,
|
|
145
|
+
assessmentFlows,
|
|
146
|
+
catalog: ABILITY_CATALOG,
|
|
147
|
+
enabledAbilityIds: assessmentFlows.abilities,
|
|
148
|
+
abilities
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function syncChapterAbilities(chapterId) {
|
|
153
|
+
const state = {
|
|
154
|
+
...(await buildChapterAbilities(chapterId)),
|
|
155
|
+
updatedAt: new Date().toISOString()
|
|
156
|
+
};
|
|
157
|
+
await writeJson(chapterDataPaths(chapterId).abilities, state);
|
|
158
|
+
return state;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function buildAbilityIndex() {
|
|
162
|
+
const chapters = await readJson(paths.chapters, []);
|
|
163
|
+
const chapterStates = {};
|
|
164
|
+
for (const chapter of chapters) {
|
|
165
|
+
const state = await buildChapterAbilities(chapter.id);
|
|
166
|
+
if (!state.enabledAbilityIds.length) continue;
|
|
167
|
+
chapterStates[chapter.id] = {
|
|
168
|
+
chapterTitle: state.chapterTitle,
|
|
169
|
+
enabledAbilityIds: state.enabledAbilityIds,
|
|
170
|
+
abilities: state.abilities,
|
|
171
|
+
updatedAt: state.updatedAt
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
version: 1,
|
|
176
|
+
updatedAt: new Date().toISOString(),
|
|
177
|
+
source: 'chapter-abilities',
|
|
178
|
+
catalog: ABILITY_CATALOG,
|
|
179
|
+
chapters: chapterStates
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function syncAbilityIndex() {
|
|
184
|
+
const index = await buildAbilityIndex();
|
|
185
|
+
await writeJson(paths.globalAbilityIndex, index);
|
|
186
|
+
return index;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function updateAbilitiesAfterArchive({ practice, grading = [], submissionId }) {
|
|
190
|
+
if (practice?.type !== 'ability_assessment') return null;
|
|
191
|
+
const requestedAbilityIds = await assertChapterAbilitiesEnabled(practice.chapterId, practice.abilityIds || []);
|
|
192
|
+
const state = await buildChapterAbilities(practice.chapterId);
|
|
193
|
+
const gradingByQuestionId = new Map((grading || []).map((item) => [item.questionId, item]));
|
|
194
|
+
const now = new Date().toISOString();
|
|
195
|
+
const nextAbilities = { ...state.abilities };
|
|
196
|
+
|
|
197
|
+
for (const abilityId of requestedAbilityIds) {
|
|
198
|
+
const catalog = ABILITY_CATALOG.find((item) => item.id === abilityId);
|
|
199
|
+
const relatedQuestions = (practice.questions || []).filter((question) =>
|
|
200
|
+
abilityIdsForQuestion(question, requestedAbilityIds).includes(abilityId)
|
|
201
|
+
);
|
|
202
|
+
const relatedGrades = relatedQuestions
|
|
203
|
+
.map((question) => ({
|
|
204
|
+
question,
|
|
205
|
+
grade: gradingByQuestionId.get(question.id)
|
|
206
|
+
}))
|
|
207
|
+
.filter((item) => item.grade);
|
|
208
|
+
const previous = nextAbilities[abilityId] || initialAbilityState(abilityId);
|
|
209
|
+
if (!relatedGrades.length) {
|
|
210
|
+
nextAbilities[abilityId] = {
|
|
211
|
+
...previous,
|
|
212
|
+
lastAssessmentPracticeId: practice.id,
|
|
213
|
+
lastSubmissionId: submissionId,
|
|
214
|
+
lastUpdatedAt: now
|
|
215
|
+
};
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const totalScore = relatedGrades.reduce((sum, item) => sum + Number(item.grade.maxScore || item.question.score || 0), 0);
|
|
219
|
+
const earnedScore = relatedGrades.reduce((sum, item) => sum + Number(item.grade.score || 0), 0);
|
|
220
|
+
const correctItems = relatedGrades.filter((item) => isCorrectGrade(item.grade)).length;
|
|
221
|
+
const weakItems = relatedGrades.length - correctItems;
|
|
222
|
+
const accuracy = totalScore > 0 ? earnedScore / totalScore : 0;
|
|
223
|
+
const passed = accuracy >= Number(catalog?.passThreshold || 0.9);
|
|
224
|
+
const consecutivePasses = passed ? Number(previous.consecutivePasses || 0) + 1 : 0;
|
|
225
|
+
const status = consecutivePasses >= Number(catalog?.requiredConsecutivePasses || 2)
|
|
226
|
+
? 'mastered'
|
|
227
|
+
: passed
|
|
228
|
+
? 'in_progress'
|
|
229
|
+
: 'needs_review';
|
|
230
|
+
const gradingErrors = relatedGrades.flatMap((item) => Array.isArray(item.grade.errorTypes) ? item.grade.errorTypes : []);
|
|
231
|
+
const expectedErrors = relatedGrades
|
|
232
|
+
.filter((item) => !isCorrectGrade(item.grade))
|
|
233
|
+
.flatMap((item) => Array.isArray(item.question.expectedAbilityErrors) ? item.question.expectedAbilityErrors : []);
|
|
234
|
+
nextAbilities[abilityId] = {
|
|
235
|
+
...previous,
|
|
236
|
+
id: abilityId,
|
|
237
|
+
title: catalog?.title || previous.title || abilityId,
|
|
238
|
+
status,
|
|
239
|
+
assessmentCount: Number(previous.assessmentCount || 0) + 1,
|
|
240
|
+
correctCount: Number(previous.correctCount || 0) + correctItems,
|
|
241
|
+
wrongCount: Number(previous.wrongCount || 0) + weakItems,
|
|
242
|
+
consecutivePasses,
|
|
243
|
+
lastAssessmentPracticeId: practice.id,
|
|
244
|
+
lastSubmissionId: submissionId,
|
|
245
|
+
lastUpdatedAt: now,
|
|
246
|
+
lastAccuracy: Math.round(accuracy * 100),
|
|
247
|
+
errorTypes: mergeUniqueLabels(previous.errorTypes || [], gradingErrors, expectedErrors)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const nextState = {
|
|
252
|
+
...state,
|
|
253
|
+
updatedAt: now,
|
|
254
|
+
abilities: nextAbilities
|
|
255
|
+
};
|
|
256
|
+
await writeJson(chapterDataPaths(practice.chapterId).abilities, nextState);
|
|
257
|
+
await syncAbilityIndex();
|
|
258
|
+
return nextState;
|
|
259
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getLlmRuntimeConfig } from './llmConfig.js';
|
|
4
|
+
|
|
5
|
+
function mimeForFile(filePath) {
|
|
6
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
7
|
+
if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
|
|
8
|
+
if (ext === '.webp') return 'image/webp';
|
|
9
|
+
return 'image/png';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sleep(ms) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fixtureMode() {
|
|
17
|
+
return process.env.NODE_ENV === 'test' ? process.env.MATH_AGENT_AGENT_FIXTURE || '' : '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function knowledgeExtractPageFixture() {
|
|
21
|
+
return {
|
|
22
|
+
ok: true,
|
|
23
|
+
data: {
|
|
24
|
+
pageTitle: '测试页知识点',
|
|
25
|
+
rawOutline: ['测试用章节知识点提取流程'],
|
|
26
|
+
knowledgePoints: [
|
|
27
|
+
{
|
|
28
|
+
title: '有理数的运算核心知识点',
|
|
29
|
+
summary: '理解有理数加减乘除的运算顺序和符号处理。',
|
|
30
|
+
formulas: [],
|
|
31
|
+
examples: ['-3 + 5 = 2'],
|
|
32
|
+
prerequisite: '有理数的概念',
|
|
33
|
+
difficulty: 'basic'
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
easyMistakes: [
|
|
37
|
+
{
|
|
38
|
+
title: '符号处理错误',
|
|
39
|
+
errorType: '符号处理错误',
|
|
40
|
+
description: '负数参与运算时容易漏写负号。',
|
|
41
|
+
correction: '先确定符号,再计算绝对值。'
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
exerciseHints: ['设计一道有理数混合运算题。']
|
|
45
|
+
},
|
|
46
|
+
attempts: 1
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function knowledgeSummarizeFixture() {
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
data: {
|
|
54
|
+
sections: [
|
|
55
|
+
{
|
|
56
|
+
title: '知识点覆盖',
|
|
57
|
+
points: [
|
|
58
|
+
{
|
|
59
|
+
id: 'fixture-kp-01',
|
|
60
|
+
title: '有理数的运算核心知识点',
|
|
61
|
+
summary: '理解有理数加减乘除的运算顺序和符号处理。',
|
|
62
|
+
formulas: [],
|
|
63
|
+
pitfalls: ['符号处理错误'],
|
|
64
|
+
examples: ['-3 + 5 = 2'],
|
|
65
|
+
questionTemplates: [
|
|
66
|
+
['计算:-3 + 5。', '2', '符号处理错误']
|
|
67
|
+
],
|
|
68
|
+
sources: ['fixture-page.png']
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
review: {
|
|
74
|
+
passed: true,
|
|
75
|
+
coverageSummary: '测试 fixture 覆盖 1 个知识点。',
|
|
76
|
+
missingOrWeak: [],
|
|
77
|
+
duplicateMerged: []
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
attempts: 1
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function shouldRetry(result) {
|
|
85
|
+
if (result.ok) return false;
|
|
86
|
+
return [
|
|
87
|
+
'timeout',
|
|
88
|
+
'fetch_failed',
|
|
89
|
+
'empty_response',
|
|
90
|
+
'invalid_json',
|
|
91
|
+
'http_408',
|
|
92
|
+
'http_409',
|
|
93
|
+
'http_425',
|
|
94
|
+
'http_429',
|
|
95
|
+
'http_500',
|
|
96
|
+
'http_502',
|
|
97
|
+
'http_503',
|
|
98
|
+
'http_504'
|
|
99
|
+
].includes(result.reason);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function postChatCompletionOnce({ messages, temperature = 0.3, timeoutMs = 12000 }) {
|
|
103
|
+
const { baseUrl, apiKey, model } = await getLlmRuntimeConfig();
|
|
104
|
+
if (!baseUrl || !apiKey) {
|
|
105
|
+
return { ok: false, reason: 'missing_env' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
110
|
+
let response;
|
|
111
|
+
try {
|
|
112
|
+
response = await fetch(`${baseUrl.replace(/\/$/, '')}/v1/chat/completions`, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
signal: controller.signal,
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: `Bearer ${apiKey}`,
|
|
117
|
+
'Content-Type': 'application/json'
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
model,
|
|
121
|
+
temperature,
|
|
122
|
+
messages,
|
|
123
|
+
response_format: { type: 'json_object' }
|
|
124
|
+
})
|
|
125
|
+
});
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return { ok: false, reason: error.name === 'AbortError' ? 'timeout' : 'fetch_failed', detail: error.message };
|
|
128
|
+
} finally {
|
|
129
|
+
clearTimeout(timeout);
|
|
130
|
+
}
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
return { ok: false, reason: `http_${response.status}`, detail: await response.text() };
|
|
133
|
+
}
|
|
134
|
+
const payload = await response.json();
|
|
135
|
+
const text = payload.choices?.[0]?.message?.content;
|
|
136
|
+
if (!text) return { ok: false, reason: 'empty_response' };
|
|
137
|
+
try {
|
|
138
|
+
return { ok: true, data: JSON.parse(text) };
|
|
139
|
+
} catch {
|
|
140
|
+
return { ok: false, reason: 'invalid_json', detail: text };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function postChatCompletion({ messages, temperature = 0.3, timeoutMs = 12000, retries = 1 }) {
|
|
145
|
+
let lastResult = null;
|
|
146
|
+
const attempts = Math.max(1, Number(retries || 0) + 1);
|
|
147
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
148
|
+
const result = await postChatCompletionOnce({ messages, temperature, timeoutMs });
|
|
149
|
+
if (result.ok || !shouldRetry(result) || attempt === attempts) {
|
|
150
|
+
return {
|
|
151
|
+
...result,
|
|
152
|
+
attempts: attempt,
|
|
153
|
+
previousReason: lastResult?.reason || null
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
lastResult = result;
|
|
157
|
+
await sleep(Math.min(1000 * attempt, 3000));
|
|
158
|
+
}
|
|
159
|
+
return lastResult;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function callChatAgent({ system, user, temperature = 0.3, timeoutMs = 12000, retries = 1 }) {
|
|
163
|
+
if (fixtureMode() === 'knowledge-extract') return knowledgeSummarizeFixture();
|
|
164
|
+
return postChatCompletion({
|
|
165
|
+
temperature,
|
|
166
|
+
timeoutMs,
|
|
167
|
+
retries,
|
|
168
|
+
messages: [
|
|
169
|
+
{ role: 'system', content: system },
|
|
170
|
+
{ role: 'user', content: user }
|
|
171
|
+
]
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function callVisionAgent({ system, text, imagePaths, temperature = 0.1, timeoutMs = 45000, retries = 1 }) {
|
|
176
|
+
if (fixtureMode() === 'knowledge-extract') return knowledgeExtractPageFixture();
|
|
177
|
+
const imageContent = [];
|
|
178
|
+
for (const imagePath of imagePaths || []) {
|
|
179
|
+
const bytes = await readFile(imagePath);
|
|
180
|
+
imageContent.push({
|
|
181
|
+
type: 'image_url',
|
|
182
|
+
image_url: {
|
|
183
|
+
url: `data:${mimeForFile(imagePath)};base64,${bytes.toString('base64')}`
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return postChatCompletion({
|
|
188
|
+
temperature,
|
|
189
|
+
timeoutMs,
|
|
190
|
+
retries,
|
|
191
|
+
messages: [
|
|
192
|
+
{ role: 'system', content: system },
|
|
193
|
+
{
|
|
194
|
+
role: 'user',
|
|
195
|
+
content: [
|
|
196
|
+
{ type: 'text', text },
|
|
197
|
+
...imageContent
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
});
|
|
202
|
+
}
|
package/server/env.js
ADDED