@nebutra/document-pipeline 0.1.1 → 0.1.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/CHANGELOG.md +26 -0
- package/LICENSE +21 -676
- package/dist/{chunk-GC4W5KJ2.js → chunk-UF5MSWGW.js} +2 -2
- package/dist/{chunk-GC4W5KJ2.js.map → chunk-UF5MSWGW.js.map} +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +19 -9
- package/.nebutra/debug/content-store.jsonl +0 -6
- package/.nebutra/debug/document-pipeline.jsonl +0 -3
- package/.turbo/turbo-build.log +0 -22
- package/.turbo/turbo-test.log +0 -14
- package/.turbo/turbo-typecheck.log +0 -4
- package/examples/ingest-markdown.ts +0 -15
- package/examples/parse-html.ts +0 -15
- package/examples/sidecar-gate.ts +0 -14
- package/src/cli.ts +0 -31
- package/src/index.test.ts +0 -93
- package/src/index.ts +0 -353
- package/tsconfig.json +0 -12
- package/tsup.config.ts +0 -16
package/src/index.ts
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { extname, join } from "node:path";
|
|
3
|
-
import { requireCapabilityTenant } from "@nebutra/capability-kit";
|
|
4
|
-
import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
|
|
5
|
-
import {
|
|
6
|
-
ContentStore,
|
|
7
|
-
parseContentFrontmatter,
|
|
8
|
-
serializeContentFrontmatter,
|
|
9
|
-
splitContentParagraphs,
|
|
10
|
-
} from "@nebutra/content-store";
|
|
11
|
-
import { CapabilityError } from "@nebutra/errors";
|
|
12
|
-
|
|
13
|
-
export type ParserChoice = "auto" | "markdown" | "html" | "text" | "sidecar";
|
|
14
|
-
export type ChunkerChoice = "paragraph" | "recursive" | "hierarchical";
|
|
15
|
-
|
|
16
|
-
export interface InlineSource {
|
|
17
|
-
readonly type: "inline";
|
|
18
|
-
readonly path: string;
|
|
19
|
-
readonly content: string;
|
|
20
|
-
readonly mimeType?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface FileSource {
|
|
24
|
-
readonly type: "file";
|
|
25
|
-
readonly path: string;
|
|
26
|
-
readonly mimeType?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type DocumentSource = InlineSource | FileSource;
|
|
30
|
-
|
|
31
|
-
export interface IngestRequest {
|
|
32
|
-
readonly tenantId?: string;
|
|
33
|
-
readonly source: DocumentSource;
|
|
34
|
-
readonly parser?: ParserChoice;
|
|
35
|
-
readonly chunker?: ChunkerChoice;
|
|
36
|
-
readonly targetPath?: string;
|
|
37
|
-
readonly metadata?: Record<string, string>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface ParseRequest {
|
|
41
|
-
readonly tenantId?: string;
|
|
42
|
-
readonly source: DocumentSource;
|
|
43
|
-
readonly parser?: ParserChoice;
|
|
44
|
-
readonly chunker?: ChunkerChoice;
|
|
45
|
-
readonly metadata?: Record<string, string>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface ParsedChunk {
|
|
49
|
-
readonly id: string;
|
|
50
|
-
readonly text: string;
|
|
51
|
-
readonly metadata: {
|
|
52
|
-
readonly tenantId: string;
|
|
53
|
-
readonly sourcePath: string;
|
|
54
|
-
readonly chunkIndex: number;
|
|
55
|
-
readonly mimeType: string;
|
|
56
|
-
readonly parser: ParserChoice;
|
|
57
|
-
readonly heading?: string;
|
|
58
|
-
} & Record<string, string | number>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface ParsedDocument {
|
|
62
|
-
readonly tenantId: string;
|
|
63
|
-
readonly path: string;
|
|
64
|
-
readonly mimeType: string;
|
|
65
|
-
readonly parser: ParserChoice;
|
|
66
|
-
readonly chunks: readonly ParsedChunk[];
|
|
67
|
-
readonly frontmatter: Record<string, string>;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface IngestResult {
|
|
71
|
-
readonly tenantId: string;
|
|
72
|
-
readonly path: string;
|
|
73
|
-
readonly parser: ParserChoice;
|
|
74
|
-
readonly chunkCount: number;
|
|
75
|
-
readonly contentIndexed: true;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface DocumentPipelineDoctorReport {
|
|
79
|
-
readonly capability: "document-pipeline";
|
|
80
|
-
readonly nativeParsers: readonly ParserChoice[];
|
|
81
|
-
readonly sidecarConfigured: boolean;
|
|
82
|
-
readonly contentStore: Awaited<ReturnType<ContentStore["doctor"]>>;
|
|
83
|
-
readonly suggestion?: string;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface DocumentParser {
|
|
87
|
-
parse(request: RequiredTenant<ParseRequest>): Promise<ParsedDocument>;
|
|
88
|
-
doctor(): Promise<{ readonly ok: boolean; readonly suggestion?: string }>;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface DocumentPipelineOptions {
|
|
92
|
-
readonly tenantId?: string;
|
|
93
|
-
readonly root?: string;
|
|
94
|
-
readonly debugRoot?: string;
|
|
95
|
-
readonly contentStore?: ContentStore;
|
|
96
|
-
readonly sidecarParser?: DocumentParser;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
type RequiredTenant<T extends { readonly tenantId?: string }> = Omit<T, "tenantId"> & {
|
|
100
|
-
readonly tenantId: string;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
export async function readDocumentPipelineDebug(
|
|
104
|
-
root = process.cwd(),
|
|
105
|
-
limit = 20,
|
|
106
|
-
): Promise<unknown[]> {
|
|
107
|
-
return readCapabilityDebug("document-pipeline", { root, limit });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export class DocumentPipeline {
|
|
111
|
-
readonly #tenantId: string | undefined;
|
|
112
|
-
readonly #root: string;
|
|
113
|
-
readonly #debugRoot: string;
|
|
114
|
-
readonly #contentStore: ContentStore | undefined;
|
|
115
|
-
readonly #sidecarParser: DocumentParser | undefined;
|
|
116
|
-
|
|
117
|
-
constructor(options: DocumentPipelineOptions = {}) {
|
|
118
|
-
this.#tenantId = options.tenantId;
|
|
119
|
-
this.#root = options.root ?? join(process.cwd(), ".nebutra", "document-pipeline");
|
|
120
|
-
this.#debugRoot = options.debugRoot ?? process.cwd();
|
|
121
|
-
this.#contentStore = options.contentStore;
|
|
122
|
-
this.#sidecarParser = options.sidecarParser;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
static async open(
|
|
126
|
-
root = ".nebutra/document-pipeline",
|
|
127
|
-
options: Omit<DocumentPipelineOptions, "root" | "contentStore"> = {},
|
|
128
|
-
): Promise<DocumentPipeline> {
|
|
129
|
-
const tenantId = options.tenantId ?? "local";
|
|
130
|
-
const contentStore = await ContentStore.open(join(root, "content"), { tenantId });
|
|
131
|
-
return new DocumentPipeline({ ...options, tenantId, root, contentStore });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async parse(request: ParseRequest): Promise<ParsedDocument> {
|
|
135
|
-
const tenantId = requireCapabilityTenant({
|
|
136
|
-
explicit: request.tenantId,
|
|
137
|
-
fallback: this.#tenantId,
|
|
138
|
-
onMissing: () =>
|
|
139
|
-
new CapabilityError("document-pipeline", "Document ingestion requires tenant context", {
|
|
140
|
-
suggestion:
|
|
141
|
-
"Pass tenantId on the request or construct DocumentPipeline with a tenantId default.",
|
|
142
|
-
statusCode: 400,
|
|
143
|
-
}),
|
|
144
|
-
});
|
|
145
|
-
const required: RequiredTenant<ParseRequest> = { ...request, tenantId };
|
|
146
|
-
const parser = this.#chooseParser(required);
|
|
147
|
-
if (parser === "sidecar") {
|
|
148
|
-
if (!this.#sidecarParser) {
|
|
149
|
-
throw new CapabilityError("document-pipeline", "Parser sidecar is not configured", {
|
|
150
|
-
suggestion:
|
|
151
|
-
"Configure the parser sidecar for PDFs, office files, OCR, and layout-aware extraction.",
|
|
152
|
-
statusCode: 503,
|
|
153
|
-
metadata: { path: request.source.path },
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
const parsed = await this.#sidecarParser.parse(required);
|
|
157
|
-
await appendCapabilityDebug(
|
|
158
|
-
"document-pipeline",
|
|
159
|
-
{
|
|
160
|
-
type: "parse",
|
|
161
|
-
tenantId,
|
|
162
|
-
parser: "sidecar",
|
|
163
|
-
path: parsed.path,
|
|
164
|
-
},
|
|
165
|
-
{ root: this.#debugRoot },
|
|
166
|
-
);
|
|
167
|
-
return parsed;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const raw = await readSource(request.source);
|
|
171
|
-
const parsed = parseNativeDocument(required, raw, parser);
|
|
172
|
-
await appendCapabilityDebug(
|
|
173
|
-
"document-pipeline",
|
|
174
|
-
{
|
|
175
|
-
type: "parse",
|
|
176
|
-
tenantId,
|
|
177
|
-
parser,
|
|
178
|
-
path: parsed.path,
|
|
179
|
-
chunks: parsed.chunks.length,
|
|
180
|
-
},
|
|
181
|
-
{ root: this.#debugRoot },
|
|
182
|
-
);
|
|
183
|
-
return parsed;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async ingest(request: IngestRequest): Promise<IngestResult> {
|
|
187
|
-
const tenantId = requireCapabilityTenant({
|
|
188
|
-
explicit: request.tenantId,
|
|
189
|
-
fallback: this.#tenantId,
|
|
190
|
-
onMissing: () =>
|
|
191
|
-
new CapabilityError("document-pipeline", "Document ingestion requires tenant context", {
|
|
192
|
-
suggestion:
|
|
193
|
-
"Pass tenantId on the request or construct DocumentPipeline with a tenantId default.",
|
|
194
|
-
statusCode: 400,
|
|
195
|
-
}),
|
|
196
|
-
});
|
|
197
|
-
const contentStore = await this.#resolveContentStore(tenantId);
|
|
198
|
-
const parsed = await this.parse({ ...request, tenantId });
|
|
199
|
-
const targetPath = request.targetPath ?? parsed.path;
|
|
200
|
-
const content = serializeForContentStore(parsed, request.metadata);
|
|
201
|
-
await contentStore.write(targetPath, content);
|
|
202
|
-
await appendCapabilityDebug(
|
|
203
|
-
"document-pipeline",
|
|
204
|
-
{
|
|
205
|
-
type: "ingest",
|
|
206
|
-
tenantId,
|
|
207
|
-
path: targetPath,
|
|
208
|
-
parser: parsed.parser,
|
|
209
|
-
chunks: parsed.chunks.length,
|
|
210
|
-
},
|
|
211
|
-
{ root: this.#debugRoot },
|
|
212
|
-
);
|
|
213
|
-
return {
|
|
214
|
-
tenantId,
|
|
215
|
-
path: targetPath,
|
|
216
|
-
parser: parsed.parser,
|
|
217
|
-
chunkCount: parsed.chunks.length,
|
|
218
|
-
contentIndexed: true,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async ingestFile(
|
|
223
|
-
path: string,
|
|
224
|
-
options: Omit<IngestRequest, "source"> = {},
|
|
225
|
-
): Promise<IngestResult> {
|
|
226
|
-
return this.ingest({ ...options, source: { type: "file", path } });
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async doctor(): Promise<DocumentPipelineDoctorReport> {
|
|
230
|
-
const contentStore = await this.#resolveContentStore(this.#tenantId ?? "local");
|
|
231
|
-
const sidecar = this.#sidecarParser ? await this.#sidecarParser.doctor() : { ok: false };
|
|
232
|
-
return {
|
|
233
|
-
capability: "document-pipeline",
|
|
234
|
-
nativeParsers: ["markdown", "html", "text"],
|
|
235
|
-
sidecarConfigured: sidecar.ok,
|
|
236
|
-
contentStore: await contentStore.doctor(),
|
|
237
|
-
...(sidecar.ok
|
|
238
|
-
? {}
|
|
239
|
-
: {
|
|
240
|
-
suggestion:
|
|
241
|
-
"Native parsers are active; configure the parser sidecar before ingesting PDFs, office files, OCR, or complex layout documents.",
|
|
242
|
-
}),
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
#chooseParser(request: RequiredTenant<ParseRequest>): ParserChoice {
|
|
247
|
-
if (request.parser && request.parser !== "auto") return request.parser;
|
|
248
|
-
const mime = request.source.mimeType ?? mimeFromPath(request.source.path);
|
|
249
|
-
if (mime === "text/markdown") return "markdown";
|
|
250
|
-
if (mime === "text/html") return "html";
|
|
251
|
-
if (mime.startsWith("text/")) return "text";
|
|
252
|
-
return "sidecar";
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async #resolveContentStore(tenantId: string): Promise<ContentStore> {
|
|
256
|
-
if (this.#contentStore) return this.#contentStore;
|
|
257
|
-
return ContentStore.open(join(this.#root, "content"), { tenantId });
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async function readSource(source: DocumentSource): Promise<string> {
|
|
262
|
-
return source.type === "inline" ? source.content : readFile(source.path, "utf8");
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function mimeFromPath(path: string): string {
|
|
266
|
-
const extension = extname(path).toLowerCase();
|
|
267
|
-
if (extension === ".md" || extension === ".mdx") return "text/markdown";
|
|
268
|
-
if (extension === ".html" || extension === ".htm") return "text/html";
|
|
269
|
-
if (extension === ".txt" || extension === ".csv" || extension === ".json") return "text/plain";
|
|
270
|
-
if (extension === ".pdf") return "application/pdf";
|
|
271
|
-
if (extension === ".docx")
|
|
272
|
-
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
273
|
-
if (extension === ".pptx")
|
|
274
|
-
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
275
|
-
if (extension === ".png" || extension === ".jpg" || extension === ".jpeg") return "image";
|
|
276
|
-
return "application/octet-stream";
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function parseNativeDocument(
|
|
280
|
-
request: RequiredTenant<ParseRequest>,
|
|
281
|
-
raw: string,
|
|
282
|
-
parser: ParserChoice,
|
|
283
|
-
): ParsedDocument {
|
|
284
|
-
const mimeType = request.source.mimeType ?? mimeFromPath(request.source.path);
|
|
285
|
-
const { frontmatter, body } =
|
|
286
|
-
parser === "markdown" ? parseContentFrontmatter(raw) : { frontmatter: {}, body: raw };
|
|
287
|
-
const text = parser === "html" ? htmlToText(body) : body;
|
|
288
|
-
const chunks = splitContentParagraphs(text).map((chunk, index) => ({
|
|
289
|
-
id: `${request.tenantId}:${request.source.path}:${index}`,
|
|
290
|
-
text: chunk,
|
|
291
|
-
metadata: {
|
|
292
|
-
tenantId: request.tenantId,
|
|
293
|
-
sourcePath: request.source.path,
|
|
294
|
-
chunkIndex: index,
|
|
295
|
-
mimeType,
|
|
296
|
-
parser,
|
|
297
|
-
...(request.metadata ?? {}),
|
|
298
|
-
},
|
|
299
|
-
}));
|
|
300
|
-
return {
|
|
301
|
-
tenantId: request.tenantId,
|
|
302
|
-
path: request.source.path,
|
|
303
|
-
mimeType,
|
|
304
|
-
parser,
|
|
305
|
-
chunks:
|
|
306
|
-
chunks.length > 0
|
|
307
|
-
? chunks
|
|
308
|
-
: [
|
|
309
|
-
{
|
|
310
|
-
id: `${request.tenantId}:${request.source.path}:0`,
|
|
311
|
-
text,
|
|
312
|
-
metadata: {
|
|
313
|
-
tenantId: request.tenantId,
|
|
314
|
-
sourcePath: request.source.path,
|
|
315
|
-
chunkIndex: 0,
|
|
316
|
-
mimeType,
|
|
317
|
-
parser,
|
|
318
|
-
...(request.metadata ?? {}),
|
|
319
|
-
},
|
|
320
|
-
},
|
|
321
|
-
],
|
|
322
|
-
frontmatter,
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function serializeForContentStore(
|
|
327
|
-
parsed: ParsedDocument,
|
|
328
|
-
metadata: Record<string, string> | undefined,
|
|
329
|
-
): string {
|
|
330
|
-
const frontmatter = {
|
|
331
|
-
...parsed.frontmatter,
|
|
332
|
-
...(metadata ?? {}),
|
|
333
|
-
source_mime: parsed.mimeType,
|
|
334
|
-
parser: parsed.parser,
|
|
335
|
-
};
|
|
336
|
-
const body = parsed.chunks.map((chunk) => chunk.text).join("\n\n");
|
|
337
|
-
return serializeContentFrontmatter(frontmatter, body);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function htmlToText(html: string): string {
|
|
341
|
-
return html
|
|
342
|
-
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
343
|
-
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
344
|
-
.replace(/<\/(h[1-6]|p|li|tr|div)>/gi, "\n\n")
|
|
345
|
-
.replace(/<[^>]+>/g, " ")
|
|
346
|
-
.replace(/ /g, " ")
|
|
347
|
-
.replace(/&/g, "&")
|
|
348
|
-
.replace(/</g, "<")
|
|
349
|
-
.replace(/>/g, ">")
|
|
350
|
-
.replace(/[ \t]+/g, " ")
|
|
351
|
-
.replace(/\n\s+/g, "\n")
|
|
352
|
-
.trim();
|
|
353
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../../tsconfig.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"target": "esnext",
|
|
7
|
-
"types": ["node"],
|
|
8
|
-
"incremental": false
|
|
9
|
-
},
|
|
10
|
-
"include": ["src", "examples"],
|
|
11
|
-
"exclude": ["node_modules", "dist"]
|
|
12
|
-
}
|
package/tsup.config.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "tsup";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
entry: ["src/index.ts", "src/cli.ts"],
|
|
5
|
-
format: ["esm"],
|
|
6
|
-
dts: true,
|
|
7
|
-
sourcemap: true,
|
|
8
|
-
clean: true,
|
|
9
|
-
target: "es2022",
|
|
10
|
-
external: [
|
|
11
|
-
"@nebutra/capability-kit/debug",
|
|
12
|
-
"@nebutra/content-store",
|
|
13
|
-
"@nebutra/errors",
|
|
14
|
-
"@nebutra/sandbox-runtime",
|
|
15
|
-
],
|
|
16
|
-
});
|