@modelprofile.com/flexharness 8.4.0 → 9.0.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 +32 -8
- package/dist_ts_document/index.js +92 -31
- package/dist_ts_document/plugins.d.ts +4 -2
- package/dist_ts_document/plugins.js +4 -3
- package/package.json +4 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_document/index.ts +110 -36
- package/ts_document/plugins.ts +4 -2
- package/ts_document/readme.md +26 -3
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@modelprofile.com/flexharness',
|
|
6
|
-
version: '
|
|
6
|
+
version: '9.0.0',
|
|
7
7
|
description: 'The FlexHarness toolbox source repository and shared dependency catalog.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSwrQkFBK0I7SUFDckMsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLDBFQUEwRTtDQUN4RixDQUFBIn0=
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
1
2
|
import type { ModelMessage } from 'ai';
|
|
2
3
|
import type { TSmartAiLanguageModel, TSmartAiProviderOptions } from '@modelprofile.com/flexharness-models';
|
|
3
4
|
/** Options every document request accepts in addition to its model and messages. */
|
|
@@ -5,16 +6,28 @@ export interface IDocumentRequestOptions {
|
|
|
5
6
|
/** Passed to the model call unchanged, for example `{ openai: { store: false } }`. */
|
|
6
7
|
providerOptions?: TSmartAiProviderOptions;
|
|
7
8
|
/**
|
|
8
|
-
* Checked before each document is rendered and before the model call, and passed to the
|
|
9
|
-
* call
|
|
9
|
+
* Checked before each document is rendered and before the model call, and passed to both the
|
|
10
|
+
* render and the model call: an abort stops a document that is rendering. The request rejects
|
|
11
|
+
* with the signal's reason.
|
|
10
12
|
*/
|
|
11
13
|
abortSignal?: AbortSignal;
|
|
12
14
|
/**
|
|
13
|
-
* The most rendered pages one request may send to the model
|
|
14
|
-
* with `DocumentPageLimitError` before the model is called
|
|
15
|
-
*
|
|
15
|
+
* The most rendered pages one request may send to the model, counted over all its documents. A
|
|
16
|
+
* request with more pages rejects with `DocumentPageLimitError` before the model is called. Each
|
|
17
|
+
* document is rendered only when its page count fits the pages the request has left, so the
|
|
18
|
+
* document that exceeds the limit and every document after it are not rendered. The exception is
|
|
19
|
+
* a one-page document that comes after the request's pages are used up: it renders and is then
|
|
20
|
+
* refused.
|
|
16
21
|
*/
|
|
17
22
|
maxPages?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Called once for every model call of the request, as soon as its usage is known, whatever the
|
|
25
|
+
* request's outcome: `reported` with the provider's usage, or `unreported` with the reason when
|
|
26
|
+
* the call failed, was aborted or its response carried no usage. A retried attempt is a call of
|
|
27
|
+
* its own. Nothing is reported for a request that ends before its model is called. When a request
|
|
28
|
+
* returns, its reported calls sum to its `usage`. Must not throw.
|
|
29
|
+
*/
|
|
30
|
+
onUsage?: plugins.TAgentModelCallUsageReporter;
|
|
18
31
|
}
|
|
19
32
|
export interface IDocumentOptions extends IDocumentRequestOptions {
|
|
20
33
|
model: TSmartAiLanguageModel;
|
|
@@ -44,17 +57,28 @@ export interface IDocumentAnalysisResult {
|
|
|
44
57
|
pageCount: number;
|
|
45
58
|
}
|
|
46
59
|
export interface IDocumentPageLimitErrorDetails {
|
|
60
|
+
/** The pages of the documents up to and including the one that exceeded the limit. */
|
|
47
61
|
renderedPages: number;
|
|
48
62
|
maxPages: number;
|
|
49
63
|
documentCount: number;
|
|
64
|
+
/** Zero-based position, in the request's documents, of the document that exceeded the limit. */
|
|
65
|
+
documentIndex: number;
|
|
66
|
+
/** Whether the pages of every document of the request were counted. */
|
|
50
67
|
allDocumentsRendered: boolean;
|
|
51
68
|
}
|
|
52
|
-
/** A request whose documents
|
|
69
|
+
/** A request whose documents have more pages than its `maxPages`; the model was not called. */
|
|
53
70
|
export declare class DocumentPageLimitError extends Error {
|
|
54
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* The pages of the documents up to and including the one that exceeded the limit. That document's
|
|
73
|
+
* pages are counted but not rendered, except a one-page document that came after the request's
|
|
74
|
+
* pages were used up, which rendered and was then refused. Documents after it are neither counted
|
|
75
|
+
* nor rendered.
|
|
76
|
+
*/
|
|
55
77
|
readonly renderedPages: number;
|
|
56
78
|
readonly maxPages: number;
|
|
57
|
-
|
|
79
|
+
/** Zero-based position, in the request's documents, of the document that exceeded the limit. */
|
|
80
|
+
readonly documentIndex: number;
|
|
81
|
+
constructor(details: IDocumentPageLimitErrorDetails, options?: ErrorOptions);
|
|
58
82
|
}
|
|
59
83
|
/** The model-based PDF OCR recipe formerly provided by SmartOCR. */
|
|
60
84
|
export declare function extractTextFromPdf(pdfDocument: Uint8Array, options: IPdfTextExtractionOptions): Promise<string>;
|
|
@@ -14,21 +14,29 @@ function ensureSmartpdf() {
|
|
|
14
14
|
}
|
|
15
15
|
return smartpdfStart;
|
|
16
16
|
}
|
|
17
|
-
/** A request whose documents
|
|
17
|
+
/** A request whose documents have more pages than its `maxPages`; the model was not called. */
|
|
18
18
|
export class DocumentPageLimitError extends Error {
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The pages of the documents up to and including the one that exceeded the limit. That document's
|
|
21
|
+
* pages are counted but not rendered, except a one-page document that came after the request's
|
|
22
|
+
* pages were used up, which rendered and was then refused. Documents after it are neither counted
|
|
23
|
+
* nor rendered.
|
|
24
|
+
*/
|
|
20
25
|
renderedPages;
|
|
21
26
|
maxPages;
|
|
22
|
-
|
|
27
|
+
/** Zero-based position, in the request's documents, of the document that exceeded the limit. */
|
|
28
|
+
documentIndex;
|
|
29
|
+
constructor(details, options) {
|
|
23
30
|
const subject = details.documentCount === 1
|
|
24
31
|
? `The document has ${details.renderedPages} pages`
|
|
25
32
|
: details.allDocumentsRendered
|
|
26
33
|
? `The documents have ${details.renderedPages} pages`
|
|
27
34
|
: `The documents have at least ${details.renderedPages} pages`;
|
|
28
|
-
super(`${subject}, more than the ${details.maxPages} allowed
|
|
35
|
+
super(`${subject}, more than the ${details.maxPages} allowed.`, options);
|
|
29
36
|
this.name = 'DocumentPageLimitError';
|
|
30
37
|
this.renderedPages = details.renderedPages;
|
|
31
38
|
this.maxPages = details.maxPages;
|
|
39
|
+
this.documentIndex = details.documentIndex;
|
|
32
40
|
}
|
|
33
41
|
}
|
|
34
42
|
const DEFAULT_OCR_SYSTEM_MESSAGE = 'You are a precise OCR engine. Extract text faithfully and do not summarize.';
|
|
@@ -48,6 +56,7 @@ export async function extractTextFromPdfWithUsage(pdfDocument, options) {
|
|
|
48
56
|
providerOptions: options.providerOptions,
|
|
49
57
|
abortSignal: options.abortSignal,
|
|
50
58
|
maxPages: options.maxPages,
|
|
59
|
+
onUsage: options.onUsage,
|
|
51
60
|
});
|
|
52
61
|
}
|
|
53
62
|
export async function analyzeDocuments(options) {
|
|
@@ -65,40 +74,92 @@ export async function analyzeDocumentsWithUsage(options) {
|
|
|
65
74
|
for (const [index, doc] of options.pdfDocuments.entries()) {
|
|
66
75
|
const pdf = await ensureSmartpdf();
|
|
67
76
|
abortSignal?.throwIfAborted();
|
|
68
|
-
const
|
|
77
|
+
const limitDetails = (renderedPages, limit) => ({
|
|
78
|
+
renderedPages,
|
|
79
|
+
maxPages: limit,
|
|
80
|
+
documentCount: options.pdfDocuments.length,
|
|
81
|
+
documentIndex: index,
|
|
82
|
+
allDocumentsRendered: index === options.pdfDocuments.length - 1,
|
|
83
|
+
});
|
|
84
|
+
// SmartPDF limits one document, so each render gets the pages the request has left. It takes
|
|
85
|
+
// no limit of zero: when the earlier documents used up the request's pages, the smallest limit
|
|
86
|
+
// lets the document's page count be known, and a document of one page renders and is refused below.
|
|
87
|
+
const renderMaxPages = maxPages === undefined ? undefined : Math.max(maxPages - imagePages.length, 1);
|
|
88
|
+
let pages;
|
|
89
|
+
try {
|
|
90
|
+
pages = await pdf.convertPDFToPngBytes(doc, { signal: abortSignal, maxPages: renderMaxPages });
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (maxPages !== undefined && error instanceof plugins.smartpdf.SmartPdfPageLimitError) {
|
|
94
|
+
throw new DocumentPageLimitError(limitDetails(imagePages.length + error.pageCount, maxPages), { cause: error });
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
69
98
|
abortSignal?.throwIfAborted();
|
|
70
99
|
imagePages.push(...pages);
|
|
71
100
|
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
|
-
});
|
|
101
|
+
throw new DocumentPageLimitError(limitDetails(imagePages.length, maxPages));
|
|
78
102
|
}
|
|
79
103
|
}
|
|
80
104
|
// Filter out empty buffers
|
|
81
105
|
const validPages = imagePages.filter(page => page && page.length > 0);
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
106
|
+
const onUsage = options.onUsage;
|
|
107
|
+
const modelCalls = onUsage ? new plugins.AgentModelCallUsageRecorder(options.model, onUsage) : null;
|
|
108
|
+
// Each attempt, retries included, is one model call: it starts in the middleware, which also
|
|
109
|
+
// settles an attempt that throws; a call whose response arrives ends in onLanguageModelCallEnd.
|
|
110
|
+
const model = modelCalls
|
|
111
|
+
? plugins.wrapLanguageModel({
|
|
112
|
+
model: options.model,
|
|
113
|
+
middleware: {
|
|
114
|
+
wrapGenerate: async ({ doGenerate, params }) => {
|
|
115
|
+
modelCalls.start();
|
|
116
|
+
try {
|
|
117
|
+
return await doGenerate();
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
modelCalls.settle(params.abortSignal?.aborted ? 'aborted' : 'failed');
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
97
124
|
},
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
125
|
+
})
|
|
126
|
+
: options.model;
|
|
127
|
+
const messages = [
|
|
128
|
+
...(options.messageHistory ?? []),
|
|
129
|
+
{
|
|
130
|
+
role: 'user',
|
|
131
|
+
content: [
|
|
132
|
+
{ type: 'text', text: options.userMessage },
|
|
133
|
+
...validPages.map(page => ({
|
|
134
|
+
type: 'file',
|
|
135
|
+
data: page,
|
|
136
|
+
mediaType: 'image/png',
|
|
137
|
+
})),
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
let result;
|
|
142
|
+
try {
|
|
143
|
+
result = await plugins.generateText({
|
|
144
|
+
model,
|
|
145
|
+
instructions: options.systemMessage,
|
|
146
|
+
messages,
|
|
147
|
+
providerOptions: options.providerOptions,
|
|
148
|
+
abortSignal,
|
|
149
|
+
...(modelCalls
|
|
150
|
+
? {
|
|
151
|
+
onLanguageModelCallEnd: ({ modelId, usage: callUsage }) => {
|
|
152
|
+
modelCalls.end(modelId, callUsage);
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
: {}),
|
|
156
|
+
});
|
|
157
|
+
modelCalls?.settle('missing');
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
modelCalls?.settle(abortSignal?.aborted ? 'aborted' : 'failed');
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
102
163
|
const usage = result.totalUsage;
|
|
103
164
|
const inputTokens = usage.inputTokens ?? 0;
|
|
104
165
|
const outputTokens = usage.outputTokens ?? 0;
|
|
@@ -132,4 +193,4 @@ export async function stopSmartpdf() {
|
|
|
132
193
|
}
|
|
133
194
|
await instance.stop();
|
|
134
195
|
}
|
|
135
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
196
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19kb2N1bWVudC9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQU14QywrR0FBK0c7QUFDL0csSUFBSSxhQUFhLEdBQThCLElBQUksQ0FBQztBQUVwRCxTQUFTLGNBQWM7SUFDckIsSUFBSSxDQUFDLGFBQWEsRUFBRSxDQUFDO1FBQ25CLE1BQU0sUUFBUSxHQUFHLElBQUksT0FBTyxDQUFDLFFBQVEsQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUNqRCxNQUFNLEtBQUssR0FBRyxRQUFRLENBQUMsS0FBSyxFQUFFLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQ3BELGFBQWEsR0FBRyxLQUFLLENBQUM7UUFDdEIsS0FBSyxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUU7WUFDZixJQUFJLGFBQWEsS0FBSyxLQUFLLEVBQUUsQ0FBQztnQkFDNUIsYUFBYSxHQUFHLElBQUksQ0FBQztZQUN2QixDQUFDO1FBQ0gsQ0FBQyxDQUFDLENBQUM7SUFDTCxDQUFDO0lBQ0QsT0FBTyxhQUFhLENBQUM7QUFDdkIsQ0FBQztBQXlFRCwrRkFBK0Y7QUFDL0YsTUFBTSxPQUFPLHNCQUF1QixTQUFRLEtBQUs7SUFDL0M7Ozs7O09BS0c7SUFDYSxhQUFhLENBQVM7SUFDdEIsUUFBUSxDQUFTO0lBQ2pDLGdHQUFnRztJQUNoRixhQUFhLENBQVM7SUFFdEMsWUFBWSxPQUF1QyxFQUFFLE9BQXNCO1FBQ3pFLE1BQU0sT0FBTyxHQUFHLE9BQU8sQ0FBQyxhQUFhLEtBQUssQ0FBQztZQUN6QyxDQUFDLENBQUMsb0JBQW9CLE9BQU8sQ0FBQyxhQUFhLFFBQVE7WUFDbkQsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxvQkFBb0I7Z0JBQzVCLENBQUMsQ0FBQyxzQkFBc0IsT0FBTyxDQUFDLGFBQWEsUUFBUTtnQkFDckQs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
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import { generateText } from 'ai';
|
|
1
|
+
import { generateText, wrapLanguageModel } from 'ai';
|
|
2
2
|
import * as smartpdf from '@push.rocks/smartpdf';
|
|
3
|
-
|
|
3
|
+
import { AgentModelCallUsageRecorder } from '@modelprofile.com/flexharness-agent';
|
|
4
|
+
export { generateText, wrapLanguageModel, smartpdf, AgentModelCallUsageRecorder };
|
|
5
|
+
export type { TAgentModelCallUsageReporter } from '@modelprofile.com/flexharness-agent';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { generateText } from 'ai';
|
|
1
|
+
import { generateText, wrapLanguageModel } from 'ai';
|
|
2
2
|
import * as smartpdf from '@push.rocks/smartpdf';
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { AgentModelCallUsageRecorder } from '@modelprofile.com/flexharness-agent';
|
|
4
|
+
export { generateText, wrapLanguageModel, smartpdf, AgentModelCallUsageRecorder };
|
|
5
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX2RvY3VtZW50L3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFlBQVksRUFBRSxpQkFBaUIsRUFBRSxNQUFNLElBQUksQ0FBQztBQUNyRCxPQUFPLEtBQUssUUFBUSxNQUFNLHNCQUFzQixDQUFDO0FBQ2pELE9BQU8sRUFBRSwyQkFBMkIsRUFBRSxNQUFNLHFDQUFxQyxDQUFDO0FBQ2xGLE9BQU8sRUFBRSxZQUFZLEVBQUUsaUJBQWlCLEVBQUUsUUFBUSxFQUFFLDJCQUEyQixFQUFFLENBQUMifQ==
|
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": "
|
|
5
|
+
"version": "9.0.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,15 +62,15 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@modelprofile.com/flexharness-agent": "
|
|
66
|
-
"@modelprofile.com/flexharness-models": "
|
|
65
|
+
"@modelprofile.com/flexharness-agent": "9.0.0",
|
|
66
|
+
"@modelprofile.com/flexharness-models": "9.0.0",
|
|
67
67
|
"@push.rocks/smartrequest": "^5.0.1",
|
|
68
68
|
"ai": "^7.0.79",
|
|
69
69
|
"zod": "^4.4.1"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
72
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
73
|
-
"@push.rocks/smartpdf": "^
|
|
73
|
+
"@push.rocks/smartpdf": "^6.0.0",
|
|
74
74
|
"puppeteer": "^24.42.0",
|
|
75
75
|
"ink": "^6.8.0",
|
|
76
76
|
"ink-text-input": "^6.0.0",
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts_document/index.ts
CHANGED
|
@@ -26,16 +26,28 @@ export interface IDocumentRequestOptions {
|
|
|
26
26
|
/** Passed to the model call unchanged, for example `{ openai: { store: false } }`. */
|
|
27
27
|
providerOptions?: TSmartAiProviderOptions;
|
|
28
28
|
/**
|
|
29
|
-
* Checked before each document is rendered and before the model call, and passed to the
|
|
30
|
-
* call
|
|
29
|
+
* Checked before each document is rendered and before the model call, and passed to both the
|
|
30
|
+
* render and the model call: an abort stops a document that is rendering. The request rejects
|
|
31
|
+
* with the signal's reason.
|
|
31
32
|
*/
|
|
32
33
|
abortSignal?: AbortSignal;
|
|
33
34
|
/**
|
|
34
|
-
* The most rendered pages one request may send to the model
|
|
35
|
-
* with `DocumentPageLimitError` before the model is called
|
|
36
|
-
*
|
|
35
|
+
* The most rendered pages one request may send to the model, counted over all its documents. A
|
|
36
|
+
* request with more pages rejects with `DocumentPageLimitError` before the model is called. Each
|
|
37
|
+
* document is rendered only when its page count fits the pages the request has left, so the
|
|
38
|
+
* document that exceeds the limit and every document after it are not rendered. The exception is
|
|
39
|
+
* a one-page document that comes after the request's pages are used up: it renders and is then
|
|
40
|
+
* refused.
|
|
37
41
|
*/
|
|
38
42
|
maxPages?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Called once for every model call of the request, as soon as its usage is known, whatever the
|
|
45
|
+
* request's outcome: `reported` with the provider's usage, or `unreported` with the reason when
|
|
46
|
+
* the call failed, was aborted or its response carried no usage. A retried attempt is a call of
|
|
47
|
+
* its own. Nothing is reported for a request that ends before its model is called. When a request
|
|
48
|
+
* returns, its reported calls sum to its `usage`. Must not throw.
|
|
49
|
+
*/
|
|
50
|
+
onUsage?: plugins.TAgentModelCallUsageReporter;
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
export interface IDocumentOptions extends IDocumentRequestOptions {
|
|
@@ -70,28 +82,40 @@ export interface IDocumentAnalysisResult {
|
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
export interface IDocumentPageLimitErrorDetails {
|
|
85
|
+
/** The pages of the documents up to and including the one that exceeded the limit. */
|
|
73
86
|
renderedPages: number;
|
|
74
87
|
maxPages: number;
|
|
75
88
|
documentCount: number;
|
|
89
|
+
/** Zero-based position, in the request's documents, of the document that exceeded the limit. */
|
|
90
|
+
documentIndex: number;
|
|
91
|
+
/** Whether the pages of every document of the request were counted. */
|
|
76
92
|
allDocumentsRendered: boolean;
|
|
77
93
|
}
|
|
78
94
|
|
|
79
|
-
/** A request whose documents
|
|
95
|
+
/** A request whose documents have more pages than its `maxPages`; the model was not called. */
|
|
80
96
|
export class DocumentPageLimitError extends Error {
|
|
81
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* The pages of the documents up to and including the one that exceeded the limit. That document's
|
|
99
|
+
* pages are counted but not rendered, except a one-page document that came after the request's
|
|
100
|
+
* pages were used up, which rendered and was then refused. Documents after it are neither counted
|
|
101
|
+
* nor rendered.
|
|
102
|
+
*/
|
|
82
103
|
public readonly renderedPages: number;
|
|
83
104
|
public readonly maxPages: number;
|
|
105
|
+
/** Zero-based position, in the request's documents, of the document that exceeded the limit. */
|
|
106
|
+
public readonly documentIndex: number;
|
|
84
107
|
|
|
85
|
-
constructor(details: IDocumentPageLimitErrorDetails) {
|
|
108
|
+
constructor(details: IDocumentPageLimitErrorDetails, options?: ErrorOptions) {
|
|
86
109
|
const subject = details.documentCount === 1
|
|
87
110
|
? `The document has ${details.renderedPages} pages`
|
|
88
111
|
: details.allDocumentsRendered
|
|
89
112
|
? `The documents have ${details.renderedPages} pages`
|
|
90
113
|
: `The documents have at least ${details.renderedPages} pages`;
|
|
91
|
-
super(`${subject}, more than the ${details.maxPages} allowed
|
|
114
|
+
super(`${subject}, more than the ${details.maxPages} allowed.`, options);
|
|
92
115
|
this.name = 'DocumentPageLimitError';
|
|
93
116
|
this.renderedPages = details.renderedPages;
|
|
94
117
|
this.maxPages = details.maxPages;
|
|
118
|
+
this.documentIndex = details.documentIndex;
|
|
95
119
|
}
|
|
96
120
|
}
|
|
97
121
|
|
|
@@ -120,6 +144,7 @@ export async function extractTextFromPdfWithUsage(
|
|
|
120
144
|
providerOptions: options.providerOptions,
|
|
121
145
|
abortSignal: options.abortSignal,
|
|
122
146
|
maxPages: options.maxPages,
|
|
147
|
+
onUsage: options.onUsage,
|
|
123
148
|
});
|
|
124
149
|
}
|
|
125
150
|
|
|
@@ -140,42 +165,91 @@ export async function analyzeDocumentsWithUsage(options: IDocumentOptions): Prom
|
|
|
140
165
|
for (const [index, doc] of options.pdfDocuments.entries()) {
|
|
141
166
|
const pdf = await ensureSmartpdf();
|
|
142
167
|
abortSignal?.throwIfAborted();
|
|
143
|
-
const
|
|
168
|
+
const limitDetails = (renderedPages: number, limit: number): IDocumentPageLimitErrorDetails => ({
|
|
169
|
+
renderedPages,
|
|
170
|
+
maxPages: limit,
|
|
171
|
+
documentCount: options.pdfDocuments.length,
|
|
172
|
+
documentIndex: index,
|
|
173
|
+
allDocumentsRendered: index === options.pdfDocuments.length - 1,
|
|
174
|
+
});
|
|
175
|
+
// SmartPDF limits one document, so each render gets the pages the request has left. It takes
|
|
176
|
+
// no limit of zero: when the earlier documents used up the request's pages, the smallest limit
|
|
177
|
+
// lets the document's page count be known, and a document of one page renders and is refused below.
|
|
178
|
+
const renderMaxPages = maxPages === undefined ? undefined : Math.max(maxPages - imagePages.length, 1);
|
|
179
|
+
let pages: Uint8Array[];
|
|
180
|
+
try {
|
|
181
|
+
pages = await pdf.convertPDFToPngBytes(doc, { signal: abortSignal, maxPages: renderMaxPages });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (maxPages !== undefined && error instanceof plugins.smartpdf.SmartPdfPageLimitError) {
|
|
184
|
+
throw new DocumentPageLimitError(limitDetails(imagePages.length + error.pageCount, maxPages), { cause: error });
|
|
185
|
+
}
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
144
188
|
abortSignal?.throwIfAborted();
|
|
145
189
|
imagePages.push(...pages);
|
|
146
190
|
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
|
-
});
|
|
191
|
+
throw new DocumentPageLimitError(limitDetails(imagePages.length, maxPages));
|
|
153
192
|
}
|
|
154
193
|
}
|
|
155
194
|
|
|
156
195
|
// Filter out empty buffers
|
|
157
196
|
const validPages = imagePages.filter(page => page && page.length > 0);
|
|
158
197
|
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
{
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
198
|
+
const onUsage = options.onUsage;
|
|
199
|
+
const modelCalls = onUsage ? new plugins.AgentModelCallUsageRecorder(options.model, onUsage) : null;
|
|
200
|
+
// Each attempt, retries included, is one model call: it starts in the middleware, which also
|
|
201
|
+
// settles an attempt that throws; a call whose response arrives ends in onLanguageModelCallEnd.
|
|
202
|
+
const model = modelCalls
|
|
203
|
+
? plugins.wrapLanguageModel({
|
|
204
|
+
model: options.model,
|
|
205
|
+
middleware: {
|
|
206
|
+
wrapGenerate: async ({ doGenerate, params }) => {
|
|
207
|
+
modelCalls.start();
|
|
208
|
+
try {
|
|
209
|
+
return await doGenerate();
|
|
210
|
+
} catch (error) {
|
|
211
|
+
modelCalls.settle(params.abortSignal?.aborted ? 'aborted' : 'failed');
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
: options.model;
|
|
218
|
+
const messages: ModelMessage[] = [
|
|
219
|
+
...(options.messageHistory ?? []),
|
|
220
|
+
{
|
|
221
|
+
role: 'user',
|
|
222
|
+
content: [
|
|
223
|
+
{ type: 'text', text: options.userMessage },
|
|
224
|
+
...validPages.map(page => ({
|
|
225
|
+
type: 'file' as const,
|
|
226
|
+
data: page,
|
|
227
|
+
mediaType: 'image/png' as const,
|
|
228
|
+
})),
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
let result: Awaited<ReturnType<typeof plugins.generateText>>;
|
|
233
|
+
try {
|
|
234
|
+
result = await plugins.generateText({
|
|
235
|
+
model,
|
|
236
|
+
instructions: options.systemMessage,
|
|
237
|
+
messages,
|
|
238
|
+
providerOptions: options.providerOptions,
|
|
239
|
+
abortSignal,
|
|
240
|
+
...(modelCalls
|
|
241
|
+
? {
|
|
242
|
+
onLanguageModelCallEnd: ({ modelId, usage: callUsage }) => {
|
|
243
|
+
modelCalls.end(modelId, callUsage);
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
: {}),
|
|
247
|
+
});
|
|
248
|
+
modelCalls?.settle('missing');
|
|
249
|
+
} catch (error) {
|
|
250
|
+
modelCalls?.settle(abortSignal?.aborted ? 'aborted' : 'failed');
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
179
253
|
const usage = result.totalUsage;
|
|
180
254
|
const inputTokens = usage.inputTokens ?? 0;
|
|
181
255
|
const outputTokens = usage.outputTokens ?? 0;
|
package/ts_document/plugins.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import { generateText } from 'ai';
|
|
1
|
+
import { generateText, wrapLanguageModel } from 'ai';
|
|
2
2
|
import * as smartpdf from '@push.rocks/smartpdf';
|
|
3
|
-
|
|
3
|
+
import { AgentModelCallUsageRecorder } from '@modelprofile.com/flexharness-agent';
|
|
4
|
+
export { generateText, wrapLanguageModel, smartpdf, AgentModelCallUsageRecorder };
|
|
5
|
+
export type { TAgentModelCallUsageReporter } from '@modelprofile.com/flexharness-agent';
|
package/ts_document/readme.md
CHANGED
|
@@ -23,11 +23,12 @@ 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
|
|
26
|
+
Both functions accept four request options:
|
|
27
27
|
|
|
28
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
|
|
30
|
-
- `maxPages` is the most rendered pages one request may send to the model. A request with more pages rejects with `DocumentPageLimitError`
|
|
29
|
+
- `abortSignal` is checked before each document is rendered and before the model call, and is passed to both the render and the model call, so an abort also stops a document that is rendering. The request rejects with the signal's reason.
|
|
30
|
+
- `maxPages` is the most rendered pages one request may send to the model, counted over all its documents. A request with more pages rejects with `DocumentPageLimitError` before the model is called. Each document is rendered only when its page count fits the pages the request has left, so the document that exceeds the limit and every document after it are not rendered; SmartPDF counts that document's pages without rendering them. The exception is a one-page document that comes after the request's pages are used up: SmartPDF takes no limit of zero pages, so that document renders and is then refused. The error carries `renderedPages` (the pages of the documents up to and including the one that exceeded the limit), `maxPages` and `documentIndex` (the zero-based position of that document), and SmartPDF's `SmartPdfPageLimitError` as its `cause` when SmartPDF refused the document.
|
|
31
|
+
- `onUsage` receives the usage of each model call of the request, also of a request that fails or is aborted; see [Usage of a request that fails or is aborted](#usage-of-a-request-that-fails-or-is-aborted).
|
|
31
32
|
|
|
32
33
|
`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
|
|
|
@@ -49,6 +50,28 @@ try {
|
|
|
49
50
|
}
|
|
50
51
|
```
|
|
51
52
|
|
|
53
|
+
### Usage of a request that fails or is aborted
|
|
54
|
+
|
|
55
|
+
A request that throws returns no `usage`. `onUsage` reports each model call of a request once, as soon as its usage is known, whatever the outcome, with the events of `AgentModelCallUsageRecorder` (the type is `TFlexModelCallUsage` from `@modelprofile.com/flexharness`):
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import type { TFlexModelCallUsage } from '@modelprofile.com/flexharness';
|
|
59
|
+
|
|
60
|
+
const calls: TFlexModelCallUsage[] = [];
|
|
61
|
+
try {
|
|
62
|
+
await extractTextFromPdfWithUsage(pdfBytes, { model, abortSignal, onUsage: (call) => calls.push(call) });
|
|
63
|
+
} finally {
|
|
64
|
+
const reported = calls.filter((call) => call.status === 'reported');
|
|
65
|
+
const complete = reported.length === calls.length; // an unreported call used an unknown number of tokens
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `status: 'reported'`: the provider reported the call's `usage`; `requestedModelId` is the model's id and `responseModelId` the id the response named.
|
|
70
|
+
- `status: 'unreported'`: the call `failed`, was `aborted`, or its response carried no usage (`missing`). The provider may still have consumed tokens for it.
|
|
71
|
+
- Every attempt is a call of its own: a call retried after a failure reports the failed attempt and the retry.
|
|
72
|
+
- A request that ends before its model is called (an abort during rendering, `DocumentPageLimitError`) reports nothing.
|
|
73
|
+
- When a request returns, its reported calls sum to its `usage`; count one or the other. `onUsage` must not throw.
|
|
74
|
+
|
|
52
75
|
## Issue Reporting and Security
|
|
53
76
|
|
|
54
77
|
Report issues and security concerns through [community.foss.global](https://community.foss.global).
|