synthos 0.10.1 → 0.11.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/README.md +5 -5
- package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
- package/default-pages/elevenlabs_effects_studio/page.json +13 -11
- package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_voice_studio/page.html +782 -801
- package/default-pages/elevenlabs_voice_studio/page.json +13 -11
- package/default-pages/json_tools/chat-history.json +1 -0
- package/default-pages/json_tools/page.html +70 -90
- package/default-pages/json_tools/page.json +12 -10
- package/default-pages/my_notes/chat-history.json +1 -0
- package/default-pages/my_notes/page.html +115 -131
- package/default-pages/my_notes/page.json +14 -12
- package/default-pages/neon_asteroids/chat-history.json +1 -0
- package/default-pages/neon_asteroids/page.html +1777 -1803
- package/default-pages/neon_asteroids/page.json +14 -12
- package/default-pages/oregon_trail/chat-history.json +1 -0
- package/default-pages/oregon_trail/page.html +290 -307
- package/default-pages/oregon_trail/page.json +14 -12
- package/default-pages/solar_explorer/chat-history.json +1 -0
- package/default-pages/solar_explorer/page.html +1929 -1951
- package/default-pages/solar_explorer/page.json +14 -12
- package/default-pages/solar_tutorial/chat-history.json +1 -0
- package/default-pages/solar_tutorial/page.html +464 -478
- package/default-pages/solar_tutorial/page.json +12 -10
- package/default-pages/us_map/chat-history.json +1 -0
- package/default-pages/us_map/page.html +170 -193
- package/default-pages/us_map/page.json +14 -12
- package/default-pages/us_map/page.light.png +0 -0
- package/default-pages/us_map_1850/chat-history.json +1 -0
- package/default-pages/us_map_1850/page.html +302 -326
- package/default-pages/us_map_1850/page.json +14 -12
- package/default-pages/western_cities_1850/chat-history.json +1 -0
- package/default-pages/western_cities_1850/page.html +503 -527
- package/default-pages/western_cities_1850/page.json +14 -12
- package/default-themes/aurora-dawn.v3.css +15 -14
- package/default-themes/aurora-dusk.v3.css +26 -26
- package/default-themes/cosmos-dawn.v3.css +15 -14
- package/default-themes/cosmos-dusk.v3.css +26 -26
- package/default-themes/elemental-dawn.v3.css +200 -0
- package/default-themes/nebula-dawn.v3.css +15 -14
- package/default-themes/nebula-dusk.v3.css +24 -24
- package/default-themes/solar-flare-dawn.v3.css +15 -14
- package/default-themes/solar-flare-dusk.v3.css +26 -26
- package/dist/builders/anthropic.d.ts +26 -2
- package/dist/builders/anthropic.d.ts.map +1 -1
- package/dist/builders/anthropic.js +132 -31
- package/dist/builders/anthropic.js.map +1 -1
- package/dist/builders/claudecode.d.ts +13 -0
- package/dist/builders/claudecode.d.ts.map +1 -0
- package/dist/builders/claudecode.js +253 -0
- package/dist/builders/claudecode.js.map +1 -0
- package/dist/builders/index.d.ts +2 -1
- package/dist/builders/index.d.ts.map +1 -1
- package/dist/builders/index.js +8 -1
- package/dist/builders/index.js.map +1 -1
- package/dist/builders/openai.js +2 -1
- package/dist/builders/openai.js.map +1 -1
- package/dist/builders/types.d.ts +31 -7
- package/dist/builders/types.d.ts.map +1 -1
- package/dist/builders/types.js +60 -28
- package/dist/builders/types.js.map +1 -1
- package/dist/connectors/types.d.ts +8 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +13 -6
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +161 -14
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +1 -0
- package/dist/models/anthropic.d.ts.map +1 -1
- package/dist/models/anthropic.js +129 -29
- package/dist/models/anthropic.js.map +1 -1
- package/dist/models/chainOfThought.d.ts.map +1 -1
- package/dist/models/chainOfThought.js +32 -19
- package/dist/models/chainOfThought.js.map +1 -1
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +2 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/providers.d.ts +1 -0
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +12 -4
- package/dist/models/providers.js.map +1 -1
- package/dist/models/types.d.ts +15 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js.map +1 -1
- package/dist/pages.d.ts +57 -8
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +258 -45
- package/dist/pages.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +5 -0
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/mediaCache.d.ts +36 -0
- package/dist/service/mediaCache.d.ts.map +1 -0
- package/dist/service/mediaCache.js +182 -0
- package/dist/service/mediaCache.js.map +1 -0
- package/dist/service/pageValidator.d.ts +25 -0
- package/dist/service/pageValidator.d.ts.map +1 -0
- package/dist/service/pageValidator.js +315 -0
- package/dist/service/pageValidator.js.map +1 -0
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +4 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/sharedTableSchema.d.ts +73 -0
- package/dist/service/sharedTableSchema.d.ts.map +1 -0
- package/dist/service/sharedTableSchema.js +206 -0
- package/dist/service/sharedTableSchema.js.map +1 -0
- package/dist/service/transformPage.d.ts +49 -11
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +354 -241
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +285 -34
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +170 -32
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +59 -2
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/useExtractRoutes.d.ts +4 -0
- package/dist/service/useExtractRoutes.d.ts.map +1 -0
- package/dist/service/useExtractRoutes.js +304 -0
- package/dist/service/useExtractRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts +17 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +1388 -483
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
- package/dist/service/useSharedDataRoutes.js +54 -2
- package/dist/service/useSharedDataRoutes.js.map +1 -1
- package/dist/settings.d.ts +27 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +40 -1
- package/dist/settings.js.map +1 -1
- package/dist/themes.d.ts +0 -5
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +3 -95
- package/dist/themes.js.map +1 -1
- package/migration-rules/v2-to-v3.md +277 -119
- package/package.json +5 -1
- package/{default-pages/application → required-pages/_shell}/page.html +56 -42
- package/required-pages/_shell/page.json +14 -0
- package/required-pages/_starters/page.html +534 -0
- package/required-pages/_starters/page.json +12 -0
- package/required-pages/builder/page.html +353 -43
- package/required-pages/builder/page.json +12 -10
- package/required-pages/pages/page.html +697 -924
- package/required-pages/pages/page.json +12 -10
- package/required-pages/settings/page.html +1888 -1753
- package/required-pages/settings/page.json +12 -10
- package/required-pages/synthos_apis/page.html +834 -845
- package/required-pages/synthos_apis/page.json +12 -10
- package/required-pages/synthos_scripts/page.html +74 -88
- package/required-pages/synthos_scripts/page.json +12 -10
- package/scripts/append-instructions.py +90 -0
- package/scripts/audit-instructions.py +76 -0
- package/scripts/cleanup-shell-markup.mjs +112 -0
- package/service-connectors/buffer/connector.json +46 -0
- package/service-connectors/canva/connector.json +67 -0
- package/service-connectors/elevenlabs/connector.json +1 -1
- package/src/builders/anthropic.ts +150 -25
- package/src/builders/claudecode.ts +310 -0
- package/src/builders/index.ts +7 -1
- package/src/builders/openai.ts +2 -1
- package/src/builders/types.ts +93 -32
- package/src/connectors/types.ts +8 -0
- package/src/init.ts +13 -7
- package/src/migrations.ts +187 -16
- package/src/models/anthropic.ts +140 -30
- package/src/models/chainOfThought.ts +33 -18
- package/src/models/index.ts +2 -2
- package/src/models/providers.ts +10 -1
- package/src/models/types.ts +21 -1
- package/src/pages.ts +271 -35
- package/src/service/createCompletePrompt.ts +6 -0
- package/src/service/mediaCache.ts +206 -0
- package/src/service/pageValidator.ts +337 -0
- package/src/service/server.ts +4 -0
- package/src/service/sharedTableSchema.ts +236 -0
- package/src/service/transformPage.ts +370 -260
- package/src/service/useApiRoutes.ts +283 -32
- package/src/service/useConnectorRoutes.ts +189 -34
- package/src/service/useDataRoutes.ts +198 -116
- package/src/service/useExtractRoutes.ts +331 -0
- package/src/service/usePageRoutes.ts +1414 -394
- package/src/service/useSharedDataRoutes.ts +184 -109
- package/src/settings.ts +65 -0
- package/src/themes.ts +78 -180
- package/starters/blank_starter/chat-history.json +1 -0
- package/starters/blank_starter/page.dark.png +0 -0
- package/starters/blank_starter/page.html +47 -0
- package/starters/blank_starter/page.json +13 -0
- package/starters/blank_starter/page.light.png +0 -0
- package/starters/calculator_starter/chat-history.json +1 -0
- package/starters/calculator_starter/page.dark.png +0 -0
- package/starters/calculator_starter/page.html +232 -0
- package/starters/calculator_starter/page.json +13 -0
- package/starters/calculator_starter/page.light.png +0 -0
- package/starters/calendar_starter/chat-history.json +1 -0
- package/starters/calendar_starter/page.dark.png +0 -0
- package/starters/calendar_starter/page.html +495 -0
- package/starters/calendar_starter/page.json +13 -0
- package/starters/calendar_starter/page.light.png +0 -0
- package/starters/chat_starter/chat-history.json +1 -0
- package/starters/chat_starter/page.dark.png +0 -0
- package/starters/chat_starter/page.html +351 -0
- package/starters/chat_starter/page.json +13 -0
- package/starters/chat_starter/page.light.png +0 -0
- package/starters/checklist_starter/chat-history.json +1 -0
- package/starters/checklist_starter/page.dark.png +0 -0
- package/starters/checklist_starter/page.html +437 -0
- package/starters/checklist_starter/page.json +13 -0
- package/starters/checklist_starter/page.light.png +0 -0
- package/starters/dashboard_starter/chat-history.json +1 -0
- package/starters/dashboard_starter/page.dark.png +0 -0
- package/starters/dashboard_starter/page.html +195 -0
- package/starters/dashboard_starter/page.json +13 -0
- package/starters/dashboard_starter/page.light.png +0 -0
- package/starters/form_starter/chat-history.json +1 -0
- package/starters/form_starter/page.dark.png +0 -0
- package/starters/form_starter/page.html +313 -0
- package/starters/form_starter/page.json +13 -0
- package/starters/form_starter/page.light.png +0 -0
- package/starters/gallery_starter/chat-history.json +1 -0
- package/starters/gallery_starter/page.dark.png +0 -0
- package/starters/gallery_starter/page.html +418 -0
- package/starters/gallery_starter/page.json +13 -0
- package/starters/gallery_starter/page.light.png +0 -0
- package/starters/generator_starter/chat-history.json +1 -0
- package/starters/generator_starter/page.dark.png +0 -0
- package/starters/generator_starter/page.html +261 -0
- package/starters/generator_starter/page.json +13 -0
- package/starters/generator_starter/page.light.png +0 -0
- package/starters/index.html +538 -0
- package/starters/kanban_starter/chat-history.json +1 -0
- package/starters/kanban_starter/page.dark.png +0 -0
- package/starters/kanban_starter/page.html +432 -0
- package/starters/kanban_starter/page.json +13 -0
- package/starters/kanban_starter/page.light.png +0 -0
- package/starters/presentation_builder/chat-history.json +1 -0
- package/starters/presentation_builder/page.dark.png +0 -0
- package/starters/presentation_builder/page.html +970 -0
- package/starters/presentation_builder/page.json +15 -0
- package/starters/presentation_builder/page.light.png +0 -0
- package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
- package/starters/pulse_starter/chat-history.json +1 -0
- package/starters/pulse_starter/page.dark.png +0 -0
- package/starters/pulse_starter/page.html +698 -0
- package/starters/pulse_starter/page.json +13 -0
- package/starters/pulse_starter/page.light.png +0 -0
- package/starters/quiz_starter/chat-history.json +1 -0
- package/starters/quiz_starter/page.dark.png +0 -0
- package/starters/quiz_starter/page.html +292 -0
- package/starters/quiz_starter/page.json +13 -0
- package/starters/quiz_starter/page.light.png +0 -0
- package/starters/reference_starter/chat-history.json +1 -0
- package/starters/reference_starter/page.dark.png +0 -0
- package/starters/reference_starter/page.html +250 -0
- package/starters/reference_starter/page.json +13 -0
- package/starters/reference_starter/page.light.png +0 -0
- package/starters/retro_game_starter/chat-history.json +1 -0
- package/starters/retro_game_starter/page.dark.png +0 -0
- package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
- package/starters/retro_game_starter/page.json +15 -0
- package/starters/retro_game_starter/page.light.png +0 -0
- package/starters/roster_starter/chat-history.json +1 -0
- package/starters/roster_starter/page.dark.png +0 -0
- package/starters/roster_starter/page.html +600 -0
- package/starters/roster_starter/page.json +13 -0
- package/starters/roster_starter/page.light.png +0 -0
- package/starters/server.js +182 -0
- package/starters/start.cmd +1 -0
- package/starters/timeline_starter/chat-history.json +1 -0
- package/starters/timeline_starter/page.dark.png +0 -0
- package/starters/timeline_starter/page.html +446 -0
- package/starters/timeline_starter/page.json +13 -0
- package/starters/timeline_starter/page.light.png +0 -0
- package/starters/tutorial_starter/chat-history.json +1 -0
- package/starters/tutorial_starter/page.dark.png +0 -0
- package/starters/tutorial_starter/page.html +283 -0
- package/starters/tutorial_starter/page.json +13 -0
- package/starters/tutorial_starter/page.light.png +0 -0
- package/static-files/agent.v3.js +122 -0
- package/static-files/connector.v3.js +48 -0
- package/static-files/extract.v3.js +188 -0
- package/static-files/helpers.v3.js +50 -6
- package/static-files/page-bridge.js +114 -0
- package/static-files/page.v3.js +1292 -1290
- package/static-files/script.v3.js +32 -0
- package/static-files/server.v3.js +89 -0
- package/static-files/shell-bridge.v3.js +174 -0
- package/static-files/shell-modals.v3.js +521 -0
- package/static-files/{shell.css → shell.v3.css} +271 -22
- package/static-files/shell.v3.js +1865 -0
- package/static-files/storage.v3.js +176 -0
- package/tests/anthropic.spec.ts +42 -7
- package/tests/builders.spec.ts +70 -2
- package/tests/pageValidator.spec.ts +548 -0
- package/tests/profiles.spec.ts +122 -0
- package/tests/sharedTableSchema.spec.ts +242 -0
- package/tests/transformPage.spec.ts +62 -81
- package/default-pages/application/page.json +0 -10
- package/default-pages/retro_game_starter/page.json +0 -12
- package/default-pages/sidebar_page/page.html +0 -51
- package/default-pages/sidebar_page/page.json +0 -10
- package/default-pages/two-panel_page/page.html +0 -68
- package/default-pages/two-panel_page/page.json +0 -10
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { Application } from 'express';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
5
|
+
import { SynthOSConfig } from '../init';
|
|
6
|
+
import { createCompletePrompt } from './createCompletePrompt';
|
|
7
|
+
import { loadSettings, getModelEntry } from '../settings';
|
|
8
|
+
import { ContentBlock } from '../models';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PROMPT = `You are a data extraction service. The user has provided a file. Extract the requested fields and return JSON matching the provided schema exactly. If a field is not present in the file, omit it unless it is required by the schema, in which case use the closest reasonable value. Do not include explanatory prose — return only the JSON object.`;
|
|
11
|
+
|
|
12
|
+
const MAX_FILE_BYTES = 50 * 1024 * 1024;
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
14
|
+
const MAX_TIMEOUT_MS = 300_000;
|
|
15
|
+
const MIN_TIMEOUT_MS = 1_000;
|
|
16
|
+
|
|
17
|
+
const TEXT_MIMES = new Set([
|
|
18
|
+
'application/json',
|
|
19
|
+
'application/xml',
|
|
20
|
+
'application/yaml',
|
|
21
|
+
'application/x-yaml',
|
|
22
|
+
'application/javascript',
|
|
23
|
+
'application/typescript',
|
|
24
|
+
'application/x-www-form-urlencoded',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const IMAGE_MIMES = new Set([
|
|
28
|
+
'image/png',
|
|
29
|
+
'image/jpeg',
|
|
30
|
+
'image/jpg',
|
|
31
|
+
'image/webp',
|
|
32
|
+
'image/gif',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function isTextMime(mime: string): boolean {
|
|
36
|
+
return mime.startsWith('text/') || TEXT_MIMES.has(mime);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isImageMime(mime: string): boolean {
|
|
40
|
+
return IMAGE_MIMES.has(mime);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isPdfMime(mime: string): boolean {
|
|
44
|
+
return mime === 'application/pdf';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isValidSchema(s: unknown): s is Record<string, unknown> {
|
|
48
|
+
return !!s && typeof s === 'object' && !Array.isArray(s);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pageFilesFolder(config: SynthOSConfig, page: string): string {
|
|
52
|
+
return path.join(config.pagesFolder, 'pages', page, 'files');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function safeFilenameInPageDir(config: SynthOSConfig, page: string, filename: string): string | null {
|
|
56
|
+
if (!filename || filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const folder = pageFilesFolder(config, page);
|
|
60
|
+
const resolved = path.resolve(folder, filename);
|
|
61
|
+
if (!resolved.startsWith(path.resolve(folder))) return null;
|
|
62
|
+
return resolved;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function useExtractRoutes(config: SynthOSConfig, app: Application): void {
|
|
66
|
+
const sp = config.storageProvider;
|
|
67
|
+
|
|
68
|
+
// 50 MB raw → ~67 MB base64; allow ~70 MB JSON envelope.
|
|
69
|
+
app.post('/api/extract', express.json({ limit: '70mb' }), async (req, res) => {
|
|
70
|
+
try {
|
|
71
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
72
|
+
const page = typeof body.page === 'string' ? body.page : '';
|
|
73
|
+
const filename = typeof body.filename === 'string' ? body.filename : '';
|
|
74
|
+
const mimeType = typeof body.mimeType === 'string' ? body.mimeType : '';
|
|
75
|
+
const data = typeof body.data === 'string' ? body.data : '';
|
|
76
|
+
const schema = body.schema;
|
|
77
|
+
const userPromptIn = typeof body.prompt === 'string' && body.prompt.length > 0 ? body.prompt : DEFAULT_PROMPT;
|
|
78
|
+
const modelOverride = typeof body.model === 'string' && body.model.length > 0 ? body.model : undefined;
|
|
79
|
+
const persist = body.persist === true;
|
|
80
|
+
const timeoutIn = typeof body.timeout === 'number' && Number.isFinite(body.timeout) ? body.timeout : DEFAULT_TIMEOUT_MS;
|
|
81
|
+
const timeoutMs = Math.min(Math.max(timeoutIn, MIN_TIMEOUT_MS), MAX_TIMEOUT_MS);
|
|
82
|
+
|
|
83
|
+
// ---- request validation ----
|
|
84
|
+
if (!page) {
|
|
85
|
+
res.status(400).json({ error: 'invalid_request', message: 'page is required' });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!filename) {
|
|
89
|
+
res.status(400).json({ error: 'invalid_request', message: 'filename is required' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!mimeType) {
|
|
93
|
+
res.status(400).json({ error: 'invalid_request', message: 'mimeType is required' });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!data) {
|
|
97
|
+
res.status(400).json({ error: 'invalid_request', message: 'data (base64) is required' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!isValidSchema(schema)) {
|
|
101
|
+
res.status(400).json({ error: 'malformed_schema', message: 'schema must be a JSON Schema object' });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- decode + size check ----
|
|
106
|
+
let buffer: Buffer;
|
|
107
|
+
try {
|
|
108
|
+
buffer = Buffer.from(data, 'base64');
|
|
109
|
+
} catch {
|
|
110
|
+
res.status(400).json({ error: 'invalid_request', message: 'data must be valid base64' });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (buffer.length === 0) {
|
|
114
|
+
res.status(400).json({ error: 'invalid_request', message: 'data decoded to empty buffer' });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (buffer.length > MAX_FILE_BYTES) {
|
|
118
|
+
res.status(413).json({
|
|
119
|
+
error: 'oversize',
|
|
120
|
+
message: `File exceeds 50 MB limit (${buffer.length} bytes)`,
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---- resolve active model ----
|
|
126
|
+
const settings = await loadSettings(config);
|
|
127
|
+
const entry = getModelEntry(settings, 'chat');
|
|
128
|
+
const provider = entry.provider;
|
|
129
|
+
const modelUsed = modelOverride ?? entry.configuration.model;
|
|
130
|
+
|
|
131
|
+
// ---- routing ----
|
|
132
|
+
const isText = isTextMime(mimeType);
|
|
133
|
+
const isImage = isImageMime(mimeType);
|
|
134
|
+
const isPdf = isPdfMime(mimeType);
|
|
135
|
+
|
|
136
|
+
if (!isText && !isImage && !isPdf) {
|
|
137
|
+
res.status(415).json({
|
|
138
|
+
error: 'unsupported_file_type',
|
|
139
|
+
message: `MIME type "${mimeType}" is not supported in v1.`,
|
|
140
|
+
modelUsed,
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (provider === 'ClaudeCode') {
|
|
145
|
+
res.status(415).json({
|
|
146
|
+
error: 'unsupported_file_type',
|
|
147
|
+
message: `The "ClaudeCode" provider is not supported by /api/extract in v1. Switch to a direct Anthropic, OpenAI, or FireworksAI chat model.`,
|
|
148
|
+
modelUsed,
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (isImage && provider === 'FireworksAI') {
|
|
153
|
+
res.status(415).json({
|
|
154
|
+
error: 'unsupported_file_type',
|
|
155
|
+
message: `The active model "${modelUsed}" cannot read images. Switch to a Claude or GPT model.`,
|
|
156
|
+
modelUsed,
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (isPdf && provider !== 'Anthropic') {
|
|
161
|
+
res.status(415).json({
|
|
162
|
+
error: 'unsupported_file_type',
|
|
163
|
+
message: `PDF extraction requires a Claude (Anthropic) model. The active model is "${modelUsed}".`,
|
|
164
|
+
modelUsed,
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!entry.configuration.apiKey) {
|
|
169
|
+
res.status(500).json({
|
|
170
|
+
error: 'invalid_request',
|
|
171
|
+
message: 'API key not configured for the active chat model.',
|
|
172
|
+
modelUsed,
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---- optional persist ----
|
|
178
|
+
let savedFile: { name: string; size: number; url: string } | undefined;
|
|
179
|
+
if (persist) {
|
|
180
|
+
const filePath = safeFilenameInPageDir(config, page, filename);
|
|
181
|
+
if (!filePath) {
|
|
182
|
+
res.status(400).json({ error: 'invalid_request', message: 'invalid filename for persist' });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
await sp.ensureFolderExists(pageFilesFolder(config, page));
|
|
186
|
+
await sp.saveBuffer(filePath, buffer);
|
|
187
|
+
savedFile = {
|
|
188
|
+
name: filename,
|
|
189
|
+
size: buffer.length,
|
|
190
|
+
url: `/api/files/${encodeURIComponent(page)}/${encodeURIComponent(filename)}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---- run extraction with timeout ----
|
|
195
|
+
const runP: Promise<string> = (async () => {
|
|
196
|
+
if (isPdf) {
|
|
197
|
+
return await extractPdfWithAnthropic({
|
|
198
|
+
apiKey: entry.configuration.apiKey!,
|
|
199
|
+
model: modelUsed,
|
|
200
|
+
filename,
|
|
201
|
+
base64: buffer.toString('base64'),
|
|
202
|
+
userPrompt: userPromptIn,
|
|
203
|
+
schema: schema as Record<string, unknown>,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const completePromptFn = await createCompletePrompt(config, 'chat', modelOverride);
|
|
208
|
+
|
|
209
|
+
if (isText) {
|
|
210
|
+
const textBody = buffer.toString('utf8');
|
|
211
|
+
const fullText =
|
|
212
|
+
`Filename: ${filename}\nMIME type: ${mimeType}\n\n--- FILE CONTENTS ---\n${textBody}\n--- END FILE CONTENTS ---\n\n${userPromptIn}`;
|
|
213
|
+
const result = await completePromptFn({
|
|
214
|
+
system: { role: 'system', content: DEFAULT_PROMPT },
|
|
215
|
+
prompt: { role: 'user', content: fullText },
|
|
216
|
+
outputSchema: schema as Record<string, unknown>,
|
|
217
|
+
jsonSchema: schema as Record<string, unknown>,
|
|
218
|
+
});
|
|
219
|
+
if (!result.completed) throw result.error ?? new Error('extraction failed');
|
|
220
|
+
return result.value ?? '';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// image branch (Anthropic | OpenAI)
|
|
224
|
+
const promptContent: ContentBlock[] = [
|
|
225
|
+
{ type: 'image', mediaType: mimeType, data: buffer.toString('base64') },
|
|
226
|
+
{ type: 'text', text: `Filename: ${filename}\n\n${userPromptIn}` },
|
|
227
|
+
];
|
|
228
|
+
const result = await completePromptFn({
|
|
229
|
+
system: { role: 'system', content: DEFAULT_PROMPT },
|
|
230
|
+
prompt: { role: 'user', content: promptContent },
|
|
231
|
+
outputSchema: schema as Record<string, unknown>,
|
|
232
|
+
jsonSchema: schema as Record<string, unknown>,
|
|
233
|
+
});
|
|
234
|
+
if (!result.completed) throw result.error ?? new Error('extraction failed');
|
|
235
|
+
return result.value ?? '';
|
|
236
|
+
})();
|
|
237
|
+
|
|
238
|
+
let raw: string;
|
|
239
|
+
try {
|
|
240
|
+
raw = await Promise.race([
|
|
241
|
+
runP,
|
|
242
|
+
new Promise<string>((_resolve, reject) =>
|
|
243
|
+
setTimeout(() => reject(new Error('extraction timed out')), timeoutMs),
|
|
244
|
+
),
|
|
245
|
+
]);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
const msg = (err as Error).message ?? 'unknown error';
|
|
248
|
+
if (/timed out/i.test(msg)) {
|
|
249
|
+
res.status(504).json({ error: 'timeout', message: msg, modelUsed });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
console.error('EXTRACT ERROR:', msg);
|
|
253
|
+
res.status(502).json({ error: 'extraction_failed', message: msg, modelUsed });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ---- parse + return ----
|
|
258
|
+
let parsed: unknown;
|
|
259
|
+
try {
|
|
260
|
+
parsed = JSON.parse(raw);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error('EXTRACT PARSE ERROR:', (err as Error).message, '| raw head:', raw.slice(0, 500));
|
|
263
|
+
res.status(502).json({
|
|
264
|
+
error: 'extraction_failed',
|
|
265
|
+
message: `Model response was not valid JSON: ${(err as Error).message}`,
|
|
266
|
+
modelUsed,
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const responsePayload: Record<string, unknown> = { data: parsed, modelUsed };
|
|
272
|
+
if (savedFile) responsePayload.savedFile = savedFile;
|
|
273
|
+
res.json(responsePayload);
|
|
274
|
+
} catch (err: unknown) {
|
|
275
|
+
console.error('EXTRACT ERROR:', (err as Error).message);
|
|
276
|
+
res.status(500).json({ error: 'internal_error', message: (err as Error).message });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
interface AnthropicPdfArgs {
|
|
282
|
+
apiKey: string;
|
|
283
|
+
model: string;
|
|
284
|
+
filename: string;
|
|
285
|
+
base64: string;
|
|
286
|
+
userPrompt: string;
|
|
287
|
+
schema: Record<string, unknown>;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Direct Anthropic SDK call for PDF extraction. Streams with a raw iterator —
|
|
292
|
+
* structured-output deltas may arrive as input_json_delta, not text_delta.
|
|
293
|
+
*/
|
|
294
|
+
async function extractPdfWithAnthropic(args: AnthropicPdfArgs): Promise<string> {
|
|
295
|
+
const client = new Anthropic({ apiKey: args.apiKey });
|
|
296
|
+
|
|
297
|
+
const userContent: Anthropic.ContentBlockParam[] = [
|
|
298
|
+
{
|
|
299
|
+
type: 'document',
|
|
300
|
+
source: { type: 'base64', media_type: 'application/pdf', data: args.base64 },
|
|
301
|
+
title: args.filename,
|
|
302
|
+
},
|
|
303
|
+
{ type: 'text', text: `Filename: ${args.filename}\n\n${args.userPrompt}` },
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
const params: Anthropic.MessageCreateParamsStreaming = {
|
|
307
|
+
model: args.model,
|
|
308
|
+
max_tokens: 32768,
|
|
309
|
+
temperature: 0,
|
|
310
|
+
system: DEFAULT_PROMPT,
|
|
311
|
+
messages: [{ role: 'user', content: userContent }],
|
|
312
|
+
stream: true,
|
|
313
|
+
output_config: { format: { type: 'json_schema', schema: args.schema } },
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const stream = await client.messages.create(params);
|
|
317
|
+
const textByIndex = new Map<number, string>();
|
|
318
|
+
for await (const event of stream) {
|
|
319
|
+
if (event.type === 'content_block_delta') {
|
|
320
|
+
if (event.delta.type === 'text_delta') {
|
|
321
|
+
textByIndex.set(event.index, (textByIndex.get(event.index) ?? '') + event.delta.text);
|
|
322
|
+
} else if (event.delta.type === 'input_json_delta') {
|
|
323
|
+
textByIndex.set(event.index, (textByIndex.get(event.index) ?? '') + event.delta.partial_json);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return Array.from(textByIndex.entries())
|
|
328
|
+
.sort(([a], [b]) => a - b)
|
|
329
|
+
.map(([, v]) => v)
|
|
330
|
+
.join('');
|
|
331
|
+
}
|