@firedrill-tools/unstructured 0.1.4
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/LICENSE +201 -0
- package/README.md +275 -0
- package/firedrill/agent.target.json +16 -0
- package/firedrill/baseline.scenario.json +5 -0
- package/firedrill/conformance.suite.json +21 -0
- package/firedrill/overloaded.scenario.json +11 -0
- package/firedrill/rate-limited.scenario.json +11 -0
- package/firedrill/run-response-lost.scenario.json +11 -0
- package/firedrill/small-responses.scenario.json +21 -0
- package/firedrill/tight-limits.scenario.json +21 -0
- package/firedrill/tools/unstructured/behavior.mjs +69 -0
- package/firedrill/tools/unstructured/lib/connectors.mjs +68 -0
- package/firedrill/tools/unstructured/lib/errors.mjs +58 -0
- package/firedrill/tools/unstructured/lib/gzip.mjs +38 -0
- package/firedrill/tools/unstructured/lib/identity.mjs +19 -0
- package/firedrill/tools/unstructured/lib/ids.mjs +36 -0
- package/firedrill/tools/unstructured/lib/jobs-derive.mjs +92 -0
- package/firedrill/tools/unstructured/lib/multipart.mjs +171 -0
- package/firedrill/tools/unstructured/lib/pages.mjs +40 -0
- package/firedrill/tools/unstructured/lib/partition/chunk.mjs +243 -0
- package/firedrill/tools/unstructured/lib/partition/csv.mjs +85 -0
- package/firedrill/tools/unstructured/lib/partition/csvout.mjs +25 -0
- package/firedrill/tools/unstructured/lib/partition/elements.mjs +171 -0
- package/firedrill/tools/unstructured/lib/partition/email.mjs +269 -0
- package/firedrill/tools/unstructured/lib/partition/html-tokens.mjs +134 -0
- package/firedrill/tools/unstructured/lib/partition/html-util.mjs +99 -0
- package/firedrill/tools/unstructured/lib/partition/html.mjs +211 -0
- package/firedrill/tools/unstructured/lib/partition/index.mjs +122 -0
- package/firedrill/tools/unstructured/lib/partition/markdown.mjs +220 -0
- package/firedrill/tools/unstructured/lib/partition/other.mjs +118 -0
- package/firedrill/tools/unstructured/lib/partition/text.mjs +53 -0
- package/firedrill/tools/unstructured/lib/sha256.mjs +161 -0
- package/firedrill/tools/unstructured/lib/store.mjs +33 -0
- package/firedrill/tools/unstructured/lib/util.mjs +149 -0
- package/firedrill/tools/unstructured/lib/validate.mjs +115 -0
- package/firedrill/tools/unstructured/lib/wire-multipart.mjs +78 -0
- package/firedrill/tools/unstructured/lib/wire.mjs +154 -0
- package/firedrill/tools/unstructured/ops/connectors.mjs +129 -0
- package/firedrill/tools/unstructured/ops/jobs.mjs +83 -0
- package/firedrill/tools/unstructured/ops/nodes.mjs +107 -0
- package/firedrill/tools/unstructured/ops/partition.mjs +112 -0
- package/firedrill/tools/unstructured/ops/workflows.mjs +180 -0
- package/firedrill/tools/unstructured/unstructured.tool.json +4892 -0
- package/firedrill/unstructured-archivist.drill.json +68 -0
- package/firedrill/unstructured-chunking.drill.json +67 -0
- package/firedrill/unstructured-connectors.drill.json +121 -0
- package/firedrill/unstructured-denied.drill.json +58 -0
- package/firedrill/unstructured-fresh-actor.drill.json +68 -0
- package/firedrill/unstructured-overloaded.drill.json +51 -0
- package/firedrill/unstructured-partition-errors.drill.json +66 -0
- package/firedrill/unstructured-partition.drill.json +95 -0
- package/firedrill/unstructured-rate-limited.drill.json +66 -0
- package/firedrill/unstructured-revoked-key.drill.json +773 -0
- package/firedrill/unstructured-run-lost.drill.json +51 -0
- package/firedrill/unstructured-small-responses.drill.json +173 -0
- package/firedrill/unstructured-tight-limits.drill.json +203 -0
- package/firedrill/unstructured-workflows-jobs.drill.json +167 -0
- package/firedrill/world.json +1556 -0
- package/firedrill.json +5 -0
- package/package.json +52 -0
- package/starter.json +1114 -0
- package/test/conformance.mjs +37 -0
- package/test/flows/access.mjs +54 -0
- package/test/flows/chunking.mjs +95 -0
- package/test/flows/connectors.mjs +76 -0
- package/test/flows/errors.mjs +115 -0
- package/test/flows/faults.mjs +73 -0
- package/test/flows/partition.mjs +225 -0
- package/test/flows/workflows.mjs +123 -0
- package/test/hostile-gen.mjs +0 -0
- package/test/hostile.mjs +155 -0
- package/test/lib.mjs +113 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Workflows: list, create, get, update, delete and run (which creates a job from the source's files).
|
|
2
|
+
import { Issues, notFound, requestProblem } from "../lib/errors.mjs";
|
|
3
|
+
import { limits, nextSeq, usableId, uuid } from "../lib/ids.mjs";
|
|
4
|
+
import { workspaceOf } from "../lib/identity.mjs";
|
|
5
|
+
import { isRunning, jobPhase, jobView, nowIso } from "../lib/jobs-derive.mjs";
|
|
6
|
+
import { pageOf } from "../lib/pages.mjs";
|
|
7
|
+
import { partitionFile } from "../lib/partition/index.mjs";
|
|
8
|
+
import { scanAll, scanPrefix } from "../lib/store.mjs";
|
|
9
|
+
import { clip, utf8Length } from "../lib/util.mjs";
|
|
10
|
+
import { mangled, parseBool, parseEnum, parseInteger, parseString } from "../lib/validate.mjs";
|
|
11
|
+
import { findConnector } from "./connectors.mjs";
|
|
12
|
+
import { defaultNodes, nodeOptions, validateNodes } from "./nodes.mjs";
|
|
13
|
+
import { checkFiles } from "./partition.mjs";
|
|
14
|
+
|
|
15
|
+
export const SCHEDULES = ["every 15 minutes", "every hour", "every 2 hours", "every 4 hours", "every 6 hours", "every 8 hours", "every 10 hours", "every 12 hours", "daily", "weekly", "monthly"];
|
|
16
|
+
const STATUSES = ["active", "inactive", "paused"];
|
|
17
|
+
const SORT_BY = ["id", "name", "created_at", "updated_at", "status"];
|
|
18
|
+
const UNSUPPORTED_FILTERS = ["show_only_soft_deleted", "show_recommender_workflows", "dag_node_configuration_id", "created_since", "created_before"];
|
|
19
|
+
const UNSUPPORTED_FIELDS = ["template_id", "source_ids", "destination_ids", "skip_preflight"];
|
|
20
|
+
const KEY_RE = /^[a-z0-9](-?[a-z0-9])*$/;
|
|
21
|
+
const RUNTIME_BUDGET = 900 * 1024;
|
|
22
|
+
|
|
23
|
+
export const workflowView = (row) => ({
|
|
24
|
+
id: row.id, name: row.name, workflow_type: row.workflow_type, source_id: row.source_id, destination_id: row.destination_id, status: row.status,
|
|
25
|
+
schedule: row.schedule, workflow_nodes: row.workflow_nodes, reprocess_all: row.reprocess_all, created_at: row.created_at, updated_at: row.updated_at,
|
|
26
|
+
sources: row.source_id === null ? [] : [row.source_id], destinations: row.destination_id === null ? [] : [row.destination_id], key: row.key,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export function findWorkflow(context, id, workspace) {
|
|
30
|
+
if (!usableId(id) || mangled(id)) notFound(context, "Workflow not found");
|
|
31
|
+
const row = context.state.get("workflows", id);
|
|
32
|
+
if (row === null || row.workspace_id !== workspace) notFound(context, "Workflow not found");
|
|
33
|
+
return row;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function connectorRef(input, field, namespace, label, workspace, context, issues) {
|
|
37
|
+
const value = input[field];
|
|
38
|
+
if (value === undefined || value === null || value === "") return null;
|
|
39
|
+
if (typeof value !== "string") return (issues.add(`body.${field}`, "Input should be a valid string"), null);
|
|
40
|
+
const row = usableId(value) && !mangled(value) ? context.state.get(namespace, value) : null;
|
|
41
|
+
if (row === null || row.workspace_id !== workspace) issues.add(`body.${field}`, `${label} not found`);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function commonFields(input, context, workspace, issues, existing) {
|
|
46
|
+
for (const field of UNSUPPORTED_FIELDS) if (input[field] !== undefined) issues.add(`body.${field}`, `${field} is not supported by this Tool`);
|
|
47
|
+
const out = {};
|
|
48
|
+
out.name = parseString(input.name, "body.name", issues, { required: existing === null, min: 1, max: 200 });
|
|
49
|
+
out.source_id = Object.hasOwn(input, "source_id") ? connectorRef(input, "source_id", "sources", "Source connector", workspace, context, issues) : existing?.source_id ?? null;
|
|
50
|
+
out.destination_id = Object.hasOwn(input, "destination_id") ? connectorRef(input, "destination_id", "destinations", "Destination connector", workspace, context, issues) : existing?.destination_id ?? null;
|
|
51
|
+
out.schedule = Object.hasOwn(input, "schedule") ? parseEnum(input.schedule, "body.schedule", issues, SCHEDULES, null) : existing?.schedule ?? null;
|
|
52
|
+
out.reprocess_all = parseBool(input.reprocess_all, "body.reprocess_all", issues, existing?.reprocess_all ?? false);
|
|
53
|
+
const type = input.workflow_type === undefined || input.workflow_type === null ? existing?.workflow_type ?? null : parseEnum(input.workflow_type, "body.workflow_type", issues, ["auto", "custom"], null);
|
|
54
|
+
if (type === null && existing === null) issues.add("body.workflow_type", "Field required");
|
|
55
|
+
out.workflow_type = type;
|
|
56
|
+
if (type === "custom" && (existing === null || input.workflow_nodes !== undefined)) out.workflow_nodes = validateNodes(input.workflow_nodes, "body.workflow_nodes", issues, context);
|
|
57
|
+
else if (type === "auto") out.workflow_nodes = existing === null || existing.workflow_type !== "auto" ? defaultNodes(context) : existing.workflow_nodes;
|
|
58
|
+
else out.workflow_nodes = existing?.workflow_nodes ?? [];
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const list = (input, context) => {
|
|
63
|
+
const workspace = workspaceOf(context);
|
|
64
|
+
requestProblem(input, context);
|
|
65
|
+
const issues = new Issues();
|
|
66
|
+
for (const filter of UNSUPPORTED_FILTERS) if (input[filter] !== undefined) issues.add(`query.${filter}`, `${filter} is not supported by this Tool`);
|
|
67
|
+
const status = parseEnum(input.status, "query.status", issues, STATUSES, null);
|
|
68
|
+
const page = parseInteger(input.page, "query.page", issues, 1, { min: 1, max: 1000000 });
|
|
69
|
+
const pageSize = parseInteger(input.page_size, "query.page_size", issues, 20, { min: 1, max: 100 });
|
|
70
|
+
const sortBy = parseEnum(input.sort_by, "query.sort_by", issues, SORT_BY, "id");
|
|
71
|
+
const direction = parseEnum(input.sort_direction, "query.sort_direction", issues, ["asc", "desc"], "asc");
|
|
72
|
+
for (const field of ["source_id", "destination_id", "name"]) if (input[field] !== undefined && (typeof input[field] !== "string" || mangled(input[field]))) issues.add(`query.${field}`, "Input should be a valid string");
|
|
73
|
+
issues.raise(context);
|
|
74
|
+
const rows = scanAll(context, "workflows", limits(context).workflows).filter(
|
|
75
|
+
(row) => row.workspace_id === workspace && (status === null || row.status === status) && (input.source_id === undefined || row.source_id === input.source_id) && (input.destination_id === undefined || row.destination_id === input.destination_id) && (input.name === undefined || row.name === input.name),
|
|
76
|
+
);
|
|
77
|
+
const sign = direction === "desc" ? -1 : 1;
|
|
78
|
+
rows.sort((a, b) => {
|
|
79
|
+
const left = a[sortBy] ?? "";
|
|
80
|
+
const right = b[sortBy] ?? "";
|
|
81
|
+
if (left < right) return -sign;
|
|
82
|
+
if (left > right) return sign;
|
|
83
|
+
return (a.seq - b.seq) * sign;
|
|
84
|
+
});
|
|
85
|
+
return pageOf(context, rows.map(workflowView), (page - 1) * pageSize, pageSize);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const create = (input, context) => {
|
|
89
|
+
const workspace = workspaceOf(context);
|
|
90
|
+
requestProblem(input, context);
|
|
91
|
+
const issues = new Issues();
|
|
92
|
+
const fields = commonFields(input, context, workspace, issues, null);
|
|
93
|
+
const key = parseString(input.key, "body.key", issues, { max: 63 });
|
|
94
|
+
if (key !== null && !KEY_RE.test(key)) issues.add("body.key", "String should match pattern '^[a-z0-9](-?[a-z0-9])*$'");
|
|
95
|
+
issues.raise(context);
|
|
96
|
+
if (key !== null && scanAll(context, "workflows", limits(context).workflows).some((row) => row.workspace_id === workspace && row.key === key)) issues.add("body.key", "Workflow key already exists in this workspace").raise(context);
|
|
97
|
+
const row = { id: uuid(context), workspace_id: workspace, seq: nextSeq(context), ...fields, status: "active", key, created_at: nowIso(context), updated_at: null };
|
|
98
|
+
context.state.put("workflows", row.id, row);
|
|
99
|
+
return workflowView(row);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const get = (input, context) => workflowView(findWorkflow(context, input.workflow_id, workspaceOf(context)));
|
|
103
|
+
|
|
104
|
+
export const update = (input, context) => {
|
|
105
|
+
const workspace = workspaceOf(context);
|
|
106
|
+
requestProblem(input, context);
|
|
107
|
+
const row = findWorkflow(context, input.workflow_id, workspace);
|
|
108
|
+
const issues = new Issues();
|
|
109
|
+
const fields = commonFields(input, context, workspace, issues, row);
|
|
110
|
+
const status = parseEnum(input.status, "body.status", issues, STATUSES, row.status);
|
|
111
|
+
issues.raise(context);
|
|
112
|
+
const next = { ...row, ...fields, name: fields.name ?? row.name, status, updated_at: nowIso(context) };
|
|
113
|
+
context.state.put("workflows", row.id, next);
|
|
114
|
+
return workflowView(next);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const remove = (input, context) => {
|
|
118
|
+
const workspace = workspaceOf(context);
|
|
119
|
+
const row = findWorkflow(context, input.workflow_id, workspace);
|
|
120
|
+
const now = context.clock.nowUs();
|
|
121
|
+
if (scanAll(context, "jobs", limits(context).jobs).some((job) => job.workflow_id === row.id && isRunning(jobPhase(job, now)))) new Issues().add("body", "Workflow has a running job").raise(context);
|
|
122
|
+
context.state.delete("workflows", row.id);
|
|
123
|
+
return {};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/** Job file entry from a source file row or a runtime upload; `error` is set when the file cannot be processed. */
|
|
127
|
+
function jobFile(file, sourceRow, options, context) {
|
|
128
|
+
const entry = { file_id: file.file_id, source_row: sourceRow, filename: clip(file.filename, 200), path: clip(file.path, 500), content_type: clip(file.content_type, 100), size_bytes: file.size_bytes, error: null };
|
|
129
|
+
if (typeof file.error === "string" && file.error.length > 0) entry.error = clip(file.error, 500);
|
|
130
|
+
else {
|
|
131
|
+
const result = partitionFile({ filename: file.filename, content: file.content, content_type: file.content_type }, options, context);
|
|
132
|
+
if (!result.ok) entry.error = clip(result.message, 500);
|
|
133
|
+
}
|
|
134
|
+
return entry;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const run = (input, context) => {
|
|
138
|
+
const workspace = workspaceOf(context);
|
|
139
|
+
requestProblem(input, context);
|
|
140
|
+
const workflow = findWorkflow(context, input.workflow_id, workspace);
|
|
141
|
+
const issues = new Issues();
|
|
142
|
+
if (workflow.status !== "active") issues.add("body", "Workflow is not active").raise(context);
|
|
143
|
+
const runtime = input.input_files === undefined || input.input_files === null ? [] : input.input_files;
|
|
144
|
+
if (runtime.length > 0) checkFiles(runtime, context, "body.input_files");
|
|
145
|
+
if (workflow.source_id === null && runtime.length === 0) issues.add("body.input_files", "Field required").raise(context);
|
|
146
|
+
let total = 0;
|
|
147
|
+
for (const file of runtime) total += utf8Length(file.content);
|
|
148
|
+
if (total > RUNTIME_BUDGET) issues.add("body.input_files", `Runtime input files exceed ${RUNTIME_BUDGET} bytes in total`).raise(context);
|
|
149
|
+
const options = nodeOptions(workflow.workflow_nodes, nowIso(context));
|
|
150
|
+
const files = [];
|
|
151
|
+
if (workflow.source_id !== null) {
|
|
152
|
+
const source = findConnectorOrIssue(context, workflow.source_id, workspace, issues);
|
|
153
|
+
for (const file of scanPrefix(context, "source-files", `${source.id}:`, limits(context).files_per_source)) files.push(jobFile({ ...file, file_id: `${source.id}:${file.file_id}` }, `${source.id}:${file.file_id}`, options, context));
|
|
154
|
+
}
|
|
155
|
+
const jobId = uuid(context);
|
|
156
|
+
const uploads = [];
|
|
157
|
+
for (const [index, file] of runtime.entries()) {
|
|
158
|
+
const fileId = uuid(context);
|
|
159
|
+
const upload = { file_id: fileId, filename: file.filename, path: `upload://${file.filename}`, content_type: typeof file.content_type === "string" ? file.content_type : "", size_bytes: utf8Length(file.content), content: file.content, error: null };
|
|
160
|
+
files.push(jobFile(upload, null, options, context));
|
|
161
|
+
uploads.push({ rowId: `${jobId}:${fileId}`, value: { job_id: jobId, file_id: fileId, filename: clip(file.filename, 200), content_type: upload.content_type, size_bytes: upload.size_bytes, content: file.content } });
|
|
162
|
+
void index;
|
|
163
|
+
}
|
|
164
|
+
if (files.length === 0) issues.add("body", "Source connector holds no files").raise(context);
|
|
165
|
+
const nodes = workflow.workflow_nodes;
|
|
166
|
+
const job = {
|
|
167
|
+
id: jobId, workspace_id: workspace, seq: nextSeq(context), workflow_id: workflow.id, workflow_name: workflow.name, job_type: "ephemeral", created_at: nowIso(context),
|
|
168
|
+
cancelled_at: null, stop_requested: false, files, nodes, destination_node_id: nodes.length > 0 ? nodes[nodes.length - 1].id : null, created_by: clip(context.actor.id, 200),
|
|
169
|
+
};
|
|
170
|
+
context.state.put("jobs", job.id, job);
|
|
171
|
+
for (const upload of uploads) context.state.put("job-files", upload.rowId, upload.value);
|
|
172
|
+
context.events.emit("job.created", { job_id: job.id, workflow_id: workflow.id, workflow_name: workflow.name, input_file_count: files.length, runtime_file_count: uploads.length });
|
|
173
|
+
return jobView(job, context.clock.nowUs());
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
function findConnectorOrIssue(context, id, workspace, issues) {
|
|
177
|
+
const row = usableId(id) ? context.state.get("sources", id) : null;
|
|
178
|
+
if (row === null || row.workspace_id !== workspace) issues.add("body.source_id", "Source connector not found").raise(context);
|
|
179
|
+
return row;
|
|
180
|
+
}
|