@pellux/goodvibes-sdk 0.27.2 → 0.27.3
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/_internal/contracts/artifacts/operator-contract.json +134 -4
- package/dist/_internal/contracts/generated/foundation-metadata.d.ts +2 -2
- package/dist/_internal/contracts/generated/foundation-metadata.js +2 -2
- package/dist/_internal/contracts/generated/operator-contract.d.ts.map +1 -1
- package/dist/_internal/contracts/generated/operator-contract.js +134 -4
- package/dist/_internal/contracts/generated/operator-method-ids.d.ts +1 -1
- package/dist/_internal/contracts/generated/operator-method-ids.d.ts.map +1 -1
- package/dist/_internal/contracts/generated/operator-method-ids.js +1 -0
- package/dist/_internal/platform/control-plane/method-catalog-homegraph.d.ts.map +1 -1
- package/dist/_internal/platform/control-plane/method-catalog-homegraph.js +10 -1
- package/dist/_internal/platform/control-plane/operator-contract-schemas-knowledge.d.ts +1 -0
- package/dist/_internal/platform/control-plane/operator-contract-schemas-knowledge.d.ts.map +1 -1
- package/dist/_internal/platform/control-plane/operator-contract-schemas-knowledge.js +7 -0
- package/dist/_internal/platform/daemon/http/home-graph-routes.d.ts.map +1 -1
- package/dist/_internal/platform/daemon/http/home-graph-routes.js +7 -0
- package/dist/_internal/platform/knowledge/extractors.d.ts.map +1 -1
- package/dist/_internal/platform/knowledge/extractors.js +1 -116
- package/dist/_internal/platform/knowledge/home-graph/auto-link.d.ts +27 -0
- package/dist/_internal/platform/knowledge/home-graph/auto-link.d.ts.map +1 -0
- package/dist/_internal/platform/knowledge/home-graph/auto-link.js +236 -0
- package/dist/_internal/platform/knowledge/home-graph/generated-pages.d.ts +1 -0
- package/dist/_internal/platform/knowledge/home-graph/generated-pages.d.ts.map +1 -1
- package/dist/_internal/platform/knowledge/home-graph/generated-pages.js +14 -8
- package/dist/_internal/platform/knowledge/home-graph/index.d.ts +1 -1
- package/dist/_internal/platform/knowledge/home-graph/index.d.ts.map +1 -1
- package/dist/_internal/platform/knowledge/home-graph/pages.d.ts +11 -0
- package/dist/_internal/platform/knowledge/home-graph/pages.d.ts.map +1 -0
- package/dist/_internal/platform/knowledge/home-graph/pages.js +44 -0
- package/dist/_internal/platform/knowledge/home-graph/rendering.d.ts +3 -1
- package/dist/_internal/platform/knowledge/home-graph/rendering.d.ts.map +1 -1
- package/dist/_internal/platform/knowledge/home-graph/rendering.js +161 -4
- package/dist/_internal/platform/knowledge/home-graph/service.d.ts +6 -1
- package/dist/_internal/platform/knowledge/home-graph/service.d.ts.map +1 -1
- package/dist/_internal/platform/knowledge/home-graph/service.js +56 -3
- package/dist/_internal/platform/knowledge/home-graph/types.d.ts +17 -0
- package/dist/_internal/platform/knowledge/home-graph/types.d.ts.map +1 -1
- package/dist/_internal/platform/knowledge/index.d.ts +1 -1
- package/dist/_internal/platform/knowledge/index.d.ts.map +1 -1
- package/dist/_internal/platform/knowledge/pdf-extractor.d.ts +3 -0
- package/dist/_internal/platform/knowledge/pdf-extractor.d.ts.map +1 -0
- package/dist/_internal/platform/knowledge/pdf-extractor.js +346 -0
- package/dist/_internal/platform/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { inflateSync } from 'node:zlib';
|
|
2
|
+
const MAX_STRUCTURE_SEARCH_TEXT_CHARS = 128 * 1024;
|
|
3
|
+
function cleanText(value) {
|
|
4
|
+
return value
|
|
5
|
+
.replace(/\u0000/g, ' ')
|
|
6
|
+
.replace(/\r\n/g, '\n')
|
|
7
|
+
.replace(/\r/g, '\n')
|
|
8
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
9
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
10
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
11
|
+
.trim();
|
|
12
|
+
}
|
|
13
|
+
function searchTextPayload(value) {
|
|
14
|
+
const cleaned = cleanText(value);
|
|
15
|
+
if (!cleaned || looksBinaryLike(cleaned) || looksLikeRawPdfPayload(cleaned))
|
|
16
|
+
return undefined;
|
|
17
|
+
return cleaned.length <= MAX_STRUCTURE_SEARCH_TEXT_CHARS
|
|
18
|
+
? cleaned
|
|
19
|
+
: cleaned.slice(0, MAX_STRUCTURE_SEARCH_TEXT_CHARS);
|
|
20
|
+
}
|
|
21
|
+
function estimateTokens(...chunks) {
|
|
22
|
+
const total = chunks
|
|
23
|
+
.filter((value) => typeof value === 'string')
|
|
24
|
+
.reduce((sum, value) => sum + value.length, 0);
|
|
25
|
+
return Math.max(1, Math.ceil(total / 4));
|
|
26
|
+
}
|
|
27
|
+
function firstNonEmptyLine(value) {
|
|
28
|
+
return value
|
|
29
|
+
.split(/\n+/)
|
|
30
|
+
.map((line) => line.trim())
|
|
31
|
+
.find(Boolean);
|
|
32
|
+
}
|
|
33
|
+
function summarizeText(text, maxLength = 320) {
|
|
34
|
+
const cleaned = cleanText(text);
|
|
35
|
+
if (!cleaned || looksBinaryLike(cleaned) || looksLikeRawPdfPayload(cleaned))
|
|
36
|
+
return undefined;
|
|
37
|
+
if (cleaned.length <= maxLength)
|
|
38
|
+
return cleaned;
|
|
39
|
+
const sentence = cleaned.match(/^(.{0,320}?[.!?])(?:\s|$)/)?.[1]?.trim();
|
|
40
|
+
return sentence && sentence.length >= 40 ? sentence : `${cleaned.slice(0, maxLength - 1).trim()}...`;
|
|
41
|
+
}
|
|
42
|
+
function excerptText(text, maxLength = 480) {
|
|
43
|
+
const cleaned = cleanText(text);
|
|
44
|
+
if (!cleaned || looksBinaryLike(cleaned) || looksLikeRawPdfPayload(cleaned))
|
|
45
|
+
return undefined;
|
|
46
|
+
return cleaned.length <= maxLength ? cleaned : `${cleaned.slice(0, maxLength - 1).trim()}...`;
|
|
47
|
+
}
|
|
48
|
+
function uniqueStrings(values, limit = 24) {
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const result = [];
|
|
51
|
+
for (const value of values) {
|
|
52
|
+
const trimmed = cleanText(value);
|
|
53
|
+
if (!trimmed || seen.has(trimmed) || !isReadablePdfText(trimmed))
|
|
54
|
+
continue;
|
|
55
|
+
seen.add(trimmed);
|
|
56
|
+
result.push(trimmed);
|
|
57
|
+
if (result.length >= limit)
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
export async function extractPdf(buffer) {
|
|
63
|
+
const parsed = await extractPdfWithPdfJs(buffer);
|
|
64
|
+
if (parsed)
|
|
65
|
+
return parsed;
|
|
66
|
+
return extractPdfRawStreams(buffer);
|
|
67
|
+
}
|
|
68
|
+
async function extractPdfWithPdfJs(buffer) {
|
|
69
|
+
try {
|
|
70
|
+
const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
|
|
71
|
+
const loadingTask = pdfjs.getDocument({
|
|
72
|
+
data: new Uint8Array(buffer),
|
|
73
|
+
useSystemFonts: true,
|
|
74
|
+
});
|
|
75
|
+
const document = await loadingTask.promise;
|
|
76
|
+
const pageCount = document.numPages;
|
|
77
|
+
const pageTexts = [];
|
|
78
|
+
for (let pageNumber = 1; pageNumber <= pageCount; pageNumber += 1) {
|
|
79
|
+
const page = await document.getPage(pageNumber);
|
|
80
|
+
const content = await page.getTextContent();
|
|
81
|
+
const lines = textContentItemsToLines(content.items);
|
|
82
|
+
if (lines.length > 0)
|
|
83
|
+
pageTexts.push(lines.join('\n'));
|
|
84
|
+
page.cleanup();
|
|
85
|
+
}
|
|
86
|
+
await document.destroy();
|
|
87
|
+
const text = cleanText(pageTexts.join('\n\n'));
|
|
88
|
+
if (!text)
|
|
89
|
+
return undefined;
|
|
90
|
+
const searchText = searchTextPayload(text);
|
|
91
|
+
return {
|
|
92
|
+
extractorId: 'pdfjs',
|
|
93
|
+
format: 'pdf',
|
|
94
|
+
title: firstNonEmptyLine(text) ?? 'PDF document',
|
|
95
|
+
summary: summarizeText(text) ?? 'PDF document.',
|
|
96
|
+
excerpt: excerptText(text),
|
|
97
|
+
sections: uniqueStrings(text.split(/\n+/), 24),
|
|
98
|
+
links: uniqueStrings(Array.from(text.matchAll(/\bhttps?:\/\/[^\s)]+/g), (match) => match[0]), 50),
|
|
99
|
+
estimatedTokens: estimateTokens(text),
|
|
100
|
+
structure: {
|
|
101
|
+
pageCount,
|
|
102
|
+
extractedTextChars: text.length,
|
|
103
|
+
...(searchText ? { searchText } : {}),
|
|
104
|
+
},
|
|
105
|
+
metadata: {
|
|
106
|
+
limitations: ['PDF text extraction does not perform OCR for scanned images.'],
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function textContentItemsToLines(items) {
|
|
115
|
+
const lines = [];
|
|
116
|
+
let current = '';
|
|
117
|
+
for (const item of items) {
|
|
118
|
+
const record = unknownRecord(item);
|
|
119
|
+
const text = typeof record.str === 'string' ? cleanText(record.str) : '';
|
|
120
|
+
if (text)
|
|
121
|
+
current = current ? `${current} ${text}` : text;
|
|
122
|
+
if (record.hasEOL === true && current) {
|
|
123
|
+
lines.push(current);
|
|
124
|
+
current = '';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (current)
|
|
128
|
+
lines.push(current);
|
|
129
|
+
return lines;
|
|
130
|
+
}
|
|
131
|
+
function unknownRecord(value) {
|
|
132
|
+
return value && typeof value === 'object' ? value : {};
|
|
133
|
+
}
|
|
134
|
+
function extractPdfRawStreams(buffer) {
|
|
135
|
+
const body = buffer.toString('latin1');
|
|
136
|
+
const texts = [];
|
|
137
|
+
const streamRe = /(<<[\s\S]{0,4096}?>>)\s*stream\r?\n([\s\S]*?)\r?\nendstream/g;
|
|
138
|
+
let match;
|
|
139
|
+
while ((match = streamRe.exec(body)) !== null) {
|
|
140
|
+
const dictionary = match[1] ?? '';
|
|
141
|
+
const rawChunk = match[2] ?? '';
|
|
142
|
+
const chunk = decodePdfStreamChunk(dictionary, rawChunk);
|
|
143
|
+
for (const text of extractPdfTextStrings(chunk)) {
|
|
144
|
+
if (isReadablePdfText(text))
|
|
145
|
+
texts.push(text);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const combined = uniqueStrings(texts, 64).join('\n');
|
|
149
|
+
const searchable = uniqueStrings(texts, 512).join('\n');
|
|
150
|
+
const searchText = searchTextPayload(searchable);
|
|
151
|
+
return {
|
|
152
|
+
extractorId: 'pdf',
|
|
153
|
+
format: 'pdf',
|
|
154
|
+
title: firstNonEmptyLine(combined) ?? 'PDF document',
|
|
155
|
+
summary: summarizeText(combined) ?? 'PDF extraction produced limited text; OCR is not used in-core.',
|
|
156
|
+
excerpt: excerptText(combined),
|
|
157
|
+
sections: uniqueStrings(combined.split(/\n+/), 8),
|
|
158
|
+
links: uniqueStrings(Array.from(combined.matchAll(/\bhttps?:\/\/[^\s)]+/g), (linkMatch) => linkMatch[0]), 50),
|
|
159
|
+
estimatedTokens: estimateTokens(combined),
|
|
160
|
+
structure: {
|
|
161
|
+
extractedStringCount: texts.length,
|
|
162
|
+
...(searchText ? { searchText } : {}),
|
|
163
|
+
},
|
|
164
|
+
metadata: {
|
|
165
|
+
limitations: texts.length === 0
|
|
166
|
+
? ['No readable text streams were found. Complex PDFs need OCR or a dedicated provider.']
|
|
167
|
+
: ['PDF extraction is best-effort and does not use OCR.'],
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function decodePdfStreamChunk(dictionary, rawChunk) {
|
|
172
|
+
if (!/\/FlateDecode\b/i.test(dictionary))
|
|
173
|
+
return rawChunk;
|
|
174
|
+
try {
|
|
175
|
+
return inflateSync(Buffer.from(rawChunk, 'latin1')).toString('latin1');
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return '';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function extractPdfTextStrings(chunk) {
|
|
182
|
+
return [
|
|
183
|
+
...extractLiteralStrings(chunk),
|
|
184
|
+
...extractHexStrings(chunk),
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
function extractLiteralStrings(chunk) {
|
|
188
|
+
const values = [];
|
|
189
|
+
let index = 0;
|
|
190
|
+
while (index < chunk.length) {
|
|
191
|
+
if (chunk[index] !== '(') {
|
|
192
|
+
index += 1;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const parsed = readPdfLiteralString(chunk, index + 1);
|
|
196
|
+
if (parsed) {
|
|
197
|
+
values.push(cleanText(parsed.value));
|
|
198
|
+
index = parsed.nextIndex;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
index += 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return values;
|
|
205
|
+
}
|
|
206
|
+
function readPdfLiteralString(chunk, start) {
|
|
207
|
+
let depth = 1;
|
|
208
|
+
let escaped = false;
|
|
209
|
+
let value = '';
|
|
210
|
+
for (let index = start; index < chunk.length; index += 1) {
|
|
211
|
+
const char = chunk[index];
|
|
212
|
+
if (escaped) {
|
|
213
|
+
const decoded = decodePdfEscape(char, chunk.slice(index + 1, index + 3));
|
|
214
|
+
value += decoded.value;
|
|
215
|
+
index += decoded.consumed;
|
|
216
|
+
escaped = false;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (char === '\\') {
|
|
220
|
+
escaped = true;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (char === '(') {
|
|
224
|
+
depth += 1;
|
|
225
|
+
value += char;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (char === ')') {
|
|
229
|
+
depth -= 1;
|
|
230
|
+
if (depth === 0)
|
|
231
|
+
return { value, nextIndex: index + 1 };
|
|
232
|
+
value += char;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
value += char;
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
function decodePdfEscape(char, following) {
|
|
240
|
+
switch (char) {
|
|
241
|
+
case 'n':
|
|
242
|
+
return { value: '\n', consumed: 0 };
|
|
243
|
+
case 'r':
|
|
244
|
+
return { value: '\r', consumed: 0 };
|
|
245
|
+
case 't':
|
|
246
|
+
return { value: '\t', consumed: 0 };
|
|
247
|
+
case 'b':
|
|
248
|
+
return { value: '\b', consumed: 0 };
|
|
249
|
+
case 'f':
|
|
250
|
+
return { value: '\f', consumed: 0 };
|
|
251
|
+
case '(':
|
|
252
|
+
case ')':
|
|
253
|
+
case '\\':
|
|
254
|
+
return { value: char, consumed: 0 };
|
|
255
|
+
default:
|
|
256
|
+
if (/[0-7]/.test(char)) {
|
|
257
|
+
const octal = `${char}${(following.match(/^[0-7]{0,2}/)?.[0] ?? '')}`;
|
|
258
|
+
return { value: String.fromCharCode(Number.parseInt(octal, 8)), consumed: octal.length - 1 };
|
|
259
|
+
}
|
|
260
|
+
return { value: char, consumed: 0 };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function extractHexStrings(chunk) {
|
|
264
|
+
const values = [];
|
|
265
|
+
const hexRe = /<([0-9A-Fa-f\s]{4,})>/g;
|
|
266
|
+
let match;
|
|
267
|
+
while ((match = hexRe.exec(chunk)) !== null) {
|
|
268
|
+
const text = decodeHexPdfString(match[1] ?? '');
|
|
269
|
+
if (text)
|
|
270
|
+
values.push(cleanText(text));
|
|
271
|
+
}
|
|
272
|
+
return values;
|
|
273
|
+
}
|
|
274
|
+
function decodeHexPdfString(value) {
|
|
275
|
+
const hex = value.replace(/\s+/g, '');
|
|
276
|
+
if (hex.length < 4 || hex.length % 2 !== 0)
|
|
277
|
+
return undefined;
|
|
278
|
+
const bytes = Buffer.from(hex, 'hex');
|
|
279
|
+
if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
|
|
280
|
+
return decodeUtf16Be(bytes.subarray(2));
|
|
281
|
+
}
|
|
282
|
+
const mostlyUtf16 = bytes.length >= 4 && bytes.filter((byte, index) => index % 2 === 0 && byte === 0).length >= Math.floor(bytes.length / 4);
|
|
283
|
+
if (mostlyUtf16)
|
|
284
|
+
return decodeUtf16Be(bytes);
|
|
285
|
+
return bytes.toString('latin1');
|
|
286
|
+
}
|
|
287
|
+
function decodeUtf16Be(bytes) {
|
|
288
|
+
const swapped = Buffer.alloc(bytes.length);
|
|
289
|
+
for (let index = 0; index + 1 < bytes.length; index += 2) {
|
|
290
|
+
swapped[index] = bytes[index + 1];
|
|
291
|
+
swapped[index + 1] = bytes[index];
|
|
292
|
+
}
|
|
293
|
+
return swapped.toString('utf16le');
|
|
294
|
+
}
|
|
295
|
+
function isReadablePdfText(value) {
|
|
296
|
+
const text = cleanText(value);
|
|
297
|
+
if (text.length < 2 || looksLikeRawPdfPayload(text) || looksBinaryLike(text))
|
|
298
|
+
return false;
|
|
299
|
+
const sample = text.slice(0, 512);
|
|
300
|
+
let lettersOrDigits = 0;
|
|
301
|
+
let whitespace = 0;
|
|
302
|
+
for (const char of sample) {
|
|
303
|
+
if (/[a-z0-9]/i.test(char))
|
|
304
|
+
lettersOrDigits += 1;
|
|
305
|
+
if (/\s/.test(char))
|
|
306
|
+
whitespace += 1;
|
|
307
|
+
}
|
|
308
|
+
return (lettersOrDigits + whitespace) / sample.length >= 0.55;
|
|
309
|
+
}
|
|
310
|
+
function looksLikeRawPdfPayload(value) {
|
|
311
|
+
const lower = value.toLowerCase();
|
|
312
|
+
return lower.includes('%pdf')
|
|
313
|
+
|| /\b\d+\s+\d+\s+obj\b/.test(lower)
|
|
314
|
+
|| (lower.includes(' endobj') && lower.includes(' stream'))
|
|
315
|
+
|| (lower.includes('/filter') && lower.includes('/flatedecode'));
|
|
316
|
+
}
|
|
317
|
+
function looksBinaryLike(value) {
|
|
318
|
+
const sample = value.slice(0, 4_096);
|
|
319
|
+
if (sample.length < 120)
|
|
320
|
+
return false;
|
|
321
|
+
let control = 0;
|
|
322
|
+
let extended = 0;
|
|
323
|
+
let letters = 0;
|
|
324
|
+
let whitespace = 0;
|
|
325
|
+
let punctuation = 0;
|
|
326
|
+
for (const char of sample) {
|
|
327
|
+
const code = char.charCodeAt(0);
|
|
328
|
+
if ((code < 32 && char !== '\n' && char !== '\r' && char !== '\t') || code === 65533)
|
|
329
|
+
control += 1;
|
|
330
|
+
if (code > 126)
|
|
331
|
+
extended += 1;
|
|
332
|
+
if (/[a-z0-9]/i.test(char))
|
|
333
|
+
letters += 1;
|
|
334
|
+
if (/\s/.test(char))
|
|
335
|
+
whitespace += 1;
|
|
336
|
+
if (/[^a-z0-9\s]/i.test(char))
|
|
337
|
+
punctuation += 1;
|
|
338
|
+
}
|
|
339
|
+
const length = sample.length;
|
|
340
|
+
const extendedRatio = extended / length;
|
|
341
|
+
const usefulRatio = (letters + whitespace) / length;
|
|
342
|
+
const punctuationRatio = punctuation / length;
|
|
343
|
+
return control > 0
|
|
344
|
+
|| (extendedRatio > 0.18 && usefulRatio < 0.78)
|
|
345
|
+
|| (punctuationRatio > 0.42 && whitespace / length < 0.08);
|
|
346
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
let version = '0.27.
|
|
3
|
+
let version = '0.27.3';
|
|
4
4
|
try {
|
|
5
5
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf-8'));
|
|
6
6
|
version = pkg.version ?? version;
|