@toloka-platform/sdk 0.12.0 → 0.12.2
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 +144 -141
- 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/cli.md +11 -8
- package/docs/datasets.md +17 -5
- package/openapi/gen/api.ts +25 -0
- package/openapi/gen/models/createDatasetFieldsFromUploadedRowsBody.ts +4 -0
- package/openapi/gen/models/datasetExportReportTokensEstimatorVersion.ts +1 -1
- package/openapi/gen/models/datasetField.ts +2 -0
- package/openapi/gen/models/datasetFieldCopyOperation.ts +7 -0
- package/openapi/gen/models/datasetFieldOperation.ts +4 -0
- package/openapi/gen/models/datasetFieldSplitFillOperation.ts +5 -0
- package/openapi/gen/models/index.ts +4 -0
- package/openapi/gen/models/trainingCostEstimateOneOfInputsTrainingReportAnyOfTokensEstimatorVersion.ts +1 -1
- package/openapi/gen/models/trainingCostEstimateOneOfInputsValidationAnyOfReportAnyOfTokensEstimatorVersion.ts +1 -1
- package/openapi/gen/models/trainingCostEstimateOneOfTwosevenInputsTrainingReportAnyOfTokensEstimatorVersion.ts +1 -1
- package/openapi/gen/models/trainingCostEstimateOneOfTwosevenInputsValidationAnyOfReportAnyOfTokensEstimatorVersion.ts +1 -1
- package/openapi/gen/models/trainingRunInputsAnyOfTrainingReportTokensEstimatorVersion.ts +1 -1
- package/openapi/gen/models/trainingRunInputsAnyOfValidationAnyOfReportTokensEstimatorVersion.ts +1 -1
- package/openapi/gen/models/trainingRunQuoteInputsTrainingReportAnyOfTokensEstimatorVersion.ts +1 -1
- package/openapi/gen/models/trainingRunQuoteInputsValidationAnyOfReportAnyOfTokensEstimatorVersion.ts +1 -1
- package/package.json +2 -2
- package/skills/catalog.json +8 -8
- package/skills/toloka-build/references/workflow-and-cli.md +14 -5
- package/skills/toloka-fine-tuning/SKILL.md +5 -4
- package/skills/toloka-gisting/SKILL.md +6 -5
- package/src/cli/commands/dataset.ts +45 -3
- package/src/cli/context.ts +16 -10
- package/src/cli/data.ts +45 -17
- package/src/cli/fields.ts +80 -0
- package/src/cli/filters.ts +15 -6
- package/src/cli/slices.ts +16 -2
- package/src/cli/splits.ts +58 -3
- package/src/cli/trainings.ts +31 -7
- package/src/cli/uploaded-fields.ts +20 -0
- package/src/client.ts +38 -1
- package/src/index.ts +1 -0
|
@@ -3,6 +3,7 @@ import type { Command } from "commander";
|
|
|
3
3
|
import {
|
|
4
4
|
collectValues,
|
|
5
5
|
columnList,
|
|
6
|
+
fieldList,
|
|
6
7
|
positiveInteger,
|
|
7
8
|
resolveContext,
|
|
8
9
|
splitParts,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
loadDataset,
|
|
19
20
|
uploadDataset,
|
|
20
21
|
} from "../data";
|
|
22
|
+
import { addDatasetFields } from "../fields";
|
|
21
23
|
import { assertProjectSelected } from "../project-context";
|
|
22
24
|
import { createDatasetSlice, listDatasetSlices, showDatasetSlice } from "../slices";
|
|
23
25
|
import { listDatasetColumns, splitDataset, type SplitRatios } from "../splits";
|
|
@@ -46,8 +48,11 @@ export const registerDatasetCommands = (program: Command, host: CommandHost): vo
|
|
|
46
48
|
const result = await createDataset(client, { projectId, name });
|
|
47
49
|
context.io.stdout(result.output);
|
|
48
50
|
});
|
|
49
|
-
dataset
|
|
51
|
+
const fields = dataset
|
|
50
52
|
.command("fields")
|
|
53
|
+
.description("print field declarations inferred from uploaded rows, or add dataset fields from them");
|
|
54
|
+
fields
|
|
55
|
+
.command("declarations", { isDefault: true })
|
|
51
56
|
.argument("<name>", "dataset name or id")
|
|
52
57
|
.description("print field declarations inferred from uploaded rows")
|
|
53
58
|
.action(async (name: string) => {
|
|
@@ -59,20 +64,57 @@ export const registerDatasetCommands = (program: Command, host: CommandHost): vo
|
|
|
59
64
|
});
|
|
60
65
|
context.io.stdout(result.output);
|
|
61
66
|
});
|
|
67
|
+
fields
|
|
68
|
+
.command("add")
|
|
69
|
+
.argument("<name>", "dataset name or id")
|
|
70
|
+
.argument("[fields...]", "top-level fields of the uploaded rows to add as dataset fields")
|
|
71
|
+
.option("--all", "add every top-level field of the uploaded rows that is not a dataset field yet", false)
|
|
72
|
+
.option("--no-wait", "return as soon as the fields are added, while their values are still being copied")
|
|
73
|
+
.option("--timeout <ms>", "how long to wait for the values to be copied", positiveInteger)
|
|
74
|
+
.option("--json", "print JSON", false)
|
|
75
|
+
.description(
|
|
76
|
+
"add dataset fields from fields of the uploaded rows, so filters, --balance-by and split columns can use their values; uploads after this fill them too",
|
|
77
|
+
)
|
|
78
|
+
.action(
|
|
79
|
+
async (
|
|
80
|
+
name: string,
|
|
81
|
+
names: string[],
|
|
82
|
+
options: { all: boolean; wait: boolean; timeout?: number; json: boolean },
|
|
83
|
+
) => {
|
|
84
|
+
const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
|
|
85
|
+
const result = await addDatasetFields(client, {
|
|
86
|
+
sourceRoot,
|
|
87
|
+
datasetName: name,
|
|
88
|
+
fields: names,
|
|
89
|
+
all: options.all,
|
|
90
|
+
wait: options.wait,
|
|
91
|
+
json: options.json,
|
|
92
|
+
...(projectId === undefined ? {} : { projectId }),
|
|
93
|
+
...(options.timeout === undefined ? {} : { waitFor: { timeoutMs: options.timeout } }),
|
|
94
|
+
});
|
|
95
|
+
context.io.stdout(result.output);
|
|
96
|
+
},
|
|
97
|
+
);
|
|
62
98
|
dataset
|
|
63
99
|
.command("load")
|
|
64
100
|
.argument("<name>", "dataset name or id")
|
|
65
101
|
.requiredOption("--out <file>", "JSONL file to write")
|
|
102
|
+
.option(
|
|
103
|
+
"--fields <fields>",
|
|
104
|
+
"comma-separated dataset fields to write (default: every field); uploaded holds each uploaded row whole, as one value",
|
|
105
|
+
fieldList,
|
|
106
|
+
)
|
|
66
107
|
.option("--limit <n>", "how many rows to keep after download", positiveInteger)
|
|
67
108
|
.option("--timeout <ms>", "how long to wait for the export", positiveInteger)
|
|
68
|
-
.description("download live dataset rows into a JSONL file")
|
|
69
|
-
.action(async (name: string, options: { out: string; limit?: number; timeout?: number }) => {
|
|
109
|
+
.description("download live dataset rows into a JSONL file, one object of the chosen fields per row")
|
|
110
|
+
.action(async (name: string, options: { out: string; fields?: string[]; limit?: number; timeout?: number }) => {
|
|
70
111
|
const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
|
|
71
112
|
const result = await loadDataset(client, {
|
|
72
113
|
sourceRoot,
|
|
73
114
|
datasetName: name,
|
|
74
115
|
outPath: resolve(options.out),
|
|
75
116
|
...(projectId === undefined ? {} : { projectId }),
|
|
117
|
+
...(options.fields === undefined ? {} : { fields: options.fields }),
|
|
76
118
|
...(options.limit === undefined ? {} : { limit: options.limit }),
|
|
77
119
|
...(options.timeout === undefined ? {} : { timeoutMs: options.timeout }),
|
|
78
120
|
});
|
package/src/cli/context.ts
CHANGED
|
@@ -145,16 +145,22 @@ export const splitRatios = (raw: string): { train: number; validate: number; tes
|
|
|
145
145
|
|
|
146
146
|
export const collectValues = (value: string, previous: readonly string[] = []): string[] => [...previous, value];
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
148
|
+
const nameList =
|
|
149
|
+
(noun: string) =>
|
|
150
|
+
(raw: string): string[] => {
|
|
151
|
+
const names = raw
|
|
152
|
+
.split(",")
|
|
153
|
+
.map((name) => name.trim())
|
|
154
|
+
.filter((name) => name.length > 0);
|
|
155
|
+
if (names.length === 0) {
|
|
156
|
+
throw new InvalidArgumentError(`Expected one or more ${noun} names separated by commas, for example text,intent.`);
|
|
157
|
+
}
|
|
158
|
+
return names;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const columnList = nameList("column");
|
|
162
|
+
|
|
163
|
+
export const fieldList = nameList("field");
|
|
158
164
|
|
|
159
165
|
const SPLIT_PART_ROLES = ["train", "validate", "test"] as const;
|
|
160
166
|
|
package/src/cli/data.ts
CHANGED
|
@@ -92,6 +92,7 @@ export type LoadDatasetOptions = {
|
|
|
92
92
|
readonly datasetName: string;
|
|
93
93
|
readonly outPath: string;
|
|
94
94
|
readonly projectId?: string;
|
|
95
|
+
readonly fields?: readonly string[];
|
|
95
96
|
readonly limit?: number;
|
|
96
97
|
readonly timeoutMs?: number;
|
|
97
98
|
readonly fetch?: typeof globalThis.fetch;
|
|
@@ -106,7 +107,7 @@ type DatasetRef = {
|
|
|
106
107
|
const isJsonObject = (value: unknown): value is Record<string, unknown> =>
|
|
107
108
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
108
109
|
|
|
109
|
-
const summarizeJsonSchema = (schema: unknown): string => {
|
|
110
|
+
export const summarizeJsonSchema = (schema: unknown): string => {
|
|
110
111
|
if (!isJsonObject(schema)) return "unknown";
|
|
111
112
|
if (Array.isArray(schema.anyOf)) {
|
|
112
113
|
const variants = [...new Set(schema.anyOf.map(summarizeJsonSchema))];
|
|
@@ -156,6 +157,13 @@ const declaredDatasetFields = async (options: {
|
|
|
156
157
|
|
|
157
158
|
const formatDatasetChoice = (dataset: Dataset): string => `${dataset.name} (${dataset.id})`;
|
|
158
159
|
|
|
160
|
+
const MAX_LISTED_NAMES = 10;
|
|
161
|
+
|
|
162
|
+
export const listedNames = (names: readonly string[]): string =>
|
|
163
|
+
names.length <= MAX_LISTED_NAMES
|
|
164
|
+
? names.join(", ")
|
|
165
|
+
: `${names.slice(0, MAX_LISTED_NAMES).join(", ")} and ${names.length - MAX_LISTED_NAMES} more`;
|
|
166
|
+
|
|
159
167
|
const collectLiveDatasets = async (client: PotatoClient, projectId: string): Promise<Dataset[]> => {
|
|
160
168
|
const datasets: Dataset[] = [];
|
|
161
169
|
for await (const dataset of client.datasets.listAll({ projectId })) datasets.push(dataset);
|
|
@@ -210,8 +218,10 @@ export const resolveLiveDataset = async (
|
|
|
210
218
|
createWhenMissing && datasetId === undefined,
|
|
211
219
|
live.length === 0
|
|
212
220
|
? `Dataset "${token}" does not exist in project ${projectId}. Run toloka dataset create ${JSON.stringify(token)} first, or toloka apply for a dataset a pipeline declares.`
|
|
213
|
-
: `Dataset "${token}" does not exist in project ${projectId}. This project has: ${live.map(formatDatasetChoice)
|
|
214
|
-
|
|
221
|
+
: `Dataset "${token}" does not exist in project ${projectId}. This project has: ${listedNames(live.map(formatDatasetChoice))}.`,
|
|
222
|
+
live.length <= MAX_LISTED_NAMES
|
|
223
|
+
? { hint: { disabled: true } }
|
|
224
|
+
: { hint: { description: "Run toloka dataset list to see every dataset of the project." } },
|
|
215
225
|
);
|
|
216
226
|
|
|
217
227
|
assertCli(
|
|
@@ -440,22 +450,35 @@ export async function uploadDataset(
|
|
|
440
450
|
const uploadedOutput = `Uploaded ${completed.result.itemsIngested} rows to dataset ${resolved.name}.`;
|
|
441
451
|
let inferredFields: ReadonlyArray<InferredField> = [];
|
|
442
452
|
let inferenceReport: string | undefined;
|
|
443
|
-
|
|
453
|
+
const warnings: string[] = [];
|
|
444
454
|
try {
|
|
445
455
|
const inferred = await client.datasets.getInferredFields({ datasetId });
|
|
446
456
|
inferredFields = inferred.fields;
|
|
447
457
|
inferenceReport = formatInferredFields(inferredFields);
|
|
448
458
|
} catch (error) {
|
|
449
459
|
const reason = error instanceof Error ? error.message : String(error);
|
|
450
|
-
|
|
460
|
+
warnings.push(`Upload succeeded, but inferred fields could not be fetched: ${reason}`);
|
|
461
|
+
}
|
|
462
|
+
if (fields.length > 0) {
|
|
463
|
+
try {
|
|
464
|
+
await client.waitFor({ type: "datasetFields", datasetId }, options.waitFor);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (!(error instanceof PotatoApiError && error.code === "wait_timeout")) throw error;
|
|
467
|
+
warnings.push(
|
|
468
|
+
`Upload succeeded, but its rows are still being copied into the dataset fields; check them with toloka dataset columns ${fieldsHint} --json.`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
451
471
|
}
|
|
472
|
+
const addFieldsHint =
|
|
473
|
+
fields.length === 0 && inferredFields.some((field) => field.depth === 1)
|
|
474
|
+
? `To filter, balance or split by these fields, add them as dataset fields: toloka dataset fields add ${fieldsHint} <field>… (or --all).`
|
|
475
|
+
: undefined;
|
|
452
476
|
return {
|
|
453
477
|
datasetId,
|
|
454
478
|
rowsUploaded: completed.result.itemsIngested,
|
|
455
479
|
inferredFields,
|
|
456
|
-
output:
|
|
457
|
-
|
|
458
|
-
...(warning === undefined ? {} : { warning }),
|
|
480
|
+
output: [uploadedOutput, inferenceReport, addFieldsHint].filter((line) => line !== undefined).join("\n"),
|
|
481
|
+
...(warnings.length === 0 ? {} : { warning: warnings.join("\n") }),
|
|
459
482
|
};
|
|
460
483
|
}
|
|
461
484
|
|
|
@@ -539,16 +562,19 @@ export async function loadDataset(
|
|
|
539
562
|
options: LoadDatasetOptions,
|
|
540
563
|
): Promise<DatasetCommandResult> {
|
|
541
564
|
const resolved = await resolveLiveDataset(client, options, false);
|
|
542
|
-
const
|
|
565
|
+
const datasetRef = resolved.nameIsUnique ? resolved.name : resolved.datasetId;
|
|
566
|
+
const visibleFields: string[] = [];
|
|
543
567
|
for await (const field of client.datasets.listAllFields({ datasetId: resolved.datasetId })) {
|
|
544
|
-
if (field.internal
|
|
545
|
-
fieldNames.push(field.name);
|
|
568
|
+
if (!field.internal) visibleFields.push(field.name);
|
|
546
569
|
}
|
|
547
|
-
assertCli(
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
)
|
|
570
|
+
assertCli(visibleFields.length > 0, `Dataset ${resolved.name} has no rows yet.`, {
|
|
571
|
+
hint: { description: `Upload rows with toloka dataset upload ${datasetRef} <file>.` },
|
|
572
|
+
});
|
|
573
|
+
const unknown = (options.fields ?? []).filter((name) => !visibleFields.includes(name));
|
|
574
|
+
assertCli(unknown.length === 0, `Dataset ${resolved.name} has no field ${unknown.join(", ")}.`, {
|
|
575
|
+
hint: { description: `Its fields are ${listedNames(visibleFields)}.` },
|
|
576
|
+
});
|
|
577
|
+
const fieldNames = options.fields === undefined ? visibleFields : [...options.fields];
|
|
552
578
|
|
|
553
579
|
const started = await client.datasets.startExport({
|
|
554
580
|
datasetId: resolved.datasetId,
|
|
@@ -587,7 +613,9 @@ export async function loadDataset(
|
|
|
587
613
|
options.outPath,
|
|
588
614
|
limited.length === 0 ? "" : `${limited.map((row) => JSON.stringify(row)).join("\n")}\n`,
|
|
589
615
|
);
|
|
590
|
-
return {
|
|
616
|
+
return {
|
|
617
|
+
output: `Wrote ${limited.length} rows with the fields ${listedNames(fieldNames)} to ${options.outPath}.`,
|
|
618
|
+
};
|
|
591
619
|
}
|
|
592
620
|
|
|
593
621
|
export async function setProjectSecret(
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { DatasetField, PotatoClient } from "../client";
|
|
2
|
+
import type { WaitForOptions } from "../wait-for";
|
|
3
|
+
import { assertCli } from "./cli-error";
|
|
4
|
+
import { listedNames, resolveLiveDataset, summarizeJsonSchema } from "./data";
|
|
5
|
+
import { waitForFilledFields } from "./slices";
|
|
6
|
+
import { formatTable } from "./table";
|
|
7
|
+
import { uploadedRowFieldNames } from "./uploaded-fields";
|
|
8
|
+
|
|
9
|
+
export type AddDatasetFieldsOptions = {
|
|
10
|
+
readonly sourceRoot: string;
|
|
11
|
+
readonly datasetName: string;
|
|
12
|
+
readonly projectId?: string;
|
|
13
|
+
readonly fields: readonly string[];
|
|
14
|
+
readonly all: boolean;
|
|
15
|
+
readonly wait: boolean;
|
|
16
|
+
readonly waitFor?: WaitForOptions;
|
|
17
|
+
readonly json: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const FIELDS_HINT = {
|
|
21
|
+
description: "Name fields of the uploaded rows, for example label split, or pass --all; toloka dataset upload prints them.",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const FIELD_NAME = /^[a-z]([a-z0-9_]{0,126}[a-z0-9])?$/u;
|
|
25
|
+
|
|
26
|
+
const FIELD_NAME_RULE =
|
|
27
|
+
"a dataset field name takes lowercase letters, numbers and underscores, starts with a letter, ends with a letter or number, and has at most 128 characters";
|
|
28
|
+
|
|
29
|
+
export async function addDatasetFields(
|
|
30
|
+
client: PotatoClient,
|
|
31
|
+
options: AddDatasetFieldsOptions,
|
|
32
|
+
): Promise<{ readonly output: string }> {
|
|
33
|
+
assertCli(!(options.all && options.fields.length > 0), "Name the fields or pass --all, not both.", { hint: FIELDS_HINT });
|
|
34
|
+
assertCli(options.all || options.fields.length > 0, "Name the fields of the uploaded rows to add, or pass --all.", {
|
|
35
|
+
hint: FIELDS_HINT,
|
|
36
|
+
});
|
|
37
|
+
const dataset = await resolveLiveDataset(client, options, false);
|
|
38
|
+
const datasetRef = dataset.nameIsUnique ? dataset.name : dataset.datasetId;
|
|
39
|
+
const existing = new Set<string>();
|
|
40
|
+
for await (const field of client.datasets.listAllFields({ datasetId: dataset.datasetId })) existing.add(field.name);
|
|
41
|
+
const candidates = options.all
|
|
42
|
+
? (await uploadedRowFieldNames(client, dataset.datasetId)).filter((name) => !existing.has(name))
|
|
43
|
+
: [...options.fields];
|
|
44
|
+
const unnamable = candidates.filter((name) => !FIELD_NAME.test(name));
|
|
45
|
+
assertCli(options.all || unnamable.length === 0, `${unnamable.join(", ")} cannot be a dataset field name: ${FIELD_NAME_RULE}.`, {
|
|
46
|
+
hint: { description: "Rename the field in the file before uploading it, or pick fields with names a dataset field can take." },
|
|
47
|
+
});
|
|
48
|
+
const names = candidates.filter((name) => FIELD_NAME.test(name));
|
|
49
|
+
const skipped = unnamable.length === 0 ? [] : [`Skipped ${listedNames(unnamable)}: ${FIELD_NAME_RULE}.`];
|
|
50
|
+
|
|
51
|
+
if (names.length === 0) {
|
|
52
|
+
return {
|
|
53
|
+
output: [`Every field of the rows uploaded to ${dataset.name} that can be a dataset field is one already.`, ...skipped].join(
|
|
54
|
+
"\n",
|
|
55
|
+
),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const created = await client.datasets.createFieldsFromUploadedRows({ datasetId: dataset.datasetId, names });
|
|
60
|
+
const fields: DatasetField[] = options.wait
|
|
61
|
+
? (await waitForFilledFields(client, { datasetId: dataset.datasetId, names, waitFor: options.waitFor })).filter(
|
|
62
|
+
(field) => names.includes(field.name),
|
|
63
|
+
)
|
|
64
|
+
: created;
|
|
65
|
+
|
|
66
|
+
if (options.json) return { output: JSON.stringify(fields, null, 2) };
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
output: [
|
|
70
|
+
formatTable([
|
|
71
|
+
["FIELD", "ID", "TYPE"],
|
|
72
|
+
...fields.map((field) => [field.name, field.id, summarizeJsonSchema(field.jsonSchema)]),
|
|
73
|
+
]),
|
|
74
|
+
options.wait
|
|
75
|
+
? `Copied the values of ${listedNames(names)} from the uploaded rows; later uploads fill them too.`
|
|
76
|
+
: `Copying the values of ${listedNames(names)} from the uploaded rows; check with toloka dataset columns ${datasetRef} --json.`,
|
|
77
|
+
...skipped,
|
|
78
|
+
].join("\n"),
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/cli/filters.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PotatoClient } from "../client";
|
|
2
|
-
import { assertCli } from "./cli-error";
|
|
2
|
+
import { assertCli, REPORT_HINT } from "./cli-error";
|
|
3
|
+
import { uploadedFieldHint } from "./uploaded-fields";
|
|
3
4
|
|
|
4
5
|
export type FieldValueFilter = {
|
|
5
6
|
fieldValues: {
|
|
@@ -32,19 +33,27 @@ const splitFilter = (filter: string): { fieldName: string; rawValue: string } =>
|
|
|
32
33
|
|
|
33
34
|
export const resolveFieldValueFilters = async (
|
|
34
35
|
client: Pick<PotatoClient, "datasets">,
|
|
35
|
-
datasetId: string,
|
|
36
|
+
dataset: { readonly datasetId: string; readonly name: string; readonly nameIsUnique: boolean },
|
|
36
37
|
filters: readonly string[],
|
|
37
38
|
): Promise<FieldValueFilter> => {
|
|
39
|
+
const { datasetId } = dataset;
|
|
40
|
+
const datasetRef = dataset.nameIsUnique ? dataset.name : datasetId;
|
|
38
41
|
const parsed = filters.map(splitFilter);
|
|
39
42
|
const fieldIdsByName = new Map<string, string[]>();
|
|
40
43
|
for await (const field of client.datasets.listAllFields({ datasetId })) {
|
|
41
44
|
fieldIdsByName.set(field.name, [...(fieldIdsByName.get(field.name) ?? []), field.id]);
|
|
42
45
|
}
|
|
46
|
+
const unresolved = parsed.find(({ fieldName }) => fieldIdsByName.get(fieldName)?.length !== 1)?.fieldName;
|
|
47
|
+
const uploadedHint =
|
|
48
|
+
unresolved === undefined || fieldIdsByName.has(unresolved)
|
|
49
|
+
? undefined
|
|
50
|
+
: await uploadedFieldHint(client, { datasetId, datasetRef, name: unresolved });
|
|
51
|
+
assertCli(unresolved === undefined, `Dataset field ${unresolved} was not found or is ambiguous.`, {
|
|
52
|
+
hint: uploadedHint ?? { description: `Run toloka dataset columns ${datasetRef} to see the column names.` },
|
|
53
|
+
});
|
|
43
54
|
const fieldValues = parsed.map(({ fieldName, rawValue }) => {
|
|
44
|
-
const [fieldId
|
|
45
|
-
assertCli(fieldId
|
|
46
|
-
hint: { description: "Run toloka dataset columns <dataset> to see the column names." },
|
|
47
|
-
});
|
|
55
|
+
const [fieldId] = fieldIdsByName.get(fieldName) ?? [];
|
|
56
|
+
assertCli(fieldId, `Dataset field ${fieldName} has no resolved ID.`, { hint: REPORT_HINT });
|
|
48
57
|
const filter: FieldValueFilter["fieldValues"][number] = {
|
|
49
58
|
fieldId,
|
|
50
59
|
jsonPath: "$",
|
package/src/cli/slices.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DatasetSlice, PotatoClient } from "../client";
|
|
1
|
+
import type { DatasetField, DatasetSlice, PotatoClient } from "../client";
|
|
2
2
|
import { PotatoApiError } from "../errors";
|
|
3
3
|
import type { WaitForOptions } from "../wait-for";
|
|
4
4
|
import { assertCli } from "./cli-error";
|
|
@@ -76,6 +76,19 @@ export const waitUntilDone = async <Resource>(
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
+
export const waitForFilledFields = (
|
|
80
|
+
client: Pick<PotatoClient, "waitFor">,
|
|
81
|
+
{
|
|
82
|
+
datasetId,
|
|
83
|
+
names,
|
|
84
|
+
waitFor,
|
|
85
|
+
}: { readonly datasetId: string; readonly names?: readonly string[]; readonly waitFor?: WaitForOptions },
|
|
86
|
+
): Promise<DatasetField[]> =>
|
|
87
|
+
waitUntilDone(
|
|
88
|
+
() => client.waitFor({ type: "datasetFields", datasetId, ...(names === undefined ? {} : { names }) }, waitFor ?? {}),
|
|
89
|
+
{ what: `the fields of dataset ${datasetId} to finish filling`, checkCommand: `dataset columns ${datasetId} --json` },
|
|
90
|
+
);
|
|
91
|
+
|
|
79
92
|
const render = async (client: SliceApi, slice: DatasetSlice, json: boolean): Promise<DatasetSliceCommandResult> => ({
|
|
80
93
|
output: json ? JSON.stringify(slice, null, 2) : formatSlice(slice, await fieldNamesOf(client, slice.datasetId)),
|
|
81
94
|
});
|
|
@@ -91,6 +104,7 @@ export async function createDatasetSlice(
|
|
|
91
104
|
);
|
|
92
105
|
const dataset = await resolveLiveDataset(client, options, false);
|
|
93
106
|
const frozenColumns = options.freeze && [...options.freeze];
|
|
107
|
+
if (options.wait) await waitForFilledFields(client, { datasetId: dataset.datasetId, waitFor: options.waitFor });
|
|
94
108
|
const created = await client.datasets.createSlice(
|
|
95
109
|
options.from === undefined
|
|
96
110
|
? {
|
|
@@ -98,7 +112,7 @@ export async function createDatasetSlice(
|
|
|
98
112
|
name: options.name,
|
|
99
113
|
...(options.filters === undefined
|
|
100
114
|
? {}
|
|
101
|
-
: { itemFilter: await resolveFieldValueFilters(client, dataset
|
|
115
|
+
: { itemFilter: await resolveFieldValueFilters(client, dataset, options.filters) }),
|
|
102
116
|
frozenColumns,
|
|
103
117
|
}
|
|
104
118
|
: { datasetId: dataset.datasetId, name: options.name, fromSlice: options.from, frozenColumns },
|
package/src/cli/splits.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { assertCli } from "./cli-error";
|
|
|
4
4
|
import type { SplitPartSpec } from "./context";
|
|
5
5
|
import { resolveLiveDataset } from "./data";
|
|
6
6
|
import { resolveFieldValueFilters } from "./filters";
|
|
7
|
-
import { fieldNamesOf, formatCount, waitUntilDone } from "./slices";
|
|
7
|
+
import { fieldNamesOf, formatCount, waitForFilledFields, waitUntilDone } from "./slices";
|
|
8
8
|
import { formatTable } from "./table";
|
|
9
|
+
import { uploadedFieldHint } from "./uploaded-fields";
|
|
9
10
|
|
|
10
11
|
type SplitApi = Pick<PotatoClient, "datasets" | "waitFor">;
|
|
11
12
|
|
|
@@ -30,6 +31,8 @@ export type SplitDatasetOptions = {
|
|
|
30
31
|
readonly json: boolean;
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
export const DEFAULT_SPLIT_COLUMN = "split";
|
|
35
|
+
|
|
33
36
|
const formatPercent = (share: number): string => `${Math.round(share * 100)}%`;
|
|
34
37
|
|
|
35
38
|
const presetParts = (ratios: SplitRatios, validateOnTest: boolean): SplitPartSpec[] => {
|
|
@@ -127,21 +130,73 @@ export const waitForSplitColumn = async (
|
|
|
127
130
|
return done;
|
|
128
131
|
};
|
|
129
132
|
|
|
133
|
+
const listFields = async (client: Pick<PotatoClient, "datasets">, datasetId: string): Promise<DatasetField[]> => {
|
|
134
|
+
const fields: DatasetField[] = [];
|
|
135
|
+
for await (const field of client.datasets.listAllFields({ datasetId })) fields.push(field);
|
|
136
|
+
return fields;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const assertFieldsToReadExist = async (
|
|
140
|
+
client: Pick<PotatoClient, "datasets">,
|
|
141
|
+
{
|
|
142
|
+
dataset,
|
|
143
|
+
fields,
|
|
144
|
+
column,
|
|
145
|
+
balanceBy,
|
|
146
|
+
}: {
|
|
147
|
+
dataset: { readonly datasetId: string; readonly name: string; readonly nameIsUnique: boolean };
|
|
148
|
+
fields: readonly DatasetField[];
|
|
149
|
+
column: string;
|
|
150
|
+
balanceBy: string | undefined;
|
|
151
|
+
},
|
|
152
|
+
): Promise<void> => {
|
|
153
|
+
const datasetRef = dataset.nameIsUnique ? dataset.name : dataset.datasetId;
|
|
154
|
+
const isField = (name: string) => fields.some((field) => field.name === name || field.id === name);
|
|
155
|
+
if (!isField(column)) {
|
|
156
|
+
const uploaded = await uploadedFieldHint(client, { datasetId: dataset.datasetId, datasetRef, name: column });
|
|
157
|
+
assertCli(
|
|
158
|
+
uploaded === undefined,
|
|
159
|
+
`Dataset ${dataset.name} has no field ${column}, but its uploaded rows have one.`,
|
|
160
|
+
{
|
|
161
|
+
hint: {
|
|
162
|
+
description: `Add it as a dataset field with toloka dataset fields add ${datasetRef} ${column}, so the split takes its values as parts, or pass another --column for a new split column.`,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (balanceBy !== undefined && !isField(balanceBy)) {
|
|
168
|
+
const uploaded = await uploadedFieldHint(client, { datasetId: dataset.datasetId, datasetRef, name: balanceBy });
|
|
169
|
+
assertCli(false, `Dataset ${dataset.name} has no field ${balanceBy} to balance by.`, {
|
|
170
|
+
hint: uploaded ?? { description: `List its fields with toloka dataset columns ${datasetRef}.` },
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
130
175
|
export async function splitDataset(client: PotatoClient, options: SplitDatasetOptions): Promise<DatasetSplitCommandResult> {
|
|
131
176
|
assertCli(options.slice === undefined || options.filters === undefined, "Pass --slice or --filter, not both.", {
|
|
132
177
|
hint: { description: "--slice fills the rows of a slice; --filter fills the rows that match now." },
|
|
133
178
|
});
|
|
179
|
+
const parts = requestedParts(options);
|
|
134
180
|
const dataset = await resolveLiveDataset(client, options, false);
|
|
181
|
+
const fields = options.wait
|
|
182
|
+
? await waitForFilledFields(client, { datasetId: dataset.datasetId, waitFor: options.waitFor })
|
|
183
|
+
: await listFields(client, dataset.datasetId);
|
|
184
|
+
await assertFieldsToReadExist(client, {
|
|
185
|
+
dataset,
|
|
186
|
+
fields,
|
|
187
|
+
column: options.column ?? DEFAULT_SPLIT_COLUMN,
|
|
188
|
+
balanceBy: options.balanceBy,
|
|
189
|
+
});
|
|
135
190
|
const field = await client.datasets.split({
|
|
136
191
|
datasetId: dataset.datasetId,
|
|
137
192
|
column: options.column,
|
|
138
|
-
parts
|
|
193
|
+
parts,
|
|
139
194
|
balanceBy: options.balanceBy,
|
|
140
195
|
seed: options.seed,
|
|
141
196
|
...(options.slice === undefined ? {} : { slice: options.slice }),
|
|
142
197
|
...(options.filters === undefined
|
|
143
198
|
? {}
|
|
144
|
-
: { itemFilter: await resolveFieldValueFilters(client, dataset
|
|
199
|
+
: { itemFilter: await resolveFieldValueFilters(client, dataset, options.filters) }),
|
|
145
200
|
});
|
|
146
201
|
const done = options.wait ? await waitForSplitColumn(client, { datasetId: dataset.datasetId, field, waitFor: options.waitFor }) : field;
|
|
147
202
|
return {
|
package/src/cli/trainings.ts
CHANGED
|
@@ -18,8 +18,9 @@ import type { BaseModelId, TrainingConfigurationInput } from "../../openapi/gen/
|
|
|
18
18
|
import { assertCli, RETRY_OR_REPORT_HINT, TolokaCliError } from "./cli-error";
|
|
19
19
|
import type { CliErrorHint } from "./cli-error";
|
|
20
20
|
import { resolveLiveDataset } from "./data";
|
|
21
|
-
import { formatCount, waitUntilDone } from "./slices";
|
|
22
|
-
import { waitForSplitColumn } from "./splits";
|
|
21
|
+
import { formatCount, waitForFilledFields, waitUntilDone } from "./slices";
|
|
22
|
+
import { DEFAULT_SPLIT_COLUMN, waitForSplitColumn } from "./splits";
|
|
23
|
+
import { uploadedFieldHint } from "./uploaded-fields";
|
|
23
24
|
import { formatTable, type TableIo as TrainingIo, writeOutput } from "./table";
|
|
24
25
|
|
|
25
26
|
const ESTIMATE_FILE_HINT: CliErrorHint = {
|
|
@@ -161,10 +162,14 @@ const formatCostEstimate = (estimate: TrainingCostEstimate): string => {
|
|
|
161
162
|
].join("\n");
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
const leftOutAsTest = estimate.inputs.training.heldOutRows ?? 0;
|
|
166
|
+
|
|
164
167
|
return formatTable([
|
|
165
168
|
["Training", estimate.trainingId],
|
|
166
169
|
["Training rows", String(estimate.inputs.training.report?.validCount ?? 0)],
|
|
167
|
-
|
|
170
|
+
...(leftOutAsTest === 0
|
|
171
|
+
? []
|
|
172
|
+
: [["Left out as test", `${formatCount(leftOutAsTest)} train rows that a split column now marks as test`]]),
|
|
168
173
|
["Training tokens", String(estimate.inputs.training.report?.tokens.total ?? 0)],
|
|
169
174
|
["Maximum GPU seconds", String(estimate.budget.maximumGpuSeconds)],
|
|
170
175
|
["Maximum charge", `$${estimate.maximumCharge} ${estimate.currency}`],
|
|
@@ -295,10 +300,14 @@ const splitColumnId = async (
|
|
|
295
300
|
|
|
296
301
|
const fillTrainingSplitColumn = async (
|
|
297
302
|
client: TrainingDraftApi,
|
|
298
|
-
{
|
|
303
|
+
{
|
|
304
|
+
datasetId,
|
|
305
|
+
datasetName,
|
|
306
|
+
datasetRef,
|
|
307
|
+
splitColumn,
|
|
308
|
+
}: { datasetId: string; datasetName: string; datasetRef: string; splitColumn: string | undefined },
|
|
299
309
|
): Promise<DatasetField> => {
|
|
300
|
-
const fields:
|
|
301
|
-
for await (const field of client.datasets.listAllFields({ datasetId })) fields.push(field);
|
|
310
|
+
const fields = await waitForFilledFields(client, { datasetId, waitFor: AUTO_WAIT });
|
|
302
311
|
const splitColumns = fields.filter((field) => field.split !== null);
|
|
303
312
|
assertCli(
|
|
304
313
|
splitColumn !== undefined || splitColumns.length <= 1,
|
|
@@ -312,9 +321,23 @@ const fillTrainingSplitColumn = async (
|
|
|
312
321
|
|
|
313
322
|
if (existing?.split?.recipe === null) return existing;
|
|
314
323
|
|
|
324
|
+
const column = existing?.name ?? splitColumn ?? DEFAULT_SPLIT_COLUMN;
|
|
325
|
+
if (existing === undefined) {
|
|
326
|
+
const uploaded = await uploadedFieldHint(client, { datasetId, datasetRef, name: column });
|
|
327
|
+
assertCli(
|
|
328
|
+
uploaded === undefined,
|
|
329
|
+
`Dataset ${datasetName} has no field ${column}, but its uploaded rows have one.`,
|
|
330
|
+
{
|
|
331
|
+
hint: {
|
|
332
|
+
description: `Add it as a dataset field with toloka dataset fields add ${datasetRef} ${column} to train on its parts, or pass --split-column <name> to sample a new split column.`,
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
315
338
|
return waitForSplitColumn(client, {
|
|
316
339
|
datasetId,
|
|
317
|
-
field: await client.datasets.split({ datasetId, column
|
|
340
|
+
field: await client.datasets.split({ datasetId, column }),
|
|
318
341
|
waitFor: AUTO_WAIT,
|
|
319
342
|
});
|
|
320
343
|
};
|
|
@@ -348,6 +371,7 @@ const splitAndSlice = async (
|
|
|
348
371
|
const column = await fillTrainingSplitColumn(client, {
|
|
349
372
|
datasetId: dataset.datasetId,
|
|
350
373
|
datasetName: dataset.name,
|
|
374
|
+
datasetRef: dataset.nameIsUnique ? dataset.name : dataset.datasetId,
|
|
351
375
|
splitColumn,
|
|
352
376
|
});
|
|
353
377
|
const slice = await freezeTrainingSlice(client, { datasetId: dataset.datasetId, name: trainingName });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PotatoClient } from "../client";
|
|
2
|
+
import type { CliErrorHint } from "./cli-error";
|
|
3
|
+
|
|
4
|
+
export const uploadedRowFieldNames = async (
|
|
5
|
+
client: Pick<PotatoClient, "datasets">,
|
|
6
|
+
datasetId: string,
|
|
7
|
+
): Promise<string[]> =>
|
|
8
|
+
(await client.datasets.getInferredFields({ datasetId })).fields
|
|
9
|
+
.filter((field) => field.depth === 1)
|
|
10
|
+
.map((field) => field.name);
|
|
11
|
+
|
|
12
|
+
export const uploadedFieldHint = async (
|
|
13
|
+
client: Pick<PotatoClient, "datasets">,
|
|
14
|
+
{ datasetId, datasetRef, name }: { readonly datasetId: string; readonly datasetRef: string; readonly name: string },
|
|
15
|
+
): Promise<CliErrorHint | undefined> =>
|
|
16
|
+
(await uploadedRowFieldNames(client, datasetId)).includes(name)
|
|
17
|
+
? {
|
|
18
|
+
description: `${name} is a field of the uploaded rows but not a dataset field yet. Add it with toloka dataset fields add ${datasetRef} ${name}, then run this again.`,
|
|
19
|
+
}
|
|
20
|
+
: undefined;
|