@realtimex/folio 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -0
- package/api/src/services/IngestionService.ts +513 -206
- package/api/src/services/ModelCapabilityService.ts +213 -56
- package/api/src/services/PolicyEngine.ts +48 -22
- package/api/src/services/RAGService.ts +2 -2
- package/dist/api/src/services/IngestionService.js +467 -194
- package/dist/api/src/services/ModelCapabilityService.js +165 -54
- package/dist/api/src/services/PolicyEngine.js +38 -22
- package/dist/api/src/services/RAGService.js +2 -2
- package/dist/assets/{index-nxHX9No5.js → index-CLpalZvv.js} +37 -37
- package/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -8,20 +8,21 @@ export class ModelCapabilityService {
|
|
|
8
8
|
static UNSUPPORTED_CONFIRMATION_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
9
9
|
static UNSUPPORTED_CONFIRMATION_FAILURES = 2;
|
|
10
10
|
static UNSUPPORTED_SCORE_THRESHOLD = 3;
|
|
11
|
-
static resolveVisionSupport(settingsRow) {
|
|
11
|
+
static resolveVisionSupport(settingsRow, modality = "image") {
|
|
12
12
|
const provider = (settingsRow?.llm_provider || SDKService.DEFAULT_LLM_PROVIDER).trim();
|
|
13
13
|
const model = (settingsRow?.llm_model || SDKService.DEFAULT_LLM_MODEL).trim();
|
|
14
|
-
const state = this.getVisionState(settingsRow?.vision_model_capabilities, provider, model);
|
|
14
|
+
const state = this.getVisionState(settingsRow?.vision_model_capabilities, provider, model, modality);
|
|
15
15
|
return {
|
|
16
16
|
provider,
|
|
17
17
|
model,
|
|
18
|
+
modality,
|
|
18
19
|
state,
|
|
19
20
|
shouldAttempt: state !== "unsupported",
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
|
-
static getVisionState(rawMap, provider, model) {
|
|
23
|
+
static getVisionState(rawMap, provider, model, modality = "image") {
|
|
23
24
|
const map = this.normalizeCapabilityMap(rawMap);
|
|
24
|
-
const entry = map[this.capabilityKey(provider, model)];
|
|
25
|
+
const entry = map[this.capabilityKey(provider, model, modality)];
|
|
25
26
|
if (!entry || this.isExpired(entry))
|
|
26
27
|
return "unknown";
|
|
27
28
|
if (entry.state === "pending_unsupported")
|
|
@@ -31,18 +32,21 @@ export class ModelCapabilityService {
|
|
|
31
32
|
static async learnVisionSuccess(opts) {
|
|
32
33
|
await this.writeCapability({
|
|
33
34
|
...opts,
|
|
35
|
+
modality: opts.modality ?? "image",
|
|
34
36
|
state: "supported",
|
|
35
37
|
reason: "vision_request_succeeded",
|
|
36
38
|
ttlMs: this.SUPPORTED_TTL_MS,
|
|
37
39
|
});
|
|
38
40
|
}
|
|
39
41
|
static async learnVisionFailure(opts) {
|
|
42
|
+
const modality = opts.modality ?? "image";
|
|
40
43
|
const classification = this.classifyVisionFailure({
|
|
41
44
|
error: opts.error,
|
|
42
45
|
provider: opts.provider,
|
|
46
|
+
modality,
|
|
43
47
|
});
|
|
44
48
|
if (!classification.isCapabilityError) {
|
|
45
|
-
logger.info(`Vision failure for ${opts.provider}/${opts.model} treated as non-capability; leaving capability unknown`, {
|
|
49
|
+
logger.info(`Vision failure for ${opts.provider}/${opts.model} (${modality}) treated as non-capability; leaving capability unknown`, {
|
|
46
50
|
reason: classification.reason,
|
|
47
51
|
score: classification.score,
|
|
48
52
|
evidence: classification.evidence,
|
|
@@ -53,7 +57,7 @@ export class ModelCapabilityService {
|
|
|
53
57
|
if (!map) {
|
|
54
58
|
return "unknown";
|
|
55
59
|
}
|
|
56
|
-
const key = this.capabilityKey(opts.provider, opts.model);
|
|
60
|
+
const key = this.capabilityKey(opts.provider, opts.model, modality);
|
|
57
61
|
const now = new Date();
|
|
58
62
|
const failureCount = this.nextFailureCount(map[key], now.getTime());
|
|
59
63
|
if (failureCount < this.UNSUPPORTED_CONFIRMATION_FAILURES) {
|
|
@@ -62,6 +66,7 @@ export class ModelCapabilityService {
|
|
|
62
66
|
userId: opts.userId,
|
|
63
67
|
provider: opts.provider,
|
|
64
68
|
model: opts.model,
|
|
69
|
+
modality,
|
|
65
70
|
state: "pending_unsupported",
|
|
66
71
|
reason: "capability_signal_pending_confirmation",
|
|
67
72
|
ttlMs: this.PENDING_UNSUPPORTED_TTL_MS,
|
|
@@ -77,6 +82,7 @@ export class ModelCapabilityService {
|
|
|
77
82
|
userId: opts.userId,
|
|
78
83
|
provider: opts.provider,
|
|
79
84
|
model: opts.model,
|
|
85
|
+
modality,
|
|
80
86
|
state: "unsupported",
|
|
81
87
|
reason: classification.reason,
|
|
82
88
|
ttlMs: this.UNSUPPORTED_TTL_MS,
|
|
@@ -113,13 +119,23 @@ export class ModelCapabilityService {
|
|
|
113
119
|
return true;
|
|
114
120
|
}
|
|
115
121
|
static async writeCapability(opts) {
|
|
116
|
-
const { supabase, userId, provider, model, state, reason, ttlMs, preloadedMap, failureCount, lastFailureAt, evidence, } = opts;
|
|
122
|
+
const { supabase, userId, provider, model, modality, state, reason, ttlMs, preloadedMap, failureCount, lastFailureAt, evidence, } = opts;
|
|
117
123
|
const map = preloadedMap ?? (await this.readCapabilityMap(supabase, userId));
|
|
118
124
|
if (!map) {
|
|
119
125
|
return;
|
|
120
126
|
}
|
|
121
127
|
const now = new Date();
|
|
122
|
-
const key = this.capabilityKey(provider, model);
|
|
128
|
+
const key = this.capabilityKey(provider, model, modality);
|
|
129
|
+
const existingEntry = map[key];
|
|
130
|
+
if (this.isManualOverrideActive(existingEntry) && reason !== "manual_override") {
|
|
131
|
+
logger.info(`Skipping auto capability update for ${provider}/${model} (${modality}) because manual override is active`, {
|
|
132
|
+
requestedState: state,
|
|
133
|
+
requestedReason: reason,
|
|
134
|
+
currentState: existingEntry?.state,
|
|
135
|
+
currentReason: existingEntry?.reason,
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
123
139
|
const nextEntry = {
|
|
124
140
|
state,
|
|
125
141
|
learned_at: now.toISOString(),
|
|
@@ -140,7 +156,7 @@ export class ModelCapabilityService {
|
|
|
140
156
|
if (!persisted) {
|
|
141
157
|
return;
|
|
142
158
|
}
|
|
143
|
-
logger.info(`Updated model capability for ${provider}/${model}: ${state}`, {
|
|
159
|
+
logger.info(`Updated model capability for ${provider}/${model} (${modality}): ${state}`, {
|
|
144
160
|
reason,
|
|
145
161
|
ttlMs,
|
|
146
162
|
failureCount,
|
|
@@ -191,15 +207,28 @@ export class ModelCapabilityService {
|
|
|
191
207
|
}
|
|
192
208
|
return normalized;
|
|
193
209
|
}
|
|
194
|
-
static
|
|
210
|
+
static capabilityBaseKey(provider, model) {
|
|
195
211
|
return `${provider.toLowerCase().trim()}:${model.toLowerCase().trim()}`;
|
|
196
212
|
}
|
|
213
|
+
static capabilityKey(provider, model, modality = "image") {
|
|
214
|
+
const base = this.capabilityBaseKey(provider, model);
|
|
215
|
+
if (modality === "image")
|
|
216
|
+
return base;
|
|
217
|
+
return `${base}:${modality}`;
|
|
218
|
+
}
|
|
197
219
|
static isExpired(entry) {
|
|
198
220
|
if (!entry.expires_at)
|
|
199
221
|
return false;
|
|
200
222
|
const expiryTs = Date.parse(entry.expires_at);
|
|
201
223
|
return Number.isFinite(expiryTs) && expiryTs <= Date.now();
|
|
202
224
|
}
|
|
225
|
+
static isManualOverrideActive(entry) {
|
|
226
|
+
if (!entry)
|
|
227
|
+
return false;
|
|
228
|
+
if (entry.reason !== "manual_override")
|
|
229
|
+
return false;
|
|
230
|
+
return !this.isExpired(entry);
|
|
231
|
+
}
|
|
203
232
|
static nextFailureCount(entry, nowTs) {
|
|
204
233
|
if (!entry || entry.state !== "pending_unsupported" || this.isExpired(entry)) {
|
|
205
234
|
return 1;
|
|
@@ -230,7 +259,7 @@ export class ModelCapabilityService {
|
|
|
230
259
|
evidence: transientEvidence,
|
|
231
260
|
};
|
|
232
261
|
}
|
|
233
|
-
const documentEvidence = this.matchDocumentSpecific(signal);
|
|
262
|
+
const documentEvidence = this.matchDocumentSpecific(signal, opts.modality);
|
|
234
263
|
if (documentEvidence.length > 0) {
|
|
235
264
|
return {
|
|
236
265
|
isCapabilityError: false,
|
|
@@ -239,7 +268,7 @@ export class ModelCapabilityService {
|
|
|
239
268
|
evidence: documentEvidence,
|
|
240
269
|
};
|
|
241
270
|
}
|
|
242
|
-
const capability = this.scoreCapabilitySignal(signal, opts.provider);
|
|
271
|
+
const capability = this.scoreCapabilitySignal(signal, opts.provider, opts.modality);
|
|
243
272
|
if (capability.score >= this.UNSUPPORTED_SCORE_THRESHOLD) {
|
|
244
273
|
return {
|
|
245
274
|
isCapabilityError: true,
|
|
@@ -373,8 +402,8 @@ export class ModelCapabilityService {
|
|
|
373
402
|
...messageMatches.map((match) => `msg:${match}`),
|
|
374
403
|
];
|
|
375
404
|
}
|
|
376
|
-
static matchDocumentSpecific(signal) {
|
|
377
|
-
const
|
|
405
|
+
static matchDocumentSpecific(signal, modality) {
|
|
406
|
+
const imageCodeHints = [
|
|
378
407
|
"image_too_large",
|
|
379
408
|
"invalid_base64",
|
|
380
409
|
"invalid_image",
|
|
@@ -382,8 +411,8 @@ export class ModelCapabilityService {
|
|
|
382
411
|
"malformed_image",
|
|
383
412
|
"invalid_image_url",
|
|
384
413
|
"image_decode_failed",
|
|
385
|
-
]
|
|
386
|
-
const
|
|
414
|
+
];
|
|
415
|
+
const imageMessageHints = [
|
|
387
416
|
"image too large",
|
|
388
417
|
"invalid base64",
|
|
389
418
|
"malformed image",
|
|
@@ -391,7 +420,30 @@ export class ModelCapabilityService {
|
|
|
391
420
|
"unable to decode image",
|
|
392
421
|
"failed to decode image",
|
|
393
422
|
"invalid image url",
|
|
394
|
-
]
|
|
423
|
+
];
|
|
424
|
+
const pdfCodeHints = [
|
|
425
|
+
"invalid_pdf",
|
|
426
|
+
"malformed_pdf",
|
|
427
|
+
"corrupt_pdf",
|
|
428
|
+
"encrypted_pdf",
|
|
429
|
+
"password_protected_pdf",
|
|
430
|
+
"pdf_parse_error",
|
|
431
|
+
"file_too_large",
|
|
432
|
+
];
|
|
433
|
+
const pdfMessageHints = [
|
|
434
|
+
"invalid pdf",
|
|
435
|
+
"malformed pdf",
|
|
436
|
+
"corrupt pdf",
|
|
437
|
+
"encrypted pdf",
|
|
438
|
+
"password protected pdf",
|
|
439
|
+
"failed to parse pdf",
|
|
440
|
+
"unable to parse pdf",
|
|
441
|
+
"pdf is corrupted",
|
|
442
|
+
"pdf too large",
|
|
443
|
+
"file too large",
|
|
444
|
+
];
|
|
445
|
+
const codeMatches = this.matchCodes(signal.codes, modality === "pdf" ? pdfCodeHints : imageCodeHints);
|
|
446
|
+
const messageMatches = this.matchMessage(signal.message, modality === "pdf" ? pdfMessageHints : imageMessageHints);
|
|
395
447
|
const statusMatches = Array.from(signal.statusCodes).filter((status) => {
|
|
396
448
|
if (status === 413)
|
|
397
449
|
return true;
|
|
@@ -406,54 +458,87 @@ export class ModelCapabilityService {
|
|
|
406
458
|
...messageMatches.map((match) => `msg:${match}`),
|
|
407
459
|
];
|
|
408
460
|
}
|
|
409
|
-
static scoreCapabilitySignal(signal, provider) {
|
|
461
|
+
static scoreCapabilitySignal(signal, provider, modality) {
|
|
410
462
|
const evidence = [];
|
|
411
463
|
let score = 0;
|
|
412
|
-
const explicitCapabilityCodes = this.matchCodes(signal.codes,
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
464
|
+
const explicitCapabilityCodes = this.matchCodes(signal.codes, modality === "pdf"
|
|
465
|
+
? [
|
|
466
|
+
"pdf_not_supported",
|
|
467
|
+
"unsupported_pdf_input",
|
|
468
|
+
"unsupported_document_input",
|
|
469
|
+
"unsupported_file_input",
|
|
470
|
+
"input_file_not_supported",
|
|
471
|
+
"unsupported_file_type",
|
|
472
|
+
"model_not_document_capable",
|
|
473
|
+
]
|
|
474
|
+
: [
|
|
475
|
+
"vision_not_supported",
|
|
476
|
+
"unsupported_vision",
|
|
477
|
+
"model_not_vision_capable",
|
|
478
|
+
"image_not_supported",
|
|
479
|
+
"unsupported_message_content",
|
|
480
|
+
"unsupported_content_type_for_model",
|
|
481
|
+
"unsupported_image_input",
|
|
482
|
+
"invalid_model_for_vision",
|
|
483
|
+
]);
|
|
422
484
|
if (explicitCapabilityCodes.length > 0) {
|
|
423
485
|
score += 3;
|
|
424
486
|
evidence.push(...explicitCapabilityCodes.map((match) => `code:${match}`));
|
|
425
487
|
}
|
|
426
|
-
const highPrecisionMessageMatches = this.matchMessage(signal.message,
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
488
|
+
const highPrecisionMessageMatches = this.matchMessage(signal.message, modality === "pdf"
|
|
489
|
+
? [
|
|
490
|
+
"this model does not support pdf",
|
|
491
|
+
"model does not support pdf",
|
|
492
|
+
"pdf is not supported for this model",
|
|
493
|
+
"file input is not supported for this model",
|
|
494
|
+
"input_file is not supported",
|
|
495
|
+
"unsupported file type: application/pdf",
|
|
496
|
+
"application/pdf is not supported for this model",
|
|
497
|
+
]
|
|
498
|
+
: [
|
|
499
|
+
"does not support images",
|
|
500
|
+
"does not support image inputs",
|
|
501
|
+
"model does not support image",
|
|
502
|
+
"this model cannot process images",
|
|
503
|
+
"text-only model",
|
|
504
|
+
"images are not supported for this model",
|
|
505
|
+
"vision is not supported for this model",
|
|
506
|
+
"vision is not supported",
|
|
507
|
+
"vision not supported",
|
|
508
|
+
"image_url is only supported by certain models",
|
|
509
|
+
]);
|
|
438
510
|
if (highPrecisionMessageMatches.length > 0) {
|
|
439
511
|
score += 3;
|
|
440
512
|
evidence.push(...highPrecisionMessageMatches.map((match) => `msg:${match}`));
|
|
441
513
|
}
|
|
442
|
-
const providerSpecificMatches = this.matchMessage(signal.message, this.providerCapabilityHints(provider));
|
|
514
|
+
const providerSpecificMatches = this.matchMessage(signal.message, this.providerCapabilityHints(provider, modality));
|
|
443
515
|
if (providerSpecificMatches.length > 0) {
|
|
444
|
-
score +=
|
|
516
|
+
score += 3;
|
|
445
517
|
evidence.push(...providerSpecificMatches.map((match) => `provider:${match}`));
|
|
446
518
|
}
|
|
447
|
-
const weakCapabilityHints = this.matchMessage(signal.message,
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
519
|
+
const weakCapabilityHints = this.matchMessage(signal.message, modality === "pdf"
|
|
520
|
+
? [
|
|
521
|
+
"pdf input",
|
|
522
|
+
"pdf support",
|
|
523
|
+
"pdf not supported",
|
|
524
|
+
"application/pdf",
|
|
525
|
+
"input_file",
|
|
526
|
+
"file input",
|
|
527
|
+
"document input",
|
|
528
|
+
"unsupported file type",
|
|
529
|
+
"unsupported content type",
|
|
530
|
+
"invalid content type",
|
|
531
|
+
]
|
|
532
|
+
: [
|
|
533
|
+
"vision",
|
|
534
|
+
"unsupported content type",
|
|
535
|
+
"unsupported message content",
|
|
536
|
+
"invalid content type",
|
|
537
|
+
"unrecognized content type",
|
|
538
|
+
"image_url",
|
|
539
|
+
"multimodal",
|
|
540
|
+
"multi-modal",
|
|
541
|
+
]);
|
|
457
542
|
const hasClientValidationStatus = Array.from(signal.statusCodes).some((status) => [400, 415, 422].includes(status));
|
|
458
543
|
if (weakCapabilityHints.length > 0 && hasClientValidationStatus) {
|
|
459
544
|
score += 1;
|
|
@@ -468,8 +553,35 @@ export class ModelCapabilityService {
|
|
|
468
553
|
evidence: Array.from(new Set(evidence)).slice(0, 8),
|
|
469
554
|
};
|
|
470
555
|
}
|
|
471
|
-
static providerCapabilityHints(provider) {
|
|
556
|
+
static providerCapabilityHints(provider, modality) {
|
|
472
557
|
const normalized = provider.toLowerCase().trim();
|
|
558
|
+
if (modality === "pdf") {
|
|
559
|
+
if (normalized.includes("openai")) {
|
|
560
|
+
return [
|
|
561
|
+
"input_file is not supported",
|
|
562
|
+
"unsupported file type: application/pdf",
|
|
563
|
+
"application/pdf is not supported for this model",
|
|
564
|
+
];
|
|
565
|
+
}
|
|
566
|
+
if (normalized.includes("anthropic")) {
|
|
567
|
+
return [
|
|
568
|
+
"pdf is not supported for this model",
|
|
569
|
+
"file input is not supported for this model",
|
|
570
|
+
];
|
|
571
|
+
}
|
|
572
|
+
if (normalized.includes("google") || normalized.includes("gemini")) {
|
|
573
|
+
return [
|
|
574
|
+
"unsupported document input",
|
|
575
|
+
"pdf input is not supported",
|
|
576
|
+
];
|
|
577
|
+
}
|
|
578
|
+
if (normalized.includes("realtimex")) {
|
|
579
|
+
return [
|
|
580
|
+
"unsupported file input",
|
|
581
|
+
];
|
|
582
|
+
}
|
|
583
|
+
return [];
|
|
584
|
+
}
|
|
473
585
|
if (normalized.includes("openai")) {
|
|
474
586
|
return [
|
|
475
587
|
"image_url is only supported by certain models",
|
|
@@ -490,7 +602,6 @@ export class ModelCapabilityService {
|
|
|
490
602
|
}
|
|
491
603
|
if (normalized.includes("realtimex")) {
|
|
492
604
|
return [
|
|
493
|
-
"invalid model",
|
|
494
605
|
"text-only model",
|
|
495
606
|
];
|
|
496
607
|
}
|
|
@@ -8,20 +8,32 @@ import { extractLlmResponse, normalizeLlmContent, previewLlmText } from "../util
|
|
|
8
8
|
import { DEFAULT_BASELINE_FIELDS } from "./BaselineConfigService.js";
|
|
9
9
|
const logger = createLogger("PolicyEngine");
|
|
10
10
|
/**
|
|
11
|
-
* Helper to build LLM message content. If the text contains
|
|
12
|
-
* generated by IngestionService, it casts the payload to
|
|
13
|
-
* Vision array structure so the underlying SDK bridge can transmit the image.
|
|
11
|
+
* Helper to build LLM message content. If the text contains a VLM marker
|
|
12
|
+
* generated by IngestionService, it casts the payload to multimodal blocks.
|
|
14
13
|
*/
|
|
15
14
|
function extractVlmPayload(text) {
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
15
|
+
const imageMarker = text.match(/\[VLM_IMAGE_DATA:(data:[^;]+;base64,[^\]]+)\]/);
|
|
16
|
+
if (imageMarker) {
|
|
17
|
+
const markerText = imageMarker[0];
|
|
18
|
+
return {
|
|
19
|
+
kind: "image",
|
|
20
|
+
dataUrl: imageMarker[1],
|
|
21
|
+
supplementalText: text.replace(markerText, "").trim().slice(0, 4000),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const pdfMarker = text.match(/\[VLM_PDF_DATA:(data:[^;]+;base64,[^\]]+)\]/);
|
|
25
|
+
if (pdfMarker) {
|
|
26
|
+
const markerText = pdfMarker[0];
|
|
27
|
+
return {
|
|
28
|
+
kind: "pdf",
|
|
29
|
+
dataUrl: pdfMarker[1],
|
|
30
|
+
supplementalText: text.replace(markerText, "").trim().slice(0, 4000),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function hasVlmPayload(text) {
|
|
36
|
+
return text.includes("[VLM_IMAGE_DATA:") || text.includes("[VLM_PDF_DATA:");
|
|
25
37
|
}
|
|
26
38
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
39
|
function buildMessageContent(prompt, text, textFirst = false) {
|
|
@@ -30,10 +42,12 @@ function buildMessageContent(prompt, text, textFirst = false) {
|
|
|
30
42
|
const textPrompt = vlmPayload.supplementalText
|
|
31
43
|
? `${prompt}\n\nSupplemental extracted fields:\n${vlmPayload.supplementalText}`
|
|
32
44
|
: prompt;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
// `input_file` is not provider-agnostic (e.g. Anthropic-style block); providers
|
|
46
|
+
// that don't accept it will fail, and IngestionService will learn unsupported pdf modality.
|
|
47
|
+
const assetBlock = vlmPayload.kind === "pdf"
|
|
48
|
+
? { type: "input_file", file_url: vlmPayload.dataUrl }
|
|
49
|
+
: { type: "image_url", image_url: { url: vlmPayload.dataUrl } };
|
|
50
|
+
return [{ type: "text", text: textPrompt }, assetBlock];
|
|
37
51
|
}
|
|
38
52
|
// Standard text payload
|
|
39
53
|
return textFirst
|
|
@@ -340,7 +354,7 @@ async function evaluateCondition(condition, doc, trace, settings = {}) {
|
|
|
340
354
|
model,
|
|
341
355
|
condition_type: condition.type,
|
|
342
356
|
prompt_preview: prompt.slice(0, 180),
|
|
343
|
-
vision_payload: doc.text
|
|
357
|
+
vision_payload: hasVlmPayload(doc.text)
|
|
344
358
|
}
|
|
345
359
|
});
|
|
346
360
|
Actuator.logEvent(doc.ingestionId, doc.userId, "analysis", "Policy Matching", {
|
|
@@ -349,7 +363,7 @@ async function evaluateCondition(condition, doc, trace, settings = {}) {
|
|
|
349
363
|
model,
|
|
350
364
|
condition_type: condition.type,
|
|
351
365
|
prompt_preview: prompt.slice(0, 180),
|
|
352
|
-
vision_payload: doc.text
|
|
366
|
+
vision_payload: hasVlmPayload(doc.text)
|
|
353
367
|
}, doc.supabase);
|
|
354
368
|
const result = await sdk.llm.chat([
|
|
355
369
|
{
|
|
@@ -443,7 +457,7 @@ async function extractData(fields, doc, trace, settings = {}) {
|
|
|
443
457
|
Fields to extract:
|
|
444
458
|
${fieldDescriptions}`;
|
|
445
459
|
try {
|
|
446
|
-
const isVlmPayload = doc.text
|
|
460
|
+
const isVlmPayload = hasVlmPayload(doc.text);
|
|
447
461
|
const mixedPrompt = isVlmPayload
|
|
448
462
|
? `You are a precise data extraction engine. Return only valid JSON.\n\n${prompt}`
|
|
449
463
|
: prompt;
|
|
@@ -593,7 +607,7 @@ Rules:
|
|
|
593
607
|
model,
|
|
594
608
|
known_fields_count: Object.keys(contractData).length,
|
|
595
609
|
}, doc.supabase);
|
|
596
|
-
const isVlmPayload = doc.text
|
|
610
|
+
const isVlmPayload = hasVlmPayload(doc.text);
|
|
597
611
|
const mixedPrompt = isVlmPayload
|
|
598
612
|
? `You are a precise data extraction engine. Return only valid JSON.\n\n${prompt}`
|
|
599
613
|
: prompt;
|
|
@@ -821,7 +835,9 @@ export class PolicyEngine {
|
|
|
821
835
|
const allowLearnedFallback = opts.allowLearnedFallback !== false && !forcedPolicyId;
|
|
822
836
|
if (allowLearnedFallback && doc.supabase && policies.length > 0) {
|
|
823
837
|
try {
|
|
824
|
-
const learningText = doc.text
|
|
838
|
+
const learningText = doc.text
|
|
839
|
+
.replace(/\[VLM_IMAGE_DATA:[^\]]+\]/g, "")
|
|
840
|
+
.replace(/\[VLM_PDF_DATA:[^\]]+\]/g, "");
|
|
825
841
|
const learned = await PolicyLearningService.resolveLearnedCandidate({
|
|
826
842
|
supabase: doc.supabase,
|
|
827
843
|
userId: doc.userId,
|
|
@@ -923,7 +939,7 @@ export class PolicyEngine {
|
|
|
923
939
|
`Include the calendar year if clearly present. Prefer hyphenated multi-word tags.\n` +
|
|
924
940
|
`No markdown, no explanation — only the JSON object.`;
|
|
925
941
|
const userPrompt = `Extract the following fields from the document:\n${fieldList}`;
|
|
926
|
-
const isVlmPayload = doc.text
|
|
942
|
+
const isVlmPayload = hasVlmPayload(doc.text);
|
|
927
943
|
const mixedPrompt = isVlmPayload ? `${systemPrompt}\n\n${userPrompt}` : userPrompt;
|
|
928
944
|
try {
|
|
929
945
|
Actuator.logEvent(doc.ingestionId, doc.userId, "analysis", "Baseline Extraction", {
|
|
@@ -90,8 +90,8 @@ export class RAGService {
|
|
|
90
90
|
* Process an ingested document's raw text: chunk it, embed it, and store in DB.
|
|
91
91
|
*/
|
|
92
92
|
static async chunkAndEmbed(ingestionId, userId, rawText, supabase, settings) {
|
|
93
|
-
if (
|
|
94
|
-
logger.info(`Skipping chunking and embedding for VLM base64
|
|
93
|
+
if (/^\[VLM_(IMAGE|PDF)_DATA:/.test(rawText)) {
|
|
94
|
+
logger.info(`Skipping chunking and embedding for VLM base64 multimodal data (Ingestion: ${ingestionId})`);
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
const chunks = this.chunkText(rawText);
|