@toloka-platform/sdk 0.11.2 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/toloka.js +157 -154
- package/dist/cli/toloka.js.jsc +0 -0
- package/dist/cli/toloka.js.jsc.sha256 +1 -1
- package/dist/cli/toloka.js.sha256 +1 -1
- package/docs/authoring.md +6 -4
- package/docs/cli.md +30 -11
- package/docs/limitations.md +2 -2
- package/docs/testing.md +4 -2
- package/examples/README.md +11 -0
- package/examples/app-crowdtesting/README.md +41 -0
- package/examples/app-crowdtesting/fields.ts +6 -0
- package/examples/app-crowdtesting/nodes/app-flow-test/node.ts +381 -0
- package/examples/app-crowdtesting/nodes/collection/node.ts +8 -0
- package/examples/app-crowdtesting/pipeline.browser.integration.spec.ts +77 -0
- package/examples/app-crowdtesting/pipeline.ts +17 -0
- package/examples/egocentric-video-collection/README.md +35 -0
- package/examples/egocentric-video-collection/fields.ts +24 -0
- package/examples/egocentric-video-collection/nodes/collection/node.ts +5 -0
- package/examples/egocentric-video-collection/nodes/record-activity/node.ts +760 -0
- package/examples/egocentric-video-collection/pipeline.browser.integration.spec.ts +65 -0
- package/examples/egocentric-video-collection/pipeline.ts +17 -0
- package/examples/product-moderation/README.md +49 -0
- package/examples/product-moderation/fields.ts +37 -0
- package/examples/product-moderation/nodes/collection/node.ts +16 -0
- package/examples/product-moderation/nodes/moderate-product/instruction.ts +77 -0
- package/examples/product-moderation/nodes/moderate-product/node.ts +368 -0
- package/examples/product-moderation/pipeline.browser.integration.spec.ts +48 -0
- package/examples/product-moderation/pipeline.ts +33 -0
- package/examples/video-pair-rating/fields.ts +16 -4
- package/examples/video-pair-rating/nodes/adjudicate/node.ts +78 -3
- package/examples/video-pair-rating/nodes/adjudicate_op/node.ts +28 -3
- package/examples/video-pair-rating/nodes/overall_preference_pick/node.ts +28 -3
- package/examples/video-pair-rating/nodes/rate_pair/node.ts +65 -2
- package/openapi/gen/api.ts +22 -0
- package/openapi/gen/models/generationBudget.ts +8 -0
- package/openapi/gen/models/generationBudgetReasoningEffort.ts +2 -0
- package/openapi/gen/models/generationModel.ts +12 -0
- package/openapi/gen/models/generationModelDefaultReasoningEffort.ts +2 -0
- package/openapi/gen/models/generationModelId.ts +2 -0
- package/openapi/gen/models/index.ts +8 -0
- package/openapi/gen/models/invalidFieldSchema.ts +6 -0
- package/openapi/gen/models/listGenerationModels200.ts +7 -0
- package/openapi/gen/models/listGenerationModels200DefaultGenerationModelId.ts +2 -0
- package/openapi/gen/models/pipelineGraphSnapshot.ts +2 -0
- package/package.json +2 -2
- package/skills/catalog.json +16 -12
- package/skills/toloka-build/SKILL.md +44 -58
- package/skills/toloka-build/references/human-work.md +156 -0
- package/skills/toloka-build/references/local-setup.md +136 -0
- package/skills/toloka-build/references/node-checks.md +11 -0
- package/skills/toloka-build/references/node-semantics.md +22 -213
- package/skills/toloka-build/references/task-ui.md +61 -0
- package/skills/toloka-build/references/workflow-and-cli.md +29 -229
- package/skills/toloka-fine-tuning/SKILL.md +1 -1
- package/skills/toloka-get-started/SKILL.md +1 -1
- package/skills/toloka-gisting/SKILL.md +1 -1
- package/src/authoring/index.ts +26 -7
- package/src/checks/task-ui-typecheck.ts +3 -3
- package/src/cli/analytics.ts +1 -0
- package/src/cli/check.ts +83 -12
- package/src/cli/commands/generation-model.ts +18 -0
- package/src/cli/commands/pipeline.ts +1 -1
- package/src/cli/commands/source.ts +14 -3
- package/src/cli/generation-models.ts +46 -0
- package/src/cli/program.ts +2 -0
- package/src/cli/project.ts +288 -138
- package/src/cli/trainings.ts +12 -0
- package/src/cli/ui.ts +3 -1
- package/src/client.ts +5 -0
- package/src/graph-rules.json +11 -0
- package/src/importer.ts +46 -40
- package/src/index.ts +4 -0
- package/src/validate.ts +183 -0
package/src/cli/project.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { resolvePipelineDefinition, validatePipelineDefinition } from "../author
|
|
|
10
10
|
import type { Dataset, DatasetField, Pipeline, PotatoClient } from "../client";
|
|
11
11
|
import { generatePipelineSourceTree } from "../importer";
|
|
12
12
|
import { readTolokaState, TOLOKA_STATE_FILENAME, writeTolokaState } from "../state";
|
|
13
|
+
import { platformRuleIssues } from "../validate";
|
|
13
14
|
import type { TolokaState } from "../state";
|
|
14
15
|
import { assertProjectSelected } from "./project-context";
|
|
15
16
|
import { collectGraphSecretNames } from "./secrets";
|
|
@@ -82,9 +83,11 @@ export type ApplyProjectResult = {
|
|
|
82
83
|
readonly output: string;
|
|
83
84
|
};
|
|
84
85
|
|
|
85
|
-
export type ValidateProjectOptions = PlanProjectOptions
|
|
86
|
+
export type ValidateProjectOptions = PlanProjectOptions & {
|
|
87
|
+
readonly wait?: { readonly timeoutMs: number; readonly pollIntervalMs: number };
|
|
88
|
+
};
|
|
86
89
|
|
|
87
|
-
export type PipelineValidationStatus = "valid" | "invalid" | "checks_pending";
|
|
90
|
+
export type PipelineValidationStatus = "valid" | "invalid" | "checks_pending" | "checks_running";
|
|
88
91
|
|
|
89
92
|
export type PipelineValidationResult = {
|
|
90
93
|
readonly sourceDirectory: string;
|
|
@@ -92,6 +95,7 @@ export type PipelineValidationResult = {
|
|
|
92
95
|
readonly report?: PipelineValidationReport;
|
|
93
96
|
readonly status?: PipelineValidationStatus;
|
|
94
97
|
readonly skipped?: string;
|
|
98
|
+
readonly note?: string;
|
|
95
99
|
};
|
|
96
100
|
|
|
97
101
|
export type ValidateProjectResult = {
|
|
@@ -261,6 +265,13 @@ export function diffPipelineGraphs(
|
|
|
261
265
|
];
|
|
262
266
|
}
|
|
263
267
|
|
|
268
|
+
type CheckedNode = Extract<PipelineGraphV1["nodes"][number], { type: "code" | "generation" | "quorum" }>;
|
|
269
|
+
|
|
270
|
+
const withLiveChecks = <Node extends CheckedNode>(authored: Node, live: Node): Node =>
|
|
271
|
+
live.settings.checks === undefined
|
|
272
|
+
? authored
|
|
273
|
+
: { ...authored, settings: { ...authored.settings, checks: { ...live.settings.checks, ...authored.settings.checks } } };
|
|
274
|
+
|
|
264
275
|
const graphWithLiveState = (
|
|
265
276
|
authoredGraph: PipelineGraphV1,
|
|
266
277
|
liveGraph: PipelineGraphV1,
|
|
@@ -277,28 +288,43 @@ const graphWithLiveState = (
|
|
|
277
288
|
? {}
|
|
278
289
|
: { itemLimit: liveRoot.settings.itemLimit }),
|
|
279
290
|
};
|
|
291
|
+
const liveNodes = new Map(liveGraph.nodes.map((node) => [node.id, node]));
|
|
280
292
|
return {
|
|
281
293
|
...authoredGraph,
|
|
282
294
|
...(liveGraph.layout === undefined ? {} : { layout: liveGraph.layout }),
|
|
283
|
-
nodes: authoredGraph.nodes.map((node) =>
|
|
284
|
-
|
|
285
|
-
|
|
295
|
+
nodes: authoredGraph.nodes.map((node) => {
|
|
296
|
+
const live = liveNodes.get(node.id);
|
|
297
|
+
switch (node.type) {
|
|
298
|
+
case "root":
|
|
299
|
+
return { ...node, settings: { ...liveRunScope, ...node.settings } };
|
|
300
|
+
case "code":
|
|
301
|
+
return live?.type === "code" ? withLiveChecks(node, live) : node;
|
|
302
|
+
case "generation":
|
|
303
|
+
return live?.type === "generation" ? withLiveChecks(node, live) : node;
|
|
304
|
+
case "quorum":
|
|
305
|
+
return live?.type === "quorum" ? withLiveChecks(node, live) : node;
|
|
306
|
+
case "match":
|
|
307
|
+
return node;
|
|
308
|
+
}
|
|
309
|
+
}),
|
|
286
310
|
};
|
|
287
311
|
};
|
|
288
312
|
|
|
289
313
|
export const desiredDatasets = (source: ProjectSource): readonly DesiredDataset[] => {
|
|
290
314
|
const fieldsByDataset = new Map<string, Map<string, PipelineGraphV1["fields"][number]>>();
|
|
291
315
|
for (const pipeline of source.pipelines) {
|
|
292
|
-
|
|
316
|
+
const datasetName = pipeline.definition.dataset;
|
|
317
|
+
if (datasetName === undefined) continue;
|
|
318
|
+
let fields = fieldsByDataset.get(datasetName);
|
|
293
319
|
if (fields === undefined) {
|
|
294
320
|
fields = new Map();
|
|
295
|
-
fieldsByDataset.set(
|
|
321
|
+
fieldsByDataset.set(datasetName, fields);
|
|
296
322
|
}
|
|
297
323
|
for (const field of pipeline.definition.graph.fields) {
|
|
298
324
|
const existing = fields.get(field.name);
|
|
299
325
|
assertCli(
|
|
300
326
|
existing === undefined || valueKey(existing.jsonSchema) === valueKey(field.jsonSchema),
|
|
301
|
-
`Dataset "${
|
|
327
|
+
`Dataset "${datasetName}" has conflicting schemas for field "${field.name}".`,
|
|
302
328
|
{ hint: { description: "Give the field one JSON schema across all pipelines that share the dataset." } },
|
|
303
329
|
);
|
|
304
330
|
if (existing === undefined) fields.set(field.name, field);
|
|
@@ -452,6 +478,29 @@ const inspectDatasets = async (
|
|
|
452
478
|
});
|
|
453
479
|
};
|
|
454
480
|
|
|
481
|
+
const pipelineGraphChanges = (
|
|
482
|
+
pipelineSource: ProjectPipelineSource,
|
|
483
|
+
snapshot: PipelineGraphSnapshot | undefined,
|
|
484
|
+
datasets: readonly DatasetInspection[],
|
|
485
|
+
): readonly PipelinePlanChange[] => {
|
|
486
|
+
if (snapshot === undefined) return [];
|
|
487
|
+
const datasetName = pipelineSource.definition.dataset;
|
|
488
|
+
const liveDatasetId = snapshot.graph.nodes.find((node) => node.type === "root")?.settings.datasetId;
|
|
489
|
+
if (datasetName === undefined && liveDatasetId !== undefined)
|
|
490
|
+
return [{ kind: "node", action: "removed", label: "root dataset binding", id: "root" }];
|
|
491
|
+
const dataset =
|
|
492
|
+
datasetName === undefined
|
|
493
|
+
? undefined
|
|
494
|
+
: datasets.find((entry) => entry.name === datasetName)?.dataset;
|
|
495
|
+
if (datasetName !== undefined && dataset === undefined)
|
|
496
|
+
return [{ kind: "node", action: "changed", label: "root dataset binding", id: "root" }];
|
|
497
|
+
const desiredGraph = resolvePipelineDefinition(
|
|
498
|
+
pipelineSource.definition,
|
|
499
|
+
dataset === undefined ? {} : { datasetId: dataset.id },
|
|
500
|
+
);
|
|
501
|
+
return diffPipelineGraphs(graphWithLiveState(desiredGraph, snapshot.graph), snapshot.graph);
|
|
502
|
+
};
|
|
503
|
+
|
|
455
504
|
const inspectPipelines = async (
|
|
456
505
|
client: PotatoClient,
|
|
457
506
|
source: ProjectSource,
|
|
@@ -474,27 +523,11 @@ const inspectPipelines = async (
|
|
|
474
523
|
);
|
|
475
524
|
return pipelinePairs.map(({ source: pipelineSource, pipeline }, index) => {
|
|
476
525
|
const snapshot = snapshots[index];
|
|
477
|
-
const dataset = datasets.find(
|
|
478
|
-
(entry) => entry.name === pipelineSource.definition.dataset,
|
|
479
|
-
)?.dataset;
|
|
480
|
-
let graphChanges: readonly PipelinePlanChange[] = [];
|
|
481
|
-
if (pipeline !== undefined && snapshot !== undefined) {
|
|
482
|
-
graphChanges =
|
|
483
|
-
dataset === undefined
|
|
484
|
-
? [{ kind: "node", action: "changed", label: "root dataset binding", id: "root" }]
|
|
485
|
-
: diffPipelineGraphs(
|
|
486
|
-
graphWithLiveState(
|
|
487
|
-
resolvePipelineDefinition(pipelineSource.definition, { datasetId: dataset.id }),
|
|
488
|
-
snapshot.graph,
|
|
489
|
-
),
|
|
490
|
-
snapshot.graph,
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
526
|
return {
|
|
494
527
|
source: pipelineSource,
|
|
495
528
|
...(pipeline === undefined ? {} : { pipeline }),
|
|
496
529
|
...(snapshot === undefined ? {} : { snapshot }),
|
|
497
|
-
graphChanges,
|
|
530
|
+
graphChanges: pipelineGraphChanges(pipelineSource, snapshot, datasets),
|
|
498
531
|
};
|
|
499
532
|
});
|
|
500
533
|
};
|
|
@@ -638,13 +671,8 @@ export async function importProject(
|
|
|
638
671
|
);
|
|
639
672
|
const datasetIds = new Set<string>();
|
|
640
673
|
for (const snapshot of snapshots) {
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
rootNode?.settings.datasetId,
|
|
644
|
-
"Every imported pipeline must have a bound dataset.",
|
|
645
|
-
{ hint: { description: "Bind a dataset to the pipeline in the platform, then re-run toloka import." } },
|
|
646
|
-
);
|
|
647
|
-
datasetIds.add(rootNode.settings.datasetId);
|
|
674
|
+
const datasetId = snapshot.graph.nodes.find((node) => node.type === "root")?.settings.datasetId;
|
|
675
|
+
if (datasetId !== undefined) datasetIds.add(datasetId);
|
|
648
676
|
}
|
|
649
677
|
const projectDatasets = await collectAsync(
|
|
650
678
|
client.datasets.listAll({ projectId: options.projectId }),
|
|
@@ -667,25 +695,22 @@ export async function importProject(
|
|
|
667
695
|
for (const [index, pipeline] of pipelines.entries()) {
|
|
668
696
|
const snapshot = snapshots[index];
|
|
669
697
|
assertCli(snapshot, `Pipeline ${pipeline.id} has no graph snapshot.`, { hint: REPORT_HINT });
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
rootNode?.settings.datasetId,
|
|
673
|
-
`Pipeline ${pipeline.id} has no dataset binding.`,
|
|
674
|
-
{ hint: { description: "Bind a dataset to the pipeline in the platform, then re-run toloka import." } },
|
|
675
|
-
);
|
|
676
|
-
const dataset = datasetsById.get(rootNode.settings.datasetId);
|
|
698
|
+
const datasetId = snapshot.graph.nodes.find((node) => node.type === "root")?.settings.datasetId;
|
|
699
|
+
const dataset = datasetId === undefined ? undefined : datasetsById.get(datasetId);
|
|
677
700
|
assertCli(
|
|
678
|
-
dataset,
|
|
679
|
-
`Pipeline ${pipeline.id} references unknown dataset ${
|
|
701
|
+
datasetId === undefined || dataset !== undefined,
|
|
702
|
+
`Pipeline ${pipeline.id} references unknown dataset ${datasetId}.`,
|
|
680
703
|
{ hint: { description: "Check toloka dataset list; the pipeline's dataset binding must point at a dataset of this project." } },
|
|
681
704
|
);
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
705
|
+
if (dataset !== undefined) {
|
|
706
|
+
const existingDatasetBinding = stateDatasets[dataset.name];
|
|
707
|
+
assertCli(
|
|
708
|
+
existingDatasetBinding === undefined || existingDatasetBinding.datasetId === dataset.id,
|
|
709
|
+
`Project has multiple referenced datasets named "${dataset.name}".`,
|
|
710
|
+
{ hint: { description: "Rename the duplicate datasets in the platform, then re-run toloka import." } },
|
|
711
|
+
);
|
|
712
|
+
stateDatasets[dataset.name] = { datasetId: dataset.id };
|
|
713
|
+
}
|
|
689
714
|
const sourceDirectory = directories.get(pipeline.id);
|
|
690
715
|
assertCli(
|
|
691
716
|
sourceDirectory,
|
|
@@ -694,7 +719,10 @@ export async function importProject(
|
|
|
694
719
|
);
|
|
695
720
|
statePipelines[sourceDirectory] = { pipelineId: pipeline.id };
|
|
696
721
|
for (const [relativePath, source] of Object.entries(
|
|
697
|
-
generatePipelineSourceTree(snapshot.graph, {
|
|
722
|
+
generatePipelineSourceTree(snapshot.graph, {
|
|
723
|
+
...(dataset === undefined ? {} : { dataset: dataset.name }),
|
|
724
|
+
invalidFieldSchemas: snapshot.invalidFieldSchemas,
|
|
725
|
+
}),
|
|
698
726
|
)) {
|
|
699
727
|
tree[`${sourceDirectory}/${relativePath}`] = source;
|
|
700
728
|
}
|
|
@@ -777,6 +805,45 @@ export async function projectStatus(
|
|
|
777
805
|
};
|
|
778
806
|
}
|
|
779
807
|
|
|
808
|
+
const applyPipeline = async (
|
|
809
|
+
client: PotatoClient,
|
|
810
|
+
projectId: string,
|
|
811
|
+
pipelineInspection: PipelineInspection,
|
|
812
|
+
datasetIds: ReadonlyMap<string, string>,
|
|
813
|
+
): Promise<{ readonly pipelineId: string; readonly writes: number }> => {
|
|
814
|
+
const datasetName = pipelineInspection.source.definition.dataset;
|
|
815
|
+
const datasetId = datasetName === undefined ? undefined : datasetIds.get(datasetName);
|
|
816
|
+
assertCli(
|
|
817
|
+
datasetName === undefined || datasetId !== undefined,
|
|
818
|
+
`Dataset ${datasetName} has no resolved ID.`,
|
|
819
|
+
{ hint: REPORT_HINT },
|
|
820
|
+
);
|
|
821
|
+
const desiredGraph = resolvePipelineDefinition(
|
|
822
|
+
pipelineInspection.source.definition,
|
|
823
|
+
datasetId === undefined ? {} : { datasetId },
|
|
824
|
+
);
|
|
825
|
+
if (pipelineInspection.pipeline === undefined) {
|
|
826
|
+
const pipeline = await client.pipelines.import({
|
|
827
|
+
projectId,
|
|
828
|
+
name: pipelineInspection.source.name,
|
|
829
|
+
graph: desiredGraph,
|
|
830
|
+
});
|
|
831
|
+
if (datasetId === undefined) return { pipelineId: pipeline.id, writes: 1 };
|
|
832
|
+
await client.pipelines.attachDataset({ pipelineId: pipeline.id, datasetId });
|
|
833
|
+
return { pipelineId: pipeline.id, writes: 2 };
|
|
834
|
+
}
|
|
835
|
+
const pipelineId = pipelineInspection.pipeline.id;
|
|
836
|
+
if (pipelineInspection.graphChanges.length === 0) return { pipelineId, writes: 0 };
|
|
837
|
+
const snapshot = pipelineInspection.snapshot;
|
|
838
|
+
assertCli(snapshot, `Pipeline ${pipelineId} has no graph snapshot.`, { hint: REPORT_HINT });
|
|
839
|
+
await client.pipelines.replaceGraph({
|
|
840
|
+
pipelineId,
|
|
841
|
+
graph: graphWithLiveState(desiredGraph, snapshot.graph),
|
|
842
|
+
expectedVersion: snapshot.version,
|
|
843
|
+
});
|
|
844
|
+
return { pipelineId, writes: 1 };
|
|
845
|
+
};
|
|
846
|
+
|
|
780
847
|
export async function applyProject(
|
|
781
848
|
client: PotatoClient,
|
|
782
849
|
options: ApplyProjectOptions,
|
|
@@ -787,6 +854,19 @@ export async function applyProject(
|
|
|
787
854
|
`Refusing to delete pipelines absent from source: ${inspection.removedPipelines.join(", ")}.`,
|
|
788
855
|
{ hint: { description: "Restore the pipeline in source, or run toloka project untrack pipeline <name> to stop managing it." } },
|
|
789
856
|
);
|
|
857
|
+
const detachedPipelines = inspection.pipelines
|
|
858
|
+
.filter((pipeline) =>
|
|
859
|
+
pipeline.graphChanges.some(
|
|
860
|
+
(change) => change.label === "root dataset binding" && change.action === "removed",
|
|
861
|
+
),
|
|
862
|
+
)
|
|
863
|
+
.map((pipeline) => pipeline.source.sourceDirectory);
|
|
864
|
+
assertCli(
|
|
865
|
+
detachedPipelines.length === 0,
|
|
866
|
+
`Refusing to unbind the dataset of ${detachedPipelines.join(", ")}: the source declares no dataset, but the pipeline has one on the platform.`,
|
|
867
|
+
{ hint: { description: "Declare the dataset in definePipeline; toloka import writes the name of the bound dataset." } },
|
|
868
|
+
);
|
|
869
|
+
|
|
790
870
|
assertCli(
|
|
791
871
|
inspection.removedDatasets.length === 0,
|
|
792
872
|
`Refusing to delete datasets absent from source: ${inspection.removedDatasets.join(", ")}. Dataset items are never deleted.`,
|
|
@@ -806,6 +886,19 @@ export async function applyProject(
|
|
|
806
886
|
{ hint: { description: "Add the changed columns under new field names, or create a new dataset for the new schema." } },
|
|
807
887
|
);
|
|
808
888
|
|
|
889
|
+
const ruleViolations = inspection.pipelines
|
|
890
|
+
.filter((pipeline) => pipeline.pipeline === undefined || pipeline.graphChanges.length > 0)
|
|
891
|
+
.flatMap((pipeline) =>
|
|
892
|
+
platformRuleIssues(pipeline.source.definition.graph).map(
|
|
893
|
+
(issue) => `- ${pipeline.source.sourceDirectory}, node ${issue.node}: ${issue.reason}`,
|
|
894
|
+
),
|
|
895
|
+
);
|
|
896
|
+
assertCli(
|
|
897
|
+
ruleViolations.length === 0,
|
|
898
|
+
`The platform would reject these pipelines:\n${ruleViolations.join("\n")}`,
|
|
899
|
+
{ hint: { description: "Fix the listed nodes in source; toloka check reports the same rules as warnings." } },
|
|
900
|
+
);
|
|
901
|
+
|
|
809
902
|
let writes = 0;
|
|
810
903
|
const datasetIds = new Map<string, string>();
|
|
811
904
|
for (const datasetInspection of inspection.datasets) {
|
|
@@ -832,40 +925,9 @@ export async function applyProject(
|
|
|
832
925
|
|
|
833
926
|
const pipelineIds = new Map<string, string>();
|
|
834
927
|
for (const pipelineInspection of inspection.pipelines) {
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
`Dataset ${pipelineInspection.source.definition.dataset} has no resolved ID.`,
|
|
839
|
-
{ hint: REPORT_HINT },
|
|
840
|
-
);
|
|
841
|
-
const desiredGraph = resolvePipelineDefinition(pipelineInspection.source.definition, {
|
|
842
|
-
datasetId,
|
|
843
|
-
});
|
|
844
|
-
if (pipelineInspection.pipeline === undefined) {
|
|
845
|
-
const pipeline = await client.pipelines.import({
|
|
846
|
-
projectId: inspection.projectId,
|
|
847
|
-
name: pipelineInspection.source.name,
|
|
848
|
-
graph: desiredGraph,
|
|
849
|
-
});
|
|
850
|
-
await client.pipelines.attachDataset({ pipelineId: pipeline.id, datasetId });
|
|
851
|
-
pipelineIds.set(pipelineInspection.source.sourceDirectory, pipeline.id);
|
|
852
|
-
writes += 2;
|
|
853
|
-
continue;
|
|
854
|
-
}
|
|
855
|
-
pipelineIds.set(pipelineInspection.source.sourceDirectory, pipelineInspection.pipeline.id);
|
|
856
|
-
if (pipelineInspection.graphChanges.length === 0) continue;
|
|
857
|
-
const snapshot = pipelineInspection.snapshot;
|
|
858
|
-
assertCli(
|
|
859
|
-
snapshot,
|
|
860
|
-
`Pipeline ${pipelineInspection.pipeline.id} has no graph snapshot.`,
|
|
861
|
-
{ hint: REPORT_HINT },
|
|
862
|
-
);
|
|
863
|
-
await client.pipelines.replaceGraph({
|
|
864
|
-
pipelineId: pipelineInspection.pipeline.id,
|
|
865
|
-
graph: graphWithLiveState(desiredGraph, snapshot.graph),
|
|
866
|
-
expectedVersion: snapshot.version,
|
|
867
|
-
});
|
|
868
|
-
writes += 1;
|
|
928
|
+
const applied = await applyPipeline(client, inspection.projectId, pipelineInspection, datasetIds);
|
|
929
|
+
pipelineIds.set(pipelineInspection.source.sourceDirectory, applied.pipelineId);
|
|
930
|
+
writes += applied.writes;
|
|
869
931
|
}
|
|
870
932
|
|
|
871
933
|
const nextState: TolokaState = {
|
|
@@ -910,6 +972,10 @@ const issueLocation = (issue: { nodeId?: string; checkId?: string }) =>
|
|
|
910
972
|
[issue.nodeId, issue.checkId].filter((value): value is string => value !== undefined).join("/");
|
|
911
973
|
|
|
912
974
|
const PENDING_CHECK_CODE = "not_ran_node_check";
|
|
975
|
+
const RUNNING_CHECK_CODE = "running_node_check";
|
|
976
|
+
|
|
977
|
+
const hasRunningChecks = (report: PipelineValidationReport) =>
|
|
978
|
+
report.issues.some((issue) => issue.code === RUNNING_CHECK_CODE);
|
|
913
979
|
|
|
914
980
|
const normalizeValidationIssue = (issue: PipelineValidationIssue): PipelineValidationIssue =>
|
|
915
981
|
issue.code === "empty_node_checks"
|
|
@@ -948,72 +1014,156 @@ const normalizeValidationReport = (
|
|
|
948
1014
|
const formatValidationIssues = (report: PipelineValidationReport) =>
|
|
949
1015
|
report.issues.map((issue) => {
|
|
950
1016
|
const location = issueLocation(issue);
|
|
951
|
-
const severity =
|
|
1017
|
+
const severity =
|
|
1018
|
+
issue.code === PENDING_CHECK_CODE
|
|
1019
|
+
? "pending"
|
|
1020
|
+
: issue.code === RUNNING_CHECK_CODE
|
|
1021
|
+
? "running"
|
|
1022
|
+
: issue.severity;
|
|
952
1023
|
return ` ${severity} ${issue.code}${location.length === 0 ? "" : ` ${location}`}: ${issue.message}`;
|
|
953
1024
|
});
|
|
954
1025
|
|
|
1026
|
+
type NormalizedValidation = ReturnType<typeof normalizeValidationReport>;
|
|
1027
|
+
|
|
1028
|
+
type RunningValidation = {
|
|
1029
|
+
readonly index: number;
|
|
1030
|
+
readonly validate: () => Promise<NormalizedValidation>;
|
|
1031
|
+
readonly failure?: string;
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
const sleep = (ms: number) =>
|
|
1035
|
+
new Promise<void>((resolve) => {
|
|
1036
|
+
setTimeout(resolve, ms);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
const pollValidationEntry = async (
|
|
1040
|
+
results: PipelineValidationResult[],
|
|
1041
|
+
entry: RunningValidation,
|
|
1042
|
+
): Promise<RunningValidation | undefined> => {
|
|
1043
|
+
try {
|
|
1044
|
+
const { report, status } = await entry.validate();
|
|
1045
|
+
results[entry.index] = { ...results[entry.index]!, report, status };
|
|
1046
|
+
return hasRunningChecks(report) ? { index: entry.index, validate: entry.validate } : undefined;
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
return { ...entry, failure: error instanceof Error ? error.message : String(error) };
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
const waitForRunningChecks = async (
|
|
1053
|
+
results: PipelineValidationResult[],
|
|
1054
|
+
running: readonly RunningValidation[],
|
|
1055
|
+
wait: NonNullable<ValidateProjectOptions["wait"]>,
|
|
1056
|
+
startedAt: number,
|
|
1057
|
+
) => {
|
|
1058
|
+
const deadline = startedAt + wait.timeoutMs;
|
|
1059
|
+
let pending = running;
|
|
1060
|
+
while (pending.length > 0 && Date.now() < deadline) {
|
|
1061
|
+
await sleep(Math.min(wait.pollIntervalMs, deadline - Date.now()));
|
|
1062
|
+
const next: RunningValidation[] = [];
|
|
1063
|
+
for (const entry of pending) {
|
|
1064
|
+
const retry = await pollValidationEntry(results, entry);
|
|
1065
|
+
if (retry !== undefined) next.push(retry);
|
|
1066
|
+
}
|
|
1067
|
+
pending = next;
|
|
1068
|
+
}
|
|
1069
|
+
const waitedSeconds = Math.round((Date.now() - startedAt) / 1000);
|
|
1070
|
+
for (const entry of pending) {
|
|
1071
|
+
const result = results[entry.index]!;
|
|
1072
|
+
results[entry.index] = {
|
|
1073
|
+
...result,
|
|
1074
|
+
status: result.status === "invalid" ? "invalid" : "checks_running",
|
|
1075
|
+
note:
|
|
1076
|
+
entry.failure === undefined
|
|
1077
|
+
? `checks are still running after ${waitedSeconds}s; run toloka validate --wait again`
|
|
1078
|
+
: `validation failed while waiting (${entry.failure}); run toloka validate --wait again`,
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
const validatePipelineEntry = async (
|
|
1084
|
+
client: PotatoClient,
|
|
1085
|
+
inspection: ProjectInspection,
|
|
1086
|
+
pipeline: ProjectInspection["pipelines"][number],
|
|
1087
|
+
wait: ValidateProjectOptions["wait"],
|
|
1088
|
+
): Promise<{
|
|
1089
|
+
readonly result: PipelineValidationResult;
|
|
1090
|
+
readonly poll?: () => Promise<NormalizedValidation>;
|
|
1091
|
+
}> => {
|
|
1092
|
+
const { sourceDirectory } = pipeline.source;
|
|
1093
|
+
const datasetName = pipeline.source.definition.dataset;
|
|
1094
|
+
const dataset =
|
|
1095
|
+
datasetName === undefined
|
|
1096
|
+
? undefined
|
|
1097
|
+
: inspection.datasets.find((entry) => entry.name === datasetName)?.dataset;
|
|
1098
|
+
if (pipeline.pipeline === undefined)
|
|
1099
|
+
return { result: { sourceDirectory, skipped: "pipeline is not on the platform yet; run toloka apply first" } };
|
|
1100
|
+
if (datasetName !== undefined && dataset === undefined)
|
|
1101
|
+
return {
|
|
1102
|
+
result: {
|
|
1103
|
+
sourceDirectory,
|
|
1104
|
+
pipelineId: pipeline.pipeline.id,
|
|
1105
|
+
skipped: `dataset "${datasetName}" is not on the platform yet; run toloka apply first`,
|
|
1106
|
+
},
|
|
1107
|
+
};
|
|
1108
|
+
const resolved = resolvePipelineDefinition(
|
|
1109
|
+
pipeline.source.definition,
|
|
1110
|
+
dataset === undefined ? {} : { datasetId: dataset.id },
|
|
1111
|
+
);
|
|
1112
|
+
const graph = pipeline.snapshot === undefined ? resolved : graphWithLiveState(resolved, pipeline.snapshot.graph);
|
|
1113
|
+
const pipelineId = pipeline.pipeline.id;
|
|
1114
|
+
const validatePipeline = async () =>
|
|
1115
|
+
normalizeValidationReport(await client.pipelines.validateGraph({ pipelineId, graph }));
|
|
1116
|
+
const { report, status } = await validatePipeline();
|
|
1117
|
+
const waitForChecks = wait !== undefined && hasRunningChecks(report);
|
|
1118
|
+
const unapplied = waitForChecks && pipeline.graphChanges.length > 0;
|
|
1119
|
+
return {
|
|
1120
|
+
result: {
|
|
1121
|
+
sourceDirectory,
|
|
1122
|
+
pipelineId,
|
|
1123
|
+
report,
|
|
1124
|
+
status,
|
|
1125
|
+
...(unapplied
|
|
1126
|
+
? { note: "lints run on the applied graph; run toloka apply, then toloka validate --wait" }
|
|
1127
|
+
: {}),
|
|
1128
|
+
},
|
|
1129
|
+
...(waitForChecks && !unapplied ? { poll: validatePipeline } : {}),
|
|
1130
|
+
};
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
const formatValidationResult = (result: PipelineValidationResult): string[] => {
|
|
1134
|
+
if (result.skipped !== undefined) return [`- ${result.sourceDirectory}: skipped (${result.skipped})`];
|
|
1135
|
+
assertCli(result.report, `Pipeline ${result.sourceDirectory} is missing a validation report`, {
|
|
1136
|
+
hint: REPORT_HINT,
|
|
1137
|
+
});
|
|
1138
|
+
assertCli(result.status, `Pipeline ${result.sourceDirectory} is missing a validation status`, {
|
|
1139
|
+
hint: REPORT_HINT,
|
|
1140
|
+
});
|
|
1141
|
+
return [
|
|
1142
|
+
`- ${result.sourceDirectory}: ${result.status.replace("_", "-")}`,
|
|
1143
|
+
...formatValidationIssues(result.report),
|
|
1144
|
+
...(result.note === undefined ? [] : [` note: ${result.note}`]),
|
|
1145
|
+
];
|
|
1146
|
+
};
|
|
1147
|
+
|
|
955
1148
|
export async function validateProject(
|
|
956
1149
|
client: PotatoClient,
|
|
957
1150
|
options: ValidateProjectOptions,
|
|
958
1151
|
): Promise<ValidateProjectResult> {
|
|
1152
|
+
const startedAt = Date.now();
|
|
959
1153
|
const inspection = await inspectProject(client, options);
|
|
1154
|
+
const { wait } = options;
|
|
960
1155
|
const results: PipelineValidationResult[] = [];
|
|
1156
|
+
const running: RunningValidation[] = [];
|
|
961
1157
|
for (const pipeline of inspection.pipelines) {
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
)
|
|
965
|
-
if (pipeline.pipeline === undefined) {
|
|
966
|
-
results.push({
|
|
967
|
-
sourceDirectory: pipeline.source.sourceDirectory,
|
|
968
|
-
skipped: "pipeline is not on the platform yet; run toloka apply first",
|
|
969
|
-
});
|
|
970
|
-
continue;
|
|
971
|
-
}
|
|
972
|
-
if (dataset === undefined) {
|
|
973
|
-
results.push({
|
|
974
|
-
sourceDirectory: pipeline.source.sourceDirectory,
|
|
975
|
-
pipelineId: pipeline.pipeline.id,
|
|
976
|
-
skipped: `dataset "${pipeline.source.definition.dataset}" is not on the platform yet; run toloka apply first`,
|
|
977
|
-
});
|
|
978
|
-
continue;
|
|
979
|
-
}
|
|
980
|
-
const graph =
|
|
981
|
-
pipeline.snapshot === undefined
|
|
982
|
-
? resolvePipelineDefinition(pipeline.source.definition, { datasetId: dataset.id })
|
|
983
|
-
: graphWithLiveState(
|
|
984
|
-
resolvePipelineDefinition(pipeline.source.definition, { datasetId: dataset.id }),
|
|
985
|
-
pipeline.snapshot.graph,
|
|
986
|
-
);
|
|
987
|
-
const normalized = normalizeValidationReport(
|
|
988
|
-
await client.pipelines.validateGraph({
|
|
989
|
-
pipelineId: pipeline.pipeline.id,
|
|
990
|
-
graph,
|
|
991
|
-
}),
|
|
992
|
-
);
|
|
993
|
-
results.push({
|
|
994
|
-
sourceDirectory: pipeline.source.sourceDirectory,
|
|
995
|
-
pipelineId: pipeline.pipeline.id,
|
|
996
|
-
report: normalized.report,
|
|
997
|
-
status: normalized.status,
|
|
998
|
-
});
|
|
1158
|
+
const { result, poll } = await validatePipelineEntry(client, inspection, pipeline, wait);
|
|
1159
|
+
results.push(result);
|
|
1160
|
+
if (poll !== undefined) running.push({ index: results.length - 1, validate: poll });
|
|
999
1161
|
}
|
|
1000
|
-
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
result.report,
|
|
1006
|
-
`Pipeline ${result.sourceDirectory} is missing a validation report`,
|
|
1007
|
-
{ hint: REPORT_HINT },
|
|
1008
|
-
);
|
|
1009
|
-
assertCli(
|
|
1010
|
-
result.status,
|
|
1011
|
-
`Pipeline ${result.sourceDirectory} is missing a validation status`,
|
|
1012
|
-
{ hint: REPORT_HINT },
|
|
1013
|
-
);
|
|
1014
|
-
const header = `- ${result.sourceDirectory}: ${result.status.replace("_", "-")}`;
|
|
1015
|
-
return [header, ...formatValidationIssues(result.report)];
|
|
1016
|
-
});
|
|
1162
|
+
if (wait !== undefined) await waitForRunningChecks(results, running, wait, startedAt);
|
|
1163
|
+
const valid = results.every(
|
|
1164
|
+
(result) => result.status !== "invalid" && result.status !== "checks_running",
|
|
1165
|
+
);
|
|
1166
|
+
const lines = results.flatMap(formatValidationResult);
|
|
1017
1167
|
return {
|
|
1018
1168
|
projectId: inspection.projectId,
|
|
1019
1169
|
results,
|
package/src/cli/trainings.ts
CHANGED
|
@@ -313,6 +313,16 @@ const uploadPromptFile = async (
|
|
|
313
313
|
}
|
|
314
314
|
};
|
|
315
315
|
|
|
316
|
+
const assertDatasetIdOption = (
|
|
317
|
+
flag: "--dataset" | "--validation-dataset",
|
|
318
|
+
value: string | undefined,
|
|
319
|
+
): void =>
|
|
320
|
+
assertCli(
|
|
321
|
+
value === undefined || /^ds\./i.test(value),
|
|
322
|
+
`${flag} takes a Dataset id (ds.…), got ${JSON.stringify(value)}.`,
|
|
323
|
+
{ hint: { description: "Run toloka dataset list and pass the Dataset id (ds.…)." } },
|
|
324
|
+
);
|
|
325
|
+
|
|
316
326
|
const resolveSelections = async ({
|
|
317
327
|
client,
|
|
318
328
|
current,
|
|
@@ -322,6 +332,8 @@ const resolveSelections = async ({
|
|
|
322
332
|
current: Training["configuration"] | undefined;
|
|
323
333
|
options: TrainingDraftOptions;
|
|
324
334
|
}): Promise<ResolvedSelections> => {
|
|
335
|
+
assertDatasetIdOption("--dataset", options.datasetId);
|
|
336
|
+
assertDatasetIdOption("--validation-dataset", options.validationDatasetId);
|
|
325
337
|
const trainingDatasetId = options.datasetId ?? current?.training.datasetId;
|
|
326
338
|
assertCli(trainingDatasetId, "--dataset is required.", {
|
|
327
339
|
hint: { description: "Pass --dataset <dataset-id>; toloka dataset list shows the ids." },
|
package/src/cli/ui.ts
CHANGED
|
@@ -9,7 +9,9 @@ export const formatComponentIndex = (
|
|
|
9
9
|
components: Readonly<Record<string, TaskUiComponentDescription>>,
|
|
10
10
|
): string =>
|
|
11
11
|
[
|
|
12
|
-
...Object.values(components).map(
|
|
12
|
+
...Object.values(components).map(
|
|
13
|
+
({ name, description, whenToUse }) => `${name}: ${description} When to use: ${whenToUse}`,
|
|
14
|
+
),
|
|
13
15
|
"",
|
|
14
16
|
"Run `toloka ui components <name> [name...]` for the props, value shape, and example of a component.",
|
|
15
17
|
].join("\n");
|
package/src/client.ts
CHANGED
|
@@ -652,6 +652,10 @@ const createClientImplementation = (config: ClientConfig) => {
|
|
|
652
652
|
list: async () => (await wire.listDeliveryOrders(requestOptions)).deliveryOrders,
|
|
653
653
|
};
|
|
654
654
|
|
|
655
|
+
const generationModels = {
|
|
656
|
+
list: () => wire.listGenerationModels(requestOptions),
|
|
657
|
+
};
|
|
658
|
+
|
|
655
659
|
const audience = {
|
|
656
660
|
specializations: {
|
|
657
661
|
listPage: (params: ListAudienceSpecializationsParams = {}) =>
|
|
@@ -674,6 +678,7 @@ const createClientImplementation = (config: ClientConfig) => {
|
|
|
674
678
|
pipelines,
|
|
675
679
|
datasets,
|
|
676
680
|
baseModels,
|
|
681
|
+
generationModels,
|
|
677
682
|
models,
|
|
678
683
|
trainings,
|
|
679
684
|
runs,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"namePattern": "^[a-z]([a-z0-9_]*[a-z0-9])?$",
|
|
3
|
+
"nodeNameMinLength": 4,
|
|
4
|
+
"nodeNameMaxLength": 32,
|
|
5
|
+
"minTimeForTaskSeconds": 10,
|
|
6
|
+
"minHumanQaTimeForTaskSeconds": 30,
|
|
7
|
+
"humanQaSecondsPerQualityCriterion": 3,
|
|
8
|
+
"minQualityThresholdLength": 10,
|
|
9
|
+
"maxExamTasksPerExpert": 10,
|
|
10
|
+
"mediaUploadComponentPattern": "<Field\\.(?:Video(?:Upload|Record)|FileUpload)\\b"
|
|
11
|
+
}
|