@modelprofile.com/flexharness 8.1.0 → 8.2.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts_document/index.d.ts +50 -3
- package/dist_ts_document/index.js +96 -16
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_document/index.ts +147 -18
- package/ts_document/readme.md +26 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@modelprofile.com/flexharness',
|
|
6
|
-
version: '8.
|
|
6
|
+
version: '8.2.0',
|
|
7
7
|
description: 'The FlexHarness toolbox source repository and shared dependency catalog.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSwrQkFBK0I7SUFDckMsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLDBFQUEwRTtDQUN4RixDQUFBIn0=
|
|
@@ -1,19 +1,66 @@
|
|
|
1
1
|
import type { ModelMessage } from 'ai';
|
|
2
|
-
import type { TSmartAiLanguageModel } from '@modelprofile.com/flexharness-models';
|
|
3
|
-
|
|
2
|
+
import type { TSmartAiLanguageModel, TSmartAiProviderOptions } from '@modelprofile.com/flexharness-models';
|
|
3
|
+
/** Options every document request accepts in addition to its model and messages. */
|
|
4
|
+
export interface IDocumentRequestOptions {
|
|
5
|
+
/** Passed to the model call unchanged, for example `{ openai: { store: false } }`. */
|
|
6
|
+
providerOptions?: TSmartAiProviderOptions;
|
|
7
|
+
/**
|
|
8
|
+
* Checked before each document is rendered and before the model call, and passed to the model
|
|
9
|
+
* call. SmartPDF cannot stop a document it is rendering, so that render finishes first.
|
|
10
|
+
*/
|
|
11
|
+
abortSignal?: AbortSignal;
|
|
12
|
+
/**
|
|
13
|
+
* The most rendered pages one request may send to the model. A request with more pages rejects
|
|
14
|
+
* with `DocumentPageLimitError` before the model is called; documents after the one that
|
|
15
|
+
* exceeded the limit are not rendered.
|
|
16
|
+
*/
|
|
17
|
+
maxPages?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface IDocumentOptions extends IDocumentRequestOptions {
|
|
4
20
|
model: TSmartAiLanguageModel;
|
|
5
21
|
systemMessage?: string;
|
|
6
22
|
userMessage: string;
|
|
7
23
|
pdfDocuments: Uint8Array[];
|
|
8
24
|
messageHistory?: ModelMessage[];
|
|
9
25
|
}
|
|
10
|
-
export interface IPdfTextExtractionOptions {
|
|
26
|
+
export interface IPdfTextExtractionOptions extends IDocumentRequestOptions {
|
|
11
27
|
model: TSmartAiLanguageModel;
|
|
12
28
|
systemMessage?: string;
|
|
13
29
|
userMessage?: string;
|
|
14
30
|
messageHistory?: ModelMessage[];
|
|
15
31
|
}
|
|
32
|
+
/** Token usage of a document request, summed over its model steps; usage a provider leaves out counts as zero. */
|
|
33
|
+
export interface IDocumentUsage {
|
|
34
|
+
inputTokens: number;
|
|
35
|
+
outputTokens: number;
|
|
36
|
+
totalTokens: number;
|
|
37
|
+
cacheReadTokens: number;
|
|
38
|
+
cacheWriteTokens: number;
|
|
39
|
+
}
|
|
40
|
+
export interface IDocumentAnalysisResult {
|
|
41
|
+
text: string;
|
|
42
|
+
usage: IDocumentUsage;
|
|
43
|
+
/** Rendered pages sent to the model. A page that renders to an empty image is not sent. */
|
|
44
|
+
pageCount: number;
|
|
45
|
+
}
|
|
46
|
+
export interface IDocumentPageLimitErrorDetails {
|
|
47
|
+
renderedPages: number;
|
|
48
|
+
maxPages: number;
|
|
49
|
+
documentCount: number;
|
|
50
|
+
allDocumentsRendered: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** A request whose documents render to more pages than its `maxPages`; the model was not called. */
|
|
53
|
+
export declare class DocumentPageLimitError extends Error {
|
|
54
|
+
/** Pages rendered before the request stopped; with several documents, later documents are not counted. */
|
|
55
|
+
readonly renderedPages: number;
|
|
56
|
+
readonly maxPages: number;
|
|
57
|
+
constructor(details: IDocumentPageLimitErrorDetails);
|
|
58
|
+
}
|
|
16
59
|
/** The model-based PDF OCR recipe formerly provided by SmartOCR. */
|
|
17
60
|
export declare function extractTextFromPdf(pdfDocument: Uint8Array, options: IPdfTextExtractionOptions): Promise<string>;
|
|
61
|
+
/** `extractTextFromPdf` that also returns the request's token usage and the pages sent. */
|
|
62
|
+
export declare function extractTextFromPdfWithUsage(pdfDocument: Uint8Array, options: IPdfTextExtractionOptions): Promise<IDocumentAnalysisResult>;
|
|
18
63
|
export declare function analyzeDocuments(options: IDocumentOptions): Promise<string>;
|
|
64
|
+
/** `analyzeDocuments` that also returns the request's token usage and the pages sent. */
|
|
65
|
+
export declare function analyzeDocumentsWithUsage(options: IDocumentOptions): Promise<IDocumentAnalysisResult>;
|
|
19
66
|
export declare function stopSmartpdf(): Promise<void>;
|
|
@@ -1,28 +1,81 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
/** The one start of the shared SmartPDF instance; every request awaits it, and a failed start is forgotten. */
|
|
3
|
+
let smartpdfStart = null;
|
|
4
|
+
function ensureSmartpdf() {
|
|
5
|
+
if (!smartpdfStart) {
|
|
6
|
+
const instance = new plugins.smartpdf.SmartPdf();
|
|
7
|
+
const start = instance.start().then(() => instance);
|
|
8
|
+
smartpdfStart = start;
|
|
9
|
+
start.catch(() => {
|
|
10
|
+
if (smartpdfStart === start) {
|
|
11
|
+
smartpdfStart = null;
|
|
12
|
+
}
|
|
13
|
+
});
|
|
7
14
|
}
|
|
8
|
-
return
|
|
15
|
+
return smartpdfStart;
|
|
9
16
|
}
|
|
17
|
+
/** A request whose documents render to more pages than its `maxPages`; the model was not called. */
|
|
18
|
+
export class DocumentPageLimitError extends Error {
|
|
19
|
+
/** Pages rendered before the request stopped; with several documents, later documents are not counted. */
|
|
20
|
+
renderedPages;
|
|
21
|
+
maxPages;
|
|
22
|
+
constructor(details) {
|
|
23
|
+
const subject = details.documentCount === 1
|
|
24
|
+
? `The document has ${details.renderedPages} pages`
|
|
25
|
+
: details.allDocumentsRendered
|
|
26
|
+
? `The documents have ${details.renderedPages} pages`
|
|
27
|
+
: `The documents have at least ${details.renderedPages} pages`;
|
|
28
|
+
super(`${subject}, more than the ${details.maxPages} allowed.`);
|
|
29
|
+
this.name = 'DocumentPageLimitError';
|
|
30
|
+
this.renderedPages = details.renderedPages;
|
|
31
|
+
this.maxPages = details.maxPages;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const DEFAULT_OCR_SYSTEM_MESSAGE = 'You are a precise OCR engine. Extract text faithfully and do not summarize.';
|
|
35
|
+
const DEFAULT_OCR_USER_MESSAGE = 'Extract all readable text from this PDF. Preserve page order, line breaks, table structure, and obvious headings. Return only the extracted text.';
|
|
10
36
|
/** The model-based PDF OCR recipe formerly provided by SmartOCR. */
|
|
11
37
|
export async function extractTextFromPdf(pdfDocument, options) {
|
|
12
|
-
return
|
|
38
|
+
return (await extractTextFromPdfWithUsage(pdfDocument, options)).text;
|
|
39
|
+
}
|
|
40
|
+
/** `extractTextFromPdf` that also returns the request's token usage and the pages sent. */
|
|
41
|
+
export async function extractTextFromPdfWithUsage(pdfDocument, options) {
|
|
42
|
+
return analyzeDocumentsWithUsage({
|
|
13
43
|
model: options.model,
|
|
14
|
-
systemMessage: options.systemMessage ??
|
|
15
|
-
userMessage: options.userMessage ??
|
|
44
|
+
systemMessage: options.systemMessage ?? DEFAULT_OCR_SYSTEM_MESSAGE,
|
|
45
|
+
userMessage: options.userMessage ?? DEFAULT_OCR_USER_MESSAGE,
|
|
16
46
|
pdfDocuments: [pdfDocument],
|
|
17
47
|
messageHistory: options.messageHistory,
|
|
48
|
+
providerOptions: options.providerOptions,
|
|
49
|
+
abortSignal: options.abortSignal,
|
|
50
|
+
maxPages: options.maxPages,
|
|
18
51
|
});
|
|
19
52
|
}
|
|
20
53
|
export async function analyzeDocuments(options) {
|
|
21
|
-
|
|
54
|
+
return (await analyzeDocumentsWithUsage(options)).text;
|
|
55
|
+
}
|
|
56
|
+
/** `analyzeDocuments` that also returns the request's token usage and the pages sent. */
|
|
57
|
+
export async function analyzeDocumentsWithUsage(options) {
|
|
58
|
+
const maxPages = options.maxPages;
|
|
59
|
+
if (maxPages !== undefined && (!Number.isSafeInteger(maxPages) || maxPages < 1)) {
|
|
60
|
+
throw new RangeError('maxPages must be a positive whole number.');
|
|
61
|
+
}
|
|
62
|
+
const abortSignal = options.abortSignal;
|
|
63
|
+
abortSignal?.throwIfAborted();
|
|
22
64
|
const imagePages = [];
|
|
23
|
-
for (const doc of options.pdfDocuments) {
|
|
65
|
+
for (const [index, doc] of options.pdfDocuments.entries()) {
|
|
66
|
+
const pdf = await ensureSmartpdf();
|
|
67
|
+
abortSignal?.throwIfAborted();
|
|
24
68
|
const pages = await pdf.convertPDFToPngBytes(doc);
|
|
69
|
+
abortSignal?.throwIfAborted();
|
|
25
70
|
imagePages.push(...pages);
|
|
71
|
+
if (maxPages !== undefined && imagePages.length > maxPages) {
|
|
72
|
+
throw new DocumentPageLimitError({
|
|
73
|
+
renderedPages: imagePages.length,
|
|
74
|
+
maxPages,
|
|
75
|
+
documentCount: options.pdfDocuments.length,
|
|
76
|
+
allDocumentsRendered: index === options.pdfDocuments.length - 1,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
26
79
|
}
|
|
27
80
|
// Filter out empty buffers
|
|
28
81
|
const validPages = imagePages.filter(page => page && page.length > 0);
|
|
@@ -43,13 +96,40 @@ export async function analyzeDocuments(options) {
|
|
|
43
96
|
],
|
|
44
97
|
},
|
|
45
98
|
],
|
|
99
|
+
providerOptions: options.providerOptions,
|
|
100
|
+
abortSignal,
|
|
46
101
|
});
|
|
47
|
-
|
|
102
|
+
const usage = result.totalUsage;
|
|
103
|
+
const inputTokens = usage.inputTokens ?? 0;
|
|
104
|
+
const outputTokens = usage.outputTokens ?? 0;
|
|
105
|
+
return {
|
|
106
|
+
text: result.text,
|
|
107
|
+
usage: {
|
|
108
|
+
inputTokens,
|
|
109
|
+
outputTokens,
|
|
110
|
+
totalTokens: inputTokens + outputTokens,
|
|
111
|
+
cacheReadTokens: usage.inputTokenDetails.cacheReadTokens ?? 0,
|
|
112
|
+
cacheWriteTokens: usage.inputTokenDetails.cacheWriteTokens ?? 0,
|
|
113
|
+
},
|
|
114
|
+
pageCount: validPages.length,
|
|
115
|
+
};
|
|
48
116
|
}
|
|
49
117
|
export async function stopSmartpdf() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
118
|
+
const start = smartpdfStart;
|
|
119
|
+
if (!start) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// forget the instance before waiting on anything, so a request made from now on starts a fresh one
|
|
123
|
+
// instead of receiving the instance that is being stopped
|
|
124
|
+
smartpdfStart = null;
|
|
125
|
+
let instance;
|
|
126
|
+
try {
|
|
127
|
+
instance = await start;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// a failed start left no started instance that could be stopped
|
|
131
|
+
return;
|
|
53
132
|
}
|
|
133
|
+
await instance.stop();
|
|
54
134
|
}
|
|
55
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
135
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19kb2N1bWVudC9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQU14QywrR0FBK0c7QUFDL0csSUFBSSxhQUFhLEdBQThCLElBQUksQ0FBQztBQUVwRCxTQUFTLGNBQWM7SUFDckIsSUFBSSxDQUFDLGFBQWEsRUFBRSxDQUFDO1FBQ25CLE1BQU0sUUFBUSxHQUFHLElBQUksT0FBTyxDQUFDLFFBQVEsQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUNqRCxNQUFNLEtBQUssR0FBRyxRQUFRLENBQUMsS0FBSyxFQUFFLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQ3BELGFBQWEsR0FBRyxLQUFLLENBQUM7UUFDdEIsS0FBSyxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUU7WUFDZixJQUFJLGFBQWEsS0FBSyxLQUFLLEVBQUUsQ0FBQztnQkFDNUIsYUFBYSxHQUFHLElBQUksQ0FBQztZQUN2QixDQUFDO1FBQ0gsQ0FBQyxDQUFDLENBQUM7SUFDTCxDQUFDO0lBQ0QsT0FBTyxhQUFhLENBQUM7QUFDdkIsQ0FBQztBQXlERCxvR0FBb0c7QUFDcEcsTUFBTSxPQUFPLHNCQUF1QixTQUFRLEtBQUs7SUFDL0MsMEdBQTBHO0lBQzFGLGFBQWEsQ0FBUztJQUN0QixRQUFRLENBQVM7SUFFakMsWUFBWSxPQUF1QztRQUNqRCxNQUFNLE9BQU8sR0FBRyxPQUFPLENBQUMsYUFBYSxLQUFLLENBQUM7WUFDekMsQ0FBQyxDQUFDLG9CQUFvQixPQUFPLENBQUMsYUFBYSxRQUFRO1lBQ25ELENBQUMsQ0FBQyxPQUFPLENBQUMsb0JBQW9CO2dCQUM1QixDQUFDLENBQUMsc0JBQXNCLE9BQU8sQ0FBQyxhQUFhLFFBQVE7Z0JBQ3JELENBQUMsQ0FBQywrQkFBK0IsT0FBTyxDQUFDLGFBQWEsUUFBUSxDQUFD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
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"author": "Task Venture Capital GmbH",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "@modelprofile.com/flexharness",
|
|
5
|
-
"version": "8.
|
|
5
|
+
"version": "8.2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"description": "Managed agent sessions with tools, MCP, chat, OCR, documents and browser control through focused subpath imports.",
|
|
8
8
|
"main": "./dist_ts/index.js",
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@modelprofile.com/flexharness-agent": "8.
|
|
66
|
-
"@modelprofile.com/flexharness-models": "8.
|
|
65
|
+
"@modelprofile.com/flexharness-agent": "8.2.0",
|
|
66
|
+
"@modelprofile.com/flexharness-models": "8.2.0",
|
|
67
67
|
"@push.rocks/smartrequest": "^5.0.1",
|
|
68
68
|
"ai": "^7.0.79",
|
|
69
69
|
"zod": "^4.4.1"
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts_document/index.ts
CHANGED
|
@@ -1,18 +1,44 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import type { ModelMessage } from 'ai';
|
|
3
|
-
import type { TSmartAiLanguageModel } from '@modelprofile.com/flexharness-models';
|
|
3
|
+
import type { TSmartAiLanguageModel, TSmartAiProviderOptions } from '@modelprofile.com/flexharness-models';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
type TSmartPdf = InstanceType<typeof plugins.smartpdf.SmartPdf>;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
/** The one start of the shared SmartPDF instance; every request awaits it, and a failed start is forgotten. */
|
|
8
|
+
let smartpdfStart: Promise<TSmartPdf> | null = null;
|
|
9
|
+
|
|
10
|
+
function ensureSmartpdf(): Promise<TSmartPdf> {
|
|
11
|
+
if (!smartpdfStart) {
|
|
12
|
+
const instance = new plugins.smartpdf.SmartPdf();
|
|
13
|
+
const start = instance.start().then(() => instance);
|
|
14
|
+
smartpdfStart = start;
|
|
15
|
+
start.catch(() => {
|
|
16
|
+
if (smartpdfStart === start) {
|
|
17
|
+
smartpdfStart = null;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
11
20
|
}
|
|
12
|
-
return
|
|
21
|
+
return smartpdfStart;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Options every document request accepts in addition to its model and messages. */
|
|
25
|
+
export interface IDocumentRequestOptions {
|
|
26
|
+
/** Passed to the model call unchanged, for example `{ openai: { store: false } }`. */
|
|
27
|
+
providerOptions?: TSmartAiProviderOptions;
|
|
28
|
+
/**
|
|
29
|
+
* Checked before each document is rendered and before the model call, and passed to the model
|
|
30
|
+
* call. SmartPDF cannot stop a document it is rendering, so that render finishes first.
|
|
31
|
+
*/
|
|
32
|
+
abortSignal?: AbortSignal;
|
|
33
|
+
/**
|
|
34
|
+
* The most rendered pages one request may send to the model. A request with more pages rejects
|
|
35
|
+
* with `DocumentPageLimitError` before the model is called; documents after the one that
|
|
36
|
+
* exceeded the limit are not rendered.
|
|
37
|
+
*/
|
|
38
|
+
maxPages?: number;
|
|
13
39
|
}
|
|
14
40
|
|
|
15
|
-
export interface IDocumentOptions {
|
|
41
|
+
export interface IDocumentOptions extends IDocumentRequestOptions {
|
|
16
42
|
model: TSmartAiLanguageModel;
|
|
17
43
|
systemMessage?: string;
|
|
18
44
|
userMessage: string;
|
|
@@ -20,34 +46,111 @@ export interface IDocumentOptions {
|
|
|
20
46
|
messageHistory?: ModelMessage[];
|
|
21
47
|
}
|
|
22
48
|
|
|
23
|
-
export interface IPdfTextExtractionOptions {
|
|
49
|
+
export interface IPdfTextExtractionOptions extends IDocumentRequestOptions {
|
|
24
50
|
model: TSmartAiLanguageModel;
|
|
25
51
|
systemMessage?: string;
|
|
26
52
|
userMessage?: string;
|
|
27
53
|
messageHistory?: ModelMessage[];
|
|
28
54
|
}
|
|
29
55
|
|
|
56
|
+
/** Token usage of a document request, summed over its model steps; usage a provider leaves out counts as zero. */
|
|
57
|
+
export interface IDocumentUsage {
|
|
58
|
+
inputTokens: number;
|
|
59
|
+
outputTokens: number;
|
|
60
|
+
totalTokens: number;
|
|
61
|
+
cacheReadTokens: number;
|
|
62
|
+
cacheWriteTokens: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface IDocumentAnalysisResult {
|
|
66
|
+
text: string;
|
|
67
|
+
usage: IDocumentUsage;
|
|
68
|
+
/** Rendered pages sent to the model. A page that renders to an empty image is not sent. */
|
|
69
|
+
pageCount: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface IDocumentPageLimitErrorDetails {
|
|
73
|
+
renderedPages: number;
|
|
74
|
+
maxPages: number;
|
|
75
|
+
documentCount: number;
|
|
76
|
+
allDocumentsRendered: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A request whose documents render to more pages than its `maxPages`; the model was not called. */
|
|
80
|
+
export class DocumentPageLimitError extends Error {
|
|
81
|
+
/** Pages rendered before the request stopped; with several documents, later documents are not counted. */
|
|
82
|
+
public readonly renderedPages: number;
|
|
83
|
+
public readonly maxPages: number;
|
|
84
|
+
|
|
85
|
+
constructor(details: IDocumentPageLimitErrorDetails) {
|
|
86
|
+
const subject = details.documentCount === 1
|
|
87
|
+
? `The document has ${details.renderedPages} pages`
|
|
88
|
+
: details.allDocumentsRendered
|
|
89
|
+
? `The documents have ${details.renderedPages} pages`
|
|
90
|
+
: `The documents have at least ${details.renderedPages} pages`;
|
|
91
|
+
super(`${subject}, more than the ${details.maxPages} allowed.`);
|
|
92
|
+
this.name = 'DocumentPageLimitError';
|
|
93
|
+
this.renderedPages = details.renderedPages;
|
|
94
|
+
this.maxPages = details.maxPages;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const DEFAULT_OCR_SYSTEM_MESSAGE = 'You are a precise OCR engine. Extract text faithfully and do not summarize.';
|
|
99
|
+
const DEFAULT_OCR_USER_MESSAGE = 'Extract all readable text from this PDF. Preserve page order, line breaks, table structure, and obvious headings. Return only the extracted text.';
|
|
100
|
+
|
|
30
101
|
/** The model-based PDF OCR recipe formerly provided by SmartOCR. */
|
|
31
102
|
export async function extractTextFromPdf(
|
|
32
103
|
pdfDocument: Uint8Array,
|
|
33
104
|
options: IPdfTextExtractionOptions,
|
|
34
105
|
): Promise<string> {
|
|
35
|
-
return
|
|
106
|
+
return (await extractTextFromPdfWithUsage(pdfDocument, options)).text;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** `extractTextFromPdf` that also returns the request's token usage and the pages sent. */
|
|
110
|
+
export async function extractTextFromPdfWithUsage(
|
|
111
|
+
pdfDocument: Uint8Array,
|
|
112
|
+
options: IPdfTextExtractionOptions,
|
|
113
|
+
): Promise<IDocumentAnalysisResult> {
|
|
114
|
+
return analyzeDocumentsWithUsage({
|
|
36
115
|
model: options.model,
|
|
37
|
-
systemMessage: options.systemMessage ??
|
|
38
|
-
userMessage: options.userMessage ??
|
|
116
|
+
systemMessage: options.systemMessage ?? DEFAULT_OCR_SYSTEM_MESSAGE,
|
|
117
|
+
userMessage: options.userMessage ?? DEFAULT_OCR_USER_MESSAGE,
|
|
39
118
|
pdfDocuments: [pdfDocument],
|
|
40
119
|
messageHistory: options.messageHistory,
|
|
120
|
+
providerOptions: options.providerOptions,
|
|
121
|
+
abortSignal: options.abortSignal,
|
|
122
|
+
maxPages: options.maxPages,
|
|
41
123
|
});
|
|
42
124
|
}
|
|
43
125
|
|
|
44
126
|
export async function analyzeDocuments(options: IDocumentOptions): Promise<string> {
|
|
45
|
-
|
|
127
|
+
return (await analyzeDocumentsWithUsage(options)).text;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** `analyzeDocuments` that also returns the request's token usage and the pages sent. */
|
|
131
|
+
export async function analyzeDocumentsWithUsage(options: IDocumentOptions): Promise<IDocumentAnalysisResult> {
|
|
132
|
+
const maxPages = options.maxPages;
|
|
133
|
+
if (maxPages !== undefined && (!Number.isSafeInteger(maxPages) || maxPages < 1)) {
|
|
134
|
+
throw new RangeError('maxPages must be a positive whole number.');
|
|
135
|
+
}
|
|
136
|
+
const abortSignal = options.abortSignal;
|
|
137
|
+
abortSignal?.throwIfAborted();
|
|
46
138
|
|
|
47
139
|
const imagePages: Uint8Array[] = [];
|
|
48
|
-
for (const doc of options.pdfDocuments) {
|
|
140
|
+
for (const [index, doc] of options.pdfDocuments.entries()) {
|
|
141
|
+
const pdf = await ensureSmartpdf();
|
|
142
|
+
abortSignal?.throwIfAborted();
|
|
49
143
|
const pages = await pdf.convertPDFToPngBytes(doc);
|
|
144
|
+
abortSignal?.throwIfAborted();
|
|
50
145
|
imagePages.push(...pages);
|
|
146
|
+
if (maxPages !== undefined && imagePages.length > maxPages) {
|
|
147
|
+
throw new DocumentPageLimitError({
|
|
148
|
+
renderedPages: imagePages.length,
|
|
149
|
+
maxPages,
|
|
150
|
+
documentCount: options.pdfDocuments.length,
|
|
151
|
+
allDocumentsRendered: index === options.pdfDocuments.length - 1,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
51
154
|
}
|
|
52
155
|
|
|
53
156
|
// Filter out empty buffers
|
|
@@ -70,13 +173,39 @@ export async function analyzeDocuments(options: IDocumentOptions): Promise<strin
|
|
|
70
173
|
],
|
|
71
174
|
},
|
|
72
175
|
],
|
|
176
|
+
providerOptions: options.providerOptions,
|
|
177
|
+
abortSignal,
|
|
73
178
|
});
|
|
74
|
-
|
|
179
|
+
const usage = result.totalUsage;
|
|
180
|
+
const inputTokens = usage.inputTokens ?? 0;
|
|
181
|
+
const outputTokens = usage.outputTokens ?? 0;
|
|
182
|
+
return {
|
|
183
|
+
text: result.text,
|
|
184
|
+
usage: {
|
|
185
|
+
inputTokens,
|
|
186
|
+
outputTokens,
|
|
187
|
+
totalTokens: inputTokens + outputTokens,
|
|
188
|
+
cacheReadTokens: usage.inputTokenDetails.cacheReadTokens ?? 0,
|
|
189
|
+
cacheWriteTokens: usage.inputTokenDetails.cacheWriteTokens ?? 0,
|
|
190
|
+
},
|
|
191
|
+
pageCount: validPages.length,
|
|
192
|
+
};
|
|
75
193
|
}
|
|
76
194
|
|
|
77
195
|
export async function stopSmartpdf(): Promise<void> {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
196
|
+
const start = smartpdfStart;
|
|
197
|
+
if (!start) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// forget the instance before waiting on anything, so a request made from now on starts a fresh one
|
|
201
|
+
// instead of receiving the instance that is being stopped
|
|
202
|
+
smartpdfStart = null;
|
|
203
|
+
let instance: TSmartPdf;
|
|
204
|
+
try {
|
|
205
|
+
instance = await start;
|
|
206
|
+
} catch {
|
|
207
|
+
// a failed start left no started instance that could be stopped
|
|
208
|
+
return;
|
|
81
209
|
}
|
|
210
|
+
await instance.stop();
|
|
82
211
|
}
|
package/ts_document/readme.md
CHANGED
|
@@ -23,6 +23,32 @@ Supply a model from your selected FlexHarness provider. `extractTextFromPdf` rep
|
|
|
23
23
|
|
|
24
24
|
`analyzeDocuments({ model, userMessage, pdfDocuments, systemMessage?, messageHistory? })` supports general analysis across multiple PDFs. This component owns a shared SmartPDF instance; call `stopSmartpdf()` when all document work has finished. Image-only OCR is available independently in `flexharness/ocr`.
|
|
25
25
|
|
|
26
|
+
Both functions accept three request options:
|
|
27
|
+
|
|
28
|
+
- `providerOptions` is passed to the model call unchanged, for example `{ openai: { store: false } }` so the provider does not keep the request as application state.
|
|
29
|
+
- `abortSignal` is checked before each document is rendered and before the model call, and is passed to the model call. SmartPDF cannot stop a document it is already rendering, so that render finishes first.
|
|
30
|
+
- `maxPages` is the most rendered pages one request may send to the model. A request with more pages rejects with `DocumentPageLimitError` (`renderedPages`, `maxPages`) before the model is called, and documents after the one that exceeded the limit are not rendered.
|
|
31
|
+
|
|
32
|
+
`extractTextFromPdfWithUsage` and `analyzeDocumentsWithUsage` take the same arguments and return `{ text, usage, pageCount }`. `usage` has the shape of an agent run's usage (`inputTokens`, `outputTokens`, `totalTokens`, `cacheReadTokens`, `cacheWriteTokens`), summed over the request's model steps, with usage the provider leaves out counted as zero; `pageCount` is the number of rendered pages sent to the model.
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { extractTextFromPdfWithUsage, DocumentPageLimitError } from '@modelprofile.com/flexharness/documents';
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const { text, usage, pageCount } = await extractTextFromPdfWithUsage(pdfBytes, {
|
|
39
|
+
model,
|
|
40
|
+
providerOptions: { openai: { store: false } },
|
|
41
|
+
abortSignal,
|
|
42
|
+
maxPages: 20,
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error instanceof DocumentPageLimitError) {
|
|
46
|
+
console.log(`${error.renderedPages} pages, at most ${error.maxPages}`);
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
26
52
|
## Issue Reporting and Security
|
|
27
53
|
|
|
28
54
|
Report issues and security concerns through [community.foss.global](https://community.foss.global).
|