@docmana/sdk 0.3.5 → 0.4.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/README.md +115 -247
- package/dist/api/execution-result.d.ts +3 -0
- package/dist/api/execution-status.d.ts +6 -0
- package/dist/api/run-flow.d.ts +6 -0
- package/dist/cli.mjs +217 -257
- package/dist/cli.mjs.map +1 -1
- package/dist/client.d.ts +21 -0
- package/dist/config.d.ts +21 -0
- package/dist/errors.d.ts +55 -0
- package/dist/files/resolve-input.d.ts +11 -0
- package/dist/http/http-client.d.ts +26 -0
- package/dist/index.d.ts +4 -349
- package/dist/index.js +159 -298
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +159 -298
- package/dist/index.mjs.map +1 -1
- package/dist/models/docmana-api-document-execution-result.model.d.ts +9 -0
- package/dist/models/docmana-api-document-request.model.d.ts +15 -0
- package/dist/models/docmana-api-execution-result.model.d.ts +24 -0
- package/dist/models/docmana-api-node-result.model.d.ts +28 -0
- package/dist/models/docmana-classification-result.model.d.ts +3 -0
- package/dist/models/docmana-conclusion-result.model.d.ts +3 -0
- package/dist/models/docmana-cross-validation-result.model.d.ts +3 -0
- package/dist/models/docmana-document-mapping.model.d.ts +4 -0
- package/dist/models/docmana-document-result.model.d.ts +11 -0
- package/dist/models/docmana-execution-result.model.d.ts +15 -0
- package/dist/models/docmana-extraction-result.model.d.ts +3 -0
- package/dist/models/docmana-metadata-extraction-result.model.d.ts +3 -0
- package/dist/models/docmana-node-result.model.d.ts +7 -0
- package/dist/models/docmana-score-node-result.model.d.ts +5 -0
- package/dist/models/docmana-validation-result.model.d.ts +3 -0
- package/dist/models/document-content.model.d.ts +4 -0
- package/dist/models/execution-progress.model.d.ts +4 -0
- package/dist/models/execution-status.enum.d.ts +1 -0
- package/dist/models/flow-configs.model.d.ts +16 -0
- package/dist/models/flow-edge.model.d.ts +4 -0
- package/dist/models/flow-node-type.enum.d.ts +1 -0
- package/dist/models/flow-node.model.d.ts +33 -0
- package/dist/models/flow.model.d.ts +11 -0
- package/dist/models/index.d.ts +26 -0
- package/dist/models/node-translation-result.model.d.ts +7 -0
- package/dist/models/physical-document.model.d.ts +9 -0
- package/dist/models/virtual-document.model.d.ts +10 -0
- package/dist/polling/poll.d.ts +15 -0
- package/dist/types.d.ts +76 -0
- package/package.json +17 -14
- package/dist/index.d.cts +0 -349
package/dist/cli.mjs
CHANGED
|
@@ -61,7 +61,14 @@ var DocmanaAbortError = class extends DocmanaError {
|
|
|
61
61
|
super(message, { code: "aborted" });
|
|
62
62
|
}
|
|
63
63
|
};
|
|
64
|
-
|
|
64
|
+
var DocmanaRateLimitError = class extends DocmanaError {
|
|
65
|
+
retryAfterMs;
|
|
66
|
+
constructor(message, opts = {}) {
|
|
67
|
+
super(message, { ...opts, code: "rate_limited" });
|
|
68
|
+
this.retryAfterMs = opts.retryAfterMs;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
function errorFromResponse(status, message, requestId, retryAfterMs) {
|
|
65
72
|
const opts = { status, requestId };
|
|
66
73
|
switch (status) {
|
|
67
74
|
case 400:
|
|
@@ -72,6 +79,8 @@ function errorFromResponse(status, message, requestId) {
|
|
|
72
79
|
return new DocmanaPermissionError(message, opts);
|
|
73
80
|
case 404:
|
|
74
81
|
return new DocmanaNotFoundError(message, opts);
|
|
82
|
+
case 429:
|
|
83
|
+
return new DocmanaRateLimitError(message, { ...opts, retryAfterMs });
|
|
75
84
|
default:
|
|
76
85
|
return new DocmanaError(message, opts);
|
|
77
86
|
}
|
|
@@ -159,29 +168,33 @@ var TokenManager = class {
|
|
|
159
168
|
|
|
160
169
|
// src/config.ts
|
|
161
170
|
var DEFAULTS = {
|
|
162
|
-
apiBaseUrl: "https://api.docmana.ai",
|
|
163
171
|
tokenEndpoint: "https://4fe70f5b-e013-4f65-9fa7-3109a33beba5.ciamlogin.com/4fe70f5b-e013-4f65-9fa7-3109a33beba5/oauth2/v2.0/token",
|
|
164
172
|
scope: "api://d781e6ba-cc08-4618-8099-ad968abd2b9e/.default",
|
|
165
173
|
timeoutMs: 3e5,
|
|
166
|
-
pollIntervalMs: 2e3
|
|
174
|
+
pollIntervalMs: 2e3,
|
|
175
|
+
pollMaxIntervalMs: 1e4
|
|
167
176
|
};
|
|
168
177
|
function resolveConfig(config) {
|
|
169
|
-
if (!config.
|
|
170
|
-
if (!config.
|
|
171
|
-
|
|
178
|
+
if (!config.apiBaseUrl) throw new Error("DocmanaConfig.apiBaseUrl is required");
|
|
179
|
+
if (!config.accessToken && !config.getAccessToken) {
|
|
180
|
+
throw new Error("DocmanaConfig.accessToken or DocmanaConfig.getAccessToken is required");
|
|
181
|
+
}
|
|
182
|
+
const apiBaseUrl = normalizeApiBaseUrl(config.apiBaseUrl);
|
|
172
183
|
return {
|
|
173
|
-
clientId: config.clientId,
|
|
174
|
-
clientSecret: config.clientSecret,
|
|
175
184
|
apiBaseUrl,
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
accessToken: config.accessToken,
|
|
186
|
+
getAccessToken: config.getAccessToken,
|
|
178
187
|
timeoutMs: config.timeoutMs ?? DEFAULTS.timeoutMs,
|
|
179
188
|
pollIntervalMs: config.pollIntervalMs ?? DEFAULTS.pollIntervalMs,
|
|
189
|
+
pollMaxIntervalMs: config.pollMaxIntervalMs ?? DEFAULTS.pollMaxIntervalMs,
|
|
180
190
|
fetchImpl: config.fetch ?? fetch,
|
|
181
|
-
headers: config.headers ?? {}
|
|
182
|
-
tokenCache: config.tokenCache
|
|
191
|
+
headers: config.headers ?? {}
|
|
183
192
|
};
|
|
184
193
|
}
|
|
194
|
+
function normalizeApiBaseUrl(value) {
|
|
195
|
+
const trimmed = value.replace(/\/+$/, "");
|
|
196
|
+
return /\/v\d+$/i.test(trimmed) ? trimmed : `${trimmed}/v1`;
|
|
197
|
+
}
|
|
185
198
|
|
|
186
199
|
// src/http/http-client.ts
|
|
187
200
|
var HttpClient = class {
|
|
@@ -201,19 +214,23 @@ var HttpClient = class {
|
|
|
201
214
|
}
|
|
202
215
|
async requestRaw(method, path, options = {}) {
|
|
203
216
|
let res = await this.send(method, path, options);
|
|
204
|
-
if (res.status === 401) {
|
|
205
|
-
this.
|
|
206
|
-
res = await this.send(method, path, options);
|
|
217
|
+
if (res.status === 401 && this.opts.getAccessToken) {
|
|
218
|
+
res = await this.send(method, path, options, true);
|
|
207
219
|
}
|
|
208
220
|
if (!res.ok) {
|
|
209
221
|
const requestId = res.headers.get("x-request-id") ?? void 0;
|
|
210
222
|
const message = await this.extractMessage(res);
|
|
211
|
-
throw errorFromResponse(
|
|
223
|
+
throw errorFromResponse(
|
|
224
|
+
res.status,
|
|
225
|
+
message,
|
|
226
|
+
requestId,
|
|
227
|
+
parseRetryAfterMs(res.headers.get("retry-after"))
|
|
228
|
+
);
|
|
212
229
|
}
|
|
213
230
|
return res;
|
|
214
231
|
}
|
|
215
|
-
async send(method, path, options) {
|
|
216
|
-
const token = await this.
|
|
232
|
+
async send(method, path, options, forceRefresh = false) {
|
|
233
|
+
const token = await this.resolveToken(forceRefresh);
|
|
217
234
|
const url = new URL(this.opts.apiBaseUrl + path);
|
|
218
235
|
for (const [k, v] of Object.entries(options.query ?? {})) url.searchParams.set(k, v);
|
|
219
236
|
const headers = {
|
|
@@ -235,6 +252,13 @@ var HttpClient = class {
|
|
|
235
252
|
throw err;
|
|
236
253
|
}
|
|
237
254
|
}
|
|
255
|
+
async resolveToken(forceRefresh) {
|
|
256
|
+
const token = this.opts.getAccessToken ? await this.opts.getAccessToken(forceRefresh ? { forceRefresh: true } : void 0) : this.opts.accessToken;
|
|
257
|
+
if (!token) {
|
|
258
|
+
throw new DocmanaError("Missing Docmana access token", { code: "auth_missing" });
|
|
259
|
+
}
|
|
260
|
+
return token;
|
|
261
|
+
}
|
|
238
262
|
async extractMessage(res) {
|
|
239
263
|
try {
|
|
240
264
|
const data = await res.clone().json();
|
|
@@ -244,6 +268,18 @@ var HttpClient = class {
|
|
|
244
268
|
}
|
|
245
269
|
}
|
|
246
270
|
};
|
|
271
|
+
function parseRetryAfterMs(value) {
|
|
272
|
+
if (!value) return void 0;
|
|
273
|
+
const seconds = Number(value);
|
|
274
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
275
|
+
return seconds * 1e3;
|
|
276
|
+
}
|
|
277
|
+
const retryAt = Date.parse(value);
|
|
278
|
+
if (!Number.isNaN(retryAt)) {
|
|
279
|
+
return Math.max(0, retryAt - Date.now());
|
|
280
|
+
}
|
|
281
|
+
return void 0;
|
|
282
|
+
}
|
|
247
283
|
|
|
248
284
|
// src/files/resolve-input.ts
|
|
249
285
|
import { readFile } from "fs/promises";
|
|
@@ -275,16 +311,19 @@ async function streamToBuffer(stream) {
|
|
|
275
311
|
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
|
|
276
312
|
return Buffer.concat(chunks);
|
|
277
313
|
}
|
|
314
|
+
function toBlobBytes(bytes) {
|
|
315
|
+
return new Uint8Array(bytes);
|
|
316
|
+
}
|
|
278
317
|
async function resolveOne(input, fallbackName) {
|
|
279
318
|
if (typeof input === "string" || isPathInput(input)) {
|
|
280
319
|
const path = typeof input === "string" ? input : input.path;
|
|
281
320
|
const data = await readFile(path);
|
|
282
321
|
const filename = basename(path);
|
|
283
|
-
return { data: new Blob([data]), filename, contentType: contentTypeFor(filename) };
|
|
322
|
+
return { data: new Blob([toBlobBytes(data)]), filename, contentType: contentTypeFor(filename) };
|
|
284
323
|
}
|
|
285
324
|
if (input instanceof Uint8Array) {
|
|
286
325
|
return {
|
|
287
|
-
data: new Blob([input]),
|
|
326
|
+
data: new Blob([toBlobBytes(input)]),
|
|
288
327
|
filename: fallbackName,
|
|
289
328
|
contentType: contentTypeFor(fallbackName)
|
|
290
329
|
};
|
|
@@ -292,7 +331,7 @@ async function resolveOne(input, fallbackName) {
|
|
|
292
331
|
if (isReadable(input)) {
|
|
293
332
|
const buf = await streamToBuffer(input);
|
|
294
333
|
return {
|
|
295
|
-
data: new Blob([buf]),
|
|
334
|
+
data: new Blob([toBlobBytes(buf)]),
|
|
296
335
|
filename: fallbackName,
|
|
297
336
|
contentType: contentTypeFor(fallbackName)
|
|
298
337
|
};
|
|
@@ -306,39 +345,43 @@ async function resolveInputs(input) {
|
|
|
306
345
|
return Promise.all(list.map((item) => resolveOne(item, fallback)));
|
|
307
346
|
}
|
|
308
347
|
|
|
309
|
-
// src/api/
|
|
310
|
-
async function
|
|
348
|
+
// src/api/run-flow.ts
|
|
349
|
+
async function runFlow(http, flowId, parts, signal, useDraftVersion = false, context, callbackUrl, language) {
|
|
311
350
|
const form = new FormData();
|
|
312
351
|
for (const part of parts) {
|
|
313
352
|
form.append("files", new File([part.data], part.filename, { type: part.contentType }));
|
|
314
353
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (callbackUrl !== void 0) body.callback_url = callbackUrl;
|
|
325
|
-
const res = await http.requestJson("POST", path, {
|
|
326
|
-
query: { async_mode: "true" },
|
|
327
|
-
headers: { "content-type": "application/json" },
|
|
328
|
-
body: JSON.stringify(body),
|
|
329
|
-
signal
|
|
330
|
-
});
|
|
331
|
-
return res.execution_result_id;
|
|
354
|
+
if (context !== void 0) form.append("json_context", JSON.stringify(context));
|
|
355
|
+
if (callbackUrl !== void 0) form.append("callback_url", callbackUrl);
|
|
356
|
+
if (language !== void 0) form.append("language", language);
|
|
357
|
+
if (useDraftVersion) form.append("useDraftVersion", "true");
|
|
358
|
+
return http.requestJson(
|
|
359
|
+
"POST",
|
|
360
|
+
`/flows/${flowId}/execute-async`,
|
|
361
|
+
{ body: form, signal }
|
|
362
|
+
);
|
|
332
363
|
}
|
|
333
364
|
|
|
334
365
|
// src/api/execution-status.ts
|
|
335
366
|
async function getStatus(http, executionResultId, signal) {
|
|
336
|
-
|
|
367
|
+
const res = await http.requestRaw("GET", `/executions/${executionResultId}/status`, { signal });
|
|
368
|
+
const text = await res.text();
|
|
369
|
+
const body = text ? JSON.parse(text) : { status: "" };
|
|
370
|
+
const pollAfterMs = parsePositiveInt(res.headers.get("x-poll-after"));
|
|
371
|
+
return pollAfterMs === void 0 ? body : { ...body, pollAfterMs };
|
|
372
|
+
}
|
|
373
|
+
function parsePositiveInt(value) {
|
|
374
|
+
if (!value) return void 0;
|
|
375
|
+
const parsed = Number(value);
|
|
376
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
|
|
337
377
|
}
|
|
338
378
|
|
|
339
379
|
// src/api/execution-result.ts
|
|
340
|
-
async function getResult(http, executionResultId, signal) {
|
|
341
|
-
return http.requestJson("GET", `/
|
|
380
|
+
async function getResult(http, executionResultId, signal, language) {
|
|
381
|
+
return http.requestJson("GET", `/executions/${executionResultId}/result`, {
|
|
382
|
+
query: language ? { language } : void 0,
|
|
383
|
+
signal
|
|
384
|
+
});
|
|
342
385
|
}
|
|
343
386
|
|
|
344
387
|
// src/polling/poll.ts
|
|
@@ -348,166 +391,42 @@ async function pollUntilTerminal(opts) {
|
|
|
348
391
|
const sleep = opts.sleep ?? defaultSleep;
|
|
349
392
|
const start = Date.now();
|
|
350
393
|
const elapsed = opts.elapsed ?? (() => Date.now() - start);
|
|
394
|
+
const random = opts.random ?? Math.random;
|
|
395
|
+
let fallbackAttempt = 0;
|
|
351
396
|
for (; ; ) {
|
|
352
397
|
if (opts.signal?.aborted) throw new DocmanaAbortError("Polling aborted");
|
|
353
|
-
|
|
398
|
+
let result;
|
|
399
|
+
try {
|
|
400
|
+
result = normalizePollResult(await opts.check());
|
|
401
|
+
} catch (err) {
|
|
402
|
+
if (!(err instanceof DocmanaRateLimitError)) throw err;
|
|
403
|
+
if (elapsed() >= opts.timeoutMs) {
|
|
404
|
+
throw new DocmanaTimeoutError(`Execution did not finish within ${opts.timeoutMs}ms`);
|
|
405
|
+
}
|
|
406
|
+
await sleep(err.retryAfterMs ?? opts.intervalMs);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const { status } = result;
|
|
354
410
|
if (TERMINAL.has(status)) return status;
|
|
355
411
|
if (elapsed() >= opts.timeoutMs) {
|
|
356
412
|
throw new DocmanaTimeoutError(`Execution did not finish within ${opts.timeoutMs}ms`);
|
|
357
413
|
}
|
|
358
|
-
|
|
414
|
+
const fallbackDelay = fullJitterDelay(
|
|
415
|
+
opts.intervalMs,
|
|
416
|
+
opts.maxIntervalMs,
|
|
417
|
+
fallbackAttempt,
|
|
418
|
+
random
|
|
419
|
+
);
|
|
420
|
+
fallbackAttempt += 1;
|
|
421
|
+
await sleep(result.nextDelayMs ?? fallbackDelay);
|
|
359
422
|
}
|
|
360
423
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
function extractNodeFields(rawNode, targetLanguage) {
|
|
364
|
-
const metadataFields = /* @__PURE__ */ new Set([
|
|
365
|
-
"node_id",
|
|
366
|
-
"node_name",
|
|
367
|
-
"node_type",
|
|
368
|
-
"feature_name",
|
|
369
|
-
"running_time",
|
|
370
|
-
"status",
|
|
371
|
-
"score",
|
|
372
|
-
"feedback",
|
|
373
|
-
"translations",
|
|
374
|
-
"tokens",
|
|
375
|
-
"input_tokens",
|
|
376
|
-
"output_tokens",
|
|
377
|
-
"errors",
|
|
378
|
-
"mana",
|
|
379
|
-
"model",
|
|
380
|
-
"tool_calls",
|
|
381
|
-
"prompt_id",
|
|
382
|
-
"prompt",
|
|
383
|
-
"prompt_name",
|
|
384
|
-
"label",
|
|
385
|
-
"scoreDescription",
|
|
386
|
-
"score_description",
|
|
387
|
-
"result",
|
|
388
|
-
"documentType"
|
|
389
|
-
]);
|
|
390
|
-
let sourceObj = rawNode;
|
|
391
|
-
if (targetLanguage) {
|
|
392
|
-
const translations = rawNode.translations;
|
|
393
|
-
if (!translations || !translations[targetLanguage]) {
|
|
394
|
-
throw new DocmanaError(
|
|
395
|
-
`Translation for language '${targetLanguage}' is missing on node '${rawNode.node_name || rawNode.node_id}'`,
|
|
396
|
-
{ code: "translation_missing", status: 400 }
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
sourceObj = translations[targetLanguage];
|
|
400
|
-
}
|
|
401
|
-
const dynamicFields = {};
|
|
402
|
-
for (const [key, value] of Object.entries(sourceObj)) {
|
|
403
|
-
if (!metadataFields.has(key)) {
|
|
404
|
-
dynamicFields[key] = value;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
const sdkNode = {
|
|
408
|
-
status: rawNode.status || "Unknown"
|
|
409
|
-
};
|
|
410
|
-
if (rawNode.errors && rawNode.errors.length > 0) {
|
|
411
|
-
sdkNode.errors = rawNode.errors;
|
|
412
|
-
}
|
|
413
|
-
const nodeType = rawNode.node_type || "";
|
|
414
|
-
const isScoreNode = /^(Validation|Classification|CrossValidation|Conclusion|Finish)$/i.test(
|
|
415
|
-
nodeType
|
|
416
|
-
);
|
|
417
|
-
if (isScoreNode && rawNode.score !== void 0 && rawNode.score !== null) {
|
|
418
|
-
sdkNode.score = rawNode.score;
|
|
419
|
-
}
|
|
420
|
-
const feedback = sourceObj.feedback ?? rawNode.feedback;
|
|
421
|
-
if (feedback !== void 0 && feedback !== null) {
|
|
422
|
-
sdkNode.feedback = feedback;
|
|
423
|
-
}
|
|
424
|
-
if (Object.keys(dynamicFields).length > 0) {
|
|
425
|
-
sdkNode.data = dynamicFields;
|
|
426
|
-
}
|
|
427
|
-
return sdkNode;
|
|
424
|
+
function normalizePollResult(result) {
|
|
425
|
+
return typeof result === "string" ? { status: result } : result;
|
|
428
426
|
}
|
|
429
|
-
function
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
executionId: String(raw.execution_id || raw.executionResultId || raw.execution_result_id || ""),
|
|
433
|
-
documents: {
|
|
434
|
-
mappings: []
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
if (raw.errors && raw.errors.length > 0) {
|
|
438
|
-
sdkResult.errors = raw.errors;
|
|
439
|
-
}
|
|
440
|
-
const results = raw.results || [];
|
|
441
|
-
for (const item of results) {
|
|
442
|
-
const isDocSpecific = !!item.document;
|
|
443
|
-
if (isDocSpecific && item.document) {
|
|
444
|
-
const docReq = item.document;
|
|
445
|
-
const docKey = docReq.doc_key || "unknown_doc";
|
|
446
|
-
const docName = docReq.name || "";
|
|
447
|
-
sdkResult.documents.mappings.push({
|
|
448
|
-
name: docName,
|
|
449
|
-
reference: docKey
|
|
450
|
-
});
|
|
451
|
-
const docResult = {
|
|
452
|
-
status: item.status || docReq.status || "Unknown",
|
|
453
|
-
extractions: {},
|
|
454
|
-
validations: {}
|
|
455
|
-
};
|
|
456
|
-
const nodeResults = item.node_results || [];
|
|
457
|
-
for (const node of nodeResults) {
|
|
458
|
-
const nodeType = node.node_type || "";
|
|
459
|
-
const nodeName = node.node_name || node.node_id || "unknown_node";
|
|
460
|
-
if (/^Classification$/i.test(nodeType)) {
|
|
461
|
-
const mappedNode = extractNodeFields(node, targetLanguage);
|
|
462
|
-
if (!docResult.classifications) {
|
|
463
|
-
docResult.classifications = {};
|
|
464
|
-
}
|
|
465
|
-
docResult.classifications[nodeName] = mappedNode;
|
|
466
|
-
} else if (/^MetadataExtraction$/i.test(nodeType)) {
|
|
467
|
-
docResult.metadataExtraction = extractNodeFields(node, targetLanguage);
|
|
468
|
-
} else if (/^Extraction$/i.test(nodeType)) {
|
|
469
|
-
if (docResult.extractions) {
|
|
470
|
-
docResult.extractions[nodeName] = extractNodeFields(node, targetLanguage);
|
|
471
|
-
}
|
|
472
|
-
} else if (/^Validation$/i.test(nodeType)) {
|
|
473
|
-
if (docResult.validations) {
|
|
474
|
-
docResult.validations[nodeName] = extractNodeFields(node, targetLanguage);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
if (docResult.extractions && Object.keys(docResult.extractions).length === 0) {
|
|
479
|
-
delete docResult.extractions;
|
|
480
|
-
}
|
|
481
|
-
if (docResult.validations && Object.keys(docResult.validations).length === 0) {
|
|
482
|
-
delete docResult.validations;
|
|
483
|
-
}
|
|
484
|
-
sdkResult.documents[docKey] = docResult;
|
|
485
|
-
} else {
|
|
486
|
-
const nodeResults = item.node_results || [];
|
|
487
|
-
for (const node of nodeResults) {
|
|
488
|
-
const nodeType = node.node_type || "";
|
|
489
|
-
const nodeName = node.node_name || node.node_id || "unknown_node";
|
|
490
|
-
if (/^CrossValidation$/i.test(nodeType)) {
|
|
491
|
-
if (!sdkResult.crossValidations) {
|
|
492
|
-
sdkResult.crossValidations = {};
|
|
493
|
-
}
|
|
494
|
-
sdkResult.crossValidations[nodeName] = extractNodeFields(
|
|
495
|
-
node,
|
|
496
|
-
targetLanguage
|
|
497
|
-
);
|
|
498
|
-
} else if (/^(Conclusion|Finish)$/i.test(nodeType)) {
|
|
499
|
-
if (!sdkResult.conclusions) {
|
|
500
|
-
sdkResult.conclusions = {};
|
|
501
|
-
}
|
|
502
|
-
sdkResult.conclusions[nodeName] = extractNodeFields(
|
|
503
|
-
node,
|
|
504
|
-
targetLanguage
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return sdkResult;
|
|
427
|
+
function fullJitterDelay(intervalMs, maxIntervalMs, attempt, random) {
|
|
428
|
+
const capDelay = Math.min(maxIntervalMs, intervalMs * 2 ** attempt);
|
|
429
|
+
return Math.round(intervalMs + random() * (capDelay - intervalMs));
|
|
511
430
|
}
|
|
512
431
|
|
|
513
432
|
// src/client.ts
|
|
@@ -516,60 +435,79 @@ var Docmana = class {
|
|
|
516
435
|
http;
|
|
517
436
|
constructor(config) {
|
|
518
437
|
this.config = resolveConfig(config);
|
|
519
|
-
const tokenManager = new TokenManager({
|
|
520
|
-
clientId: this.config.clientId,
|
|
521
|
-
clientSecret: this.config.clientSecret,
|
|
522
|
-
tokenEndpoint: this.config.tokenEndpoint,
|
|
523
|
-
scope: this.config.scope,
|
|
524
|
-
fetchImpl: this.config.fetchImpl,
|
|
525
|
-
tokenCache: this.config.tokenCache
|
|
526
|
-
});
|
|
527
438
|
this.http = new HttpClient({
|
|
528
439
|
apiBaseUrl: this.config.apiBaseUrl,
|
|
529
440
|
fetchImpl: this.config.fetchImpl,
|
|
530
|
-
|
|
441
|
+
accessToken: this.config.accessToken,
|
|
442
|
+
getAccessToken: this.config.getAccessToken,
|
|
531
443
|
headers: this.config.headers
|
|
532
444
|
});
|
|
533
445
|
}
|
|
534
446
|
async runFlowAsync(flowId, input) {
|
|
535
447
|
const parts = await resolveInputs(input);
|
|
536
|
-
|
|
537
|
-
const executionResultId = await runFlow(
|
|
448
|
+
return runFlow(
|
|
538
449
|
this.http,
|
|
539
450
|
flowId,
|
|
540
|
-
|
|
451
|
+
parts,
|
|
541
452
|
input.signal,
|
|
542
|
-
input.
|
|
543
|
-
input.context
|
|
453
|
+
input.useDraftVersion ?? false,
|
|
454
|
+
input.context,
|
|
455
|
+
void 0,
|
|
456
|
+
input.language
|
|
544
457
|
);
|
|
545
|
-
return { executionResultId, fileIds };
|
|
546
458
|
}
|
|
547
459
|
async runFlowWithCallback(flowId, input) {
|
|
548
460
|
const parts = await resolveInputs(input);
|
|
549
|
-
|
|
550
|
-
const executionResultId = await runFlow(
|
|
461
|
+
return runFlow(
|
|
551
462
|
this.http,
|
|
552
463
|
flowId,
|
|
553
|
-
|
|
464
|
+
parts,
|
|
554
465
|
input.signal,
|
|
555
|
-
input.
|
|
466
|
+
input.useDraftVersion ?? false,
|
|
556
467
|
input.context,
|
|
557
|
-
input.callbackUrl
|
|
468
|
+
input.callbackUrl,
|
|
469
|
+
input.language
|
|
558
470
|
);
|
|
559
|
-
return { executionResultId, fileIds };
|
|
560
471
|
}
|
|
561
472
|
async getStatus(executionResultId, signal) {
|
|
562
473
|
return getStatus(this.http, executionResultId, signal);
|
|
563
474
|
}
|
|
564
475
|
async getResult(executionResultId, signal, language) {
|
|
565
|
-
|
|
566
|
-
return mapExecutionResult(rawResult, language);
|
|
476
|
+
return getResult(this.http, executionResultId, signal, language);
|
|
567
477
|
}
|
|
568
478
|
async runFlow(flowId, input) {
|
|
569
479
|
const { executionResultId } = await this.runFlowAsync(flowId, input);
|
|
480
|
+
let attempt = 0;
|
|
481
|
+
const startedAt = Date.now();
|
|
570
482
|
await pollUntilTerminal({
|
|
571
|
-
check: async () =>
|
|
483
|
+
check: async () => {
|
|
484
|
+
attempt += 1;
|
|
485
|
+
try {
|
|
486
|
+
const statusResult = await this.getStatus(executionResultId, input.signal);
|
|
487
|
+
const status = statusResult.status;
|
|
488
|
+
const nextPollAfterMs = this.isTerminalStatus(status) ? void 0 : statusResult.pollAfterMs;
|
|
489
|
+
input.onPoll?.({
|
|
490
|
+
executionResultId,
|
|
491
|
+
status,
|
|
492
|
+
attempt,
|
|
493
|
+
elapsedMs: Date.now() - startedAt,
|
|
494
|
+
nextPollAfterMs
|
|
495
|
+
});
|
|
496
|
+
return { status, nextDelayMs: nextPollAfterMs };
|
|
497
|
+
} catch (err) {
|
|
498
|
+
if (err instanceof DocmanaRateLimitError) {
|
|
499
|
+
input.onRateLimit?.({
|
|
500
|
+
executionResultId,
|
|
501
|
+
attempt,
|
|
502
|
+
elapsedMs: Date.now() - startedAt,
|
|
503
|
+
retryAfterMs: err.retryAfterMs ?? input.pollIntervalMs ?? this.config.pollIntervalMs
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
throw err;
|
|
507
|
+
}
|
|
508
|
+
},
|
|
572
509
|
intervalMs: input.pollIntervalMs ?? this.config.pollIntervalMs,
|
|
510
|
+
maxIntervalMs: input.pollMaxIntervalMs ?? this.config.pollMaxIntervalMs,
|
|
573
511
|
timeoutMs: input.timeoutMs ?? this.config.timeoutMs,
|
|
574
512
|
signal: input.signal
|
|
575
513
|
});
|
|
@@ -583,6 +521,9 @@ var Docmana = class {
|
|
|
583
521
|
}
|
|
584
522
|
return result;
|
|
585
523
|
}
|
|
524
|
+
isTerminalStatus(status) {
|
|
525
|
+
return status === "Completed" || status === "Failed";
|
|
526
|
+
}
|
|
586
527
|
};
|
|
587
528
|
|
|
588
529
|
// src/cli.ts
|
|
@@ -653,7 +594,7 @@ function buildProgram(env, io, clientFactory, deps) {
|
|
|
653
594
|
const flow = program.command("flow").description(HELP.flow).action(() => {
|
|
654
595
|
io.stdout(flow.helpInformation());
|
|
655
596
|
});
|
|
656
|
-
flow.command("run").description(HELP.run).argument("<flow-id>", "Docmana flow id").requiredOption("--files <csv>", "Comma-separated file paths to upload").option("--
|
|
597
|
+
flow.command("run").description(HELP.run).argument("<flow-id>", "Docmana flow id").requiredOption("--files <csv>", "Comma-separated file paths to upload").option("--api-url <url>", "Consumer API base URL; defaults to DOCMANA_API_URL").option("--access-token <token>", "Bearer token; defaults to DOCMANA_ACCESS_TOKEN").option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).option("--token-cache <path>", `Token cache path; defaults to ./${DEFAULT_TOKEN_CACHE_FILE}`).option("--json", "Print the raw JSON result").option("--language <lang>", "Translate output to specified language").option("--draft", "Run the current draft flow version").action(async (flowId, options) => {
|
|
657
598
|
await runFlowCommand(flowId, options, env, io, clientFactory, deps);
|
|
658
599
|
});
|
|
659
600
|
const config = program.command("config").description(HELP.config).action(() => {
|
|
@@ -668,46 +609,35 @@ async function runFlowCommand(flowId, options, env, io, clientFactory, deps) {
|
|
|
668
609
|
try {
|
|
669
610
|
const files = parseFiles(options.files);
|
|
670
611
|
const cliConfig = await loadCliConfig(options.config, getCwd(deps), Boolean(options.config));
|
|
671
|
-
const clientId = firstNonEmpty(options.clientId, env.DOCMANA_CLIENT_ID, cliConfig.clientId);
|
|
672
|
-
const clientSecret = firstNonEmpty(
|
|
673
|
-
options.clientSecret,
|
|
674
|
-
env.DOCMANA_CLIENT_SECRET,
|
|
675
|
-
cliConfig.clientSecret
|
|
676
|
-
);
|
|
677
|
-
const organisation = firstNonEmpty(
|
|
678
|
-
options.organisation,
|
|
679
|
-
env.DOCMANA_ORGANISATION,
|
|
680
|
-
cliConfig.organisation
|
|
681
|
-
);
|
|
682
612
|
const apiBaseUrl = firstNonEmpty(options.apiUrl, env.DOCMANA_API_URL, cliConfig.apiBaseUrl);
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
env.DOCMANA_TOKEN_URL,
|
|
686
|
-
cliConfig.tokenEndpoint
|
|
687
|
-
);
|
|
688
|
-
const scope = firstNonEmpty(options.scope, env.DOCMANA_SCOPE, cliConfig.scope);
|
|
689
|
-
if (!clientId)
|
|
690
|
-
throw new CliUsageError("Missing client id. Use --client-id or DOCMANA_CLIENT_ID.");
|
|
691
|
-
if (!clientSecret) {
|
|
692
|
-
throw new CliUsageError(
|
|
693
|
-
"Missing client secret. Use --client-secret or DOCMANA_CLIENT_SECRET."
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
if (!organisation) {
|
|
697
|
-
throw new CliUsageError("Missing organisation. Use --organisation or DOCMANA_ORGANISATION.");
|
|
613
|
+
if (!apiBaseUrl) {
|
|
614
|
+
throw new CliUsageError("Missing API base URL. Use --api-url or DOCMANA_API_URL.");
|
|
698
615
|
}
|
|
616
|
+
const tokenCache = createFileTokenCache(
|
|
617
|
+
resolveTokenCachePath(options.tokenCache, getCwd(deps))
|
|
618
|
+
);
|
|
619
|
+
const accessToken = firstNonEmpty(options.accessToken, env.DOCMANA_ACCESS_TOKEN) ?? await resolveFlowAccessToken(env, cliConfig, tokenCache, deps, io);
|
|
699
620
|
const client = clientFactory({
|
|
700
|
-
clientId,
|
|
701
|
-
clientSecret,
|
|
702
621
|
apiBaseUrl,
|
|
703
|
-
|
|
704
|
-
scope,
|
|
705
|
-
headers: { "X-Selected-Organization": organisation },
|
|
706
|
-
tokenCache: createFileTokenCache(resolveTokenCachePath(options.tokenCache, getCwd(deps)))
|
|
622
|
+
accessToken
|
|
707
623
|
});
|
|
708
624
|
const result = await client.runFlow(flowId, {
|
|
709
625
|
files: files.map((path) => ({ path })),
|
|
710
|
-
language: options.language
|
|
626
|
+
language: options.language,
|
|
627
|
+
useDraftVersion: options.draft === true,
|
|
628
|
+
onPoll: (event) => {
|
|
629
|
+
const nextPoll = event.nextPollAfterMs === void 0 ? "" : `, next poll in ${formatElapsedSeconds(event.nextPollAfterMs)}`;
|
|
630
|
+
io.stderr(
|
|
631
|
+
`Polling execution ${event.executionResultId}: ${event.status} (attempt ${event.attempt}, elapsed ${formatElapsedSeconds(event.elapsedMs)}${nextPoll})
|
|
632
|
+
`
|
|
633
|
+
);
|
|
634
|
+
},
|
|
635
|
+
onRateLimit: (event) => {
|
|
636
|
+
io.stderr(
|
|
637
|
+
`Polling execution ${event.executionResultId}: rate limited (attempt ${event.attempt}, retrying in ${formatElapsedSeconds(event.retryAfterMs)})
|
|
638
|
+
`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
711
641
|
});
|
|
712
642
|
if (options.json) {
|
|
713
643
|
io.stdout(`${JSON.stringify(result, null, 2)}
|
|
@@ -737,10 +667,38 @@ async function runFlowCommand(flowId, options, env, io, clientFactory, deps) {
|
|
|
737
667
|
throw err;
|
|
738
668
|
}
|
|
739
669
|
}
|
|
670
|
+
async function resolveFlowAccessToken(env, cliConfig, tokenCache, deps, io) {
|
|
671
|
+
const cached = await readFreshCachedToken(tokenCache);
|
|
672
|
+
if (cached) return cached;
|
|
673
|
+
const clientId = firstNonEmpty(env.DOCMANA_CLIENT_ID, cliConfig.clientId);
|
|
674
|
+
const clientSecret = firstNonEmpty(env.DOCMANA_CLIENT_SECRET, cliConfig.clientSecret);
|
|
675
|
+
if (!clientId || !clientSecret) {
|
|
676
|
+
throw new CliUsageError(
|
|
677
|
+
"Missing access token. Use --access-token, DOCMANA_ACCESS_TOKEN, docmana login, or configure OAuth credentials."
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
const tokenManager = new TokenManager({
|
|
681
|
+
clientId,
|
|
682
|
+
clientSecret,
|
|
683
|
+
tokenEndpoint: firstNonEmpty(env.DOCMANA_TOKEN_URL, cliConfig.tokenEndpoint) ?? DEFAULTS.tokenEndpoint,
|
|
684
|
+
scope: firstNonEmpty(env.DOCMANA_SCOPE, cliConfig.scope) ?? DEFAULTS.scope,
|
|
685
|
+
fetchImpl: deps.fetch ?? fetch,
|
|
686
|
+
tokenCache
|
|
687
|
+
});
|
|
688
|
+
const token = await tokenManager.getToken();
|
|
689
|
+
io.stderr("Generated new access token and updated the token cache.\n");
|
|
690
|
+
return token;
|
|
691
|
+
}
|
|
692
|
+
async function readFreshCachedToken(tokenCache) {
|
|
693
|
+
const cached = await tokenCache.read().catch(() => null);
|
|
694
|
+
if (!cached || Date.now() >= cached.expiresAt) return void 0;
|
|
695
|
+
return cached.accessToken;
|
|
696
|
+
}
|
|
740
697
|
async function loginCommand(options, env, io, deps) {
|
|
741
698
|
const auth = await resolveAuthConfig(options, env, deps);
|
|
742
699
|
const tokenCachePath = resolveTokenCachePath(options.tokenCache, getCwd(deps));
|
|
743
700
|
const tokenCache = createFileTokenCache(tokenCachePath);
|
|
701
|
+
const hadFreshCachedToken = Boolean(await readFreshCachedToken(tokenCache));
|
|
744
702
|
const tokenManager = new TokenManager({
|
|
745
703
|
clientId: auth.clientId,
|
|
746
704
|
clientSecret: auth.clientSecret,
|
|
@@ -751,6 +709,9 @@ async function loginCommand(options, env, io, deps) {
|
|
|
751
709
|
});
|
|
752
710
|
await tokenManager.getToken();
|
|
753
711
|
const cached = await tokenCache.read();
|
|
712
|
+
if (!hadFreshCachedToken) {
|
|
713
|
+
io.stdout("Generated new access token and updated the token cache.\n");
|
|
714
|
+
}
|
|
754
715
|
io.stdout(`Logged in. Token cached at ${tokenCachePath}
|
|
755
716
|
`);
|
|
756
717
|
if (cached) io.stdout(formatTokenExpiry(cached.expiresAt));
|
|
@@ -789,7 +750,7 @@ async function configInitCommand(options, io, deps) {
|
|
|
789
750
|
try {
|
|
790
751
|
const config = {
|
|
791
752
|
apiBaseUrl: await prompt("API base URL", {
|
|
792
|
-
defaultValue: existing.apiBaseUrl
|
|
753
|
+
defaultValue: existing.apiBaseUrl
|
|
793
754
|
}),
|
|
794
755
|
tokenEndpoint: await prompt("OAuth2 token endpoint", {
|
|
795
756
|
defaultValue: existing.tokenEndpoint ?? DEFAULTS.tokenEndpoint
|
|
@@ -797,9 +758,6 @@ async function configInitCommand(options, io, deps) {
|
|
|
797
758
|
scope: await prompt("OAuth2 scope", {
|
|
798
759
|
defaultValue: existing.scope ?? DEFAULTS.scope
|
|
799
760
|
}),
|
|
800
|
-
organisation: await prompt("Organisation", {
|
|
801
|
-
defaultValue: existing.organisation
|
|
802
|
-
}),
|
|
803
761
|
clientId: await prompt("Client id", {
|
|
804
762
|
defaultValue: existing.clientId
|
|
805
763
|
}),
|
|
@@ -848,7 +806,6 @@ function normalizeCliConfig(config) {
|
|
|
848
806
|
apiBaseUrl: stringValue(config.apiBaseUrl),
|
|
849
807
|
tokenEndpoint: stringValue(config.tokenEndpoint),
|
|
850
808
|
scope: stringValue(config.scope),
|
|
851
|
-
organisation: stringValue(config.organisation),
|
|
852
809
|
clientId: stringValue(config.clientId),
|
|
853
810
|
clientSecret: stringValue(config.clientSecret)
|
|
854
811
|
};
|
|
@@ -924,6 +881,9 @@ function formatPrompt(label, options) {
|
|
|
924
881
|
const defaultHint = options.defaultValue && !options.secret ? ` [${options.defaultValue}]` : options.defaultValue && options.secret ? " [current value hidden]" : "";
|
|
925
882
|
return `${label}${defaultHint}: `;
|
|
926
883
|
}
|
|
884
|
+
function formatElapsedSeconds(elapsedMs) {
|
|
885
|
+
return `${Math.floor(elapsedMs / 1e3)}s`;
|
|
886
|
+
}
|
|
927
887
|
async function readStdinLines() {
|
|
928
888
|
let input = "";
|
|
929
889
|
for await (const chunk of process.stdin) input += String(chunk);
|