@toloka-platform/sdk 0.11.2 → 0.11.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.
Files changed (76) hide show
  1. package/dist/cli/toloka.js +158 -154
  2. package/dist/cli/toloka.js.jsc +0 -0
  3. package/dist/cli/toloka.js.jsc.sha256 +1 -1
  4. package/dist/cli/toloka.js.sha256 +1 -1
  5. package/docs/authoring.md +6 -4
  6. package/docs/cli.md +30 -11
  7. package/docs/limitations.md +2 -2
  8. package/docs/testing.md +4 -2
  9. package/examples/README.md +11 -0
  10. package/examples/app-crowdtesting/README.md +41 -0
  11. package/examples/app-crowdtesting/fields.ts +6 -0
  12. package/examples/app-crowdtesting/nodes/app-flow-test/node.ts +381 -0
  13. package/examples/app-crowdtesting/nodes/collection/node.ts +8 -0
  14. package/examples/app-crowdtesting/pipeline.browser.integration.spec.ts +77 -0
  15. package/examples/app-crowdtesting/pipeline.ts +17 -0
  16. package/examples/egocentric-video-collection/README.md +35 -0
  17. package/examples/egocentric-video-collection/fields.ts +24 -0
  18. package/examples/egocentric-video-collection/nodes/collection/node.ts +5 -0
  19. package/examples/egocentric-video-collection/nodes/record-activity/node.ts +760 -0
  20. package/examples/egocentric-video-collection/pipeline.browser.integration.spec.ts +65 -0
  21. package/examples/egocentric-video-collection/pipeline.ts +17 -0
  22. package/examples/product-moderation/README.md +49 -0
  23. package/examples/product-moderation/fields.ts +37 -0
  24. package/examples/product-moderation/nodes/collection/node.ts +16 -0
  25. package/examples/product-moderation/nodes/moderate-product/instruction.ts +77 -0
  26. package/examples/product-moderation/nodes/moderate-product/node.ts +368 -0
  27. package/examples/product-moderation/pipeline.browser.integration.spec.ts +48 -0
  28. package/examples/product-moderation/pipeline.ts +33 -0
  29. package/examples/video-pair-rating/fields.ts +16 -4
  30. package/examples/video-pair-rating/nodes/adjudicate/node.ts +78 -3
  31. package/examples/video-pair-rating/nodes/adjudicate_op/node.ts +28 -3
  32. package/examples/video-pair-rating/nodes/overall_preference_pick/node.ts +28 -3
  33. package/examples/video-pair-rating/nodes/rate_pair/node.ts +65 -2
  34. package/openapi/gen/api.ts +22 -0
  35. package/openapi/gen/models/generationBudget.ts +8 -0
  36. package/openapi/gen/models/generationBudgetReasoningEffort.ts +2 -0
  37. package/openapi/gen/models/generationModel.ts +12 -0
  38. package/openapi/gen/models/generationModelDefaultReasoningEffort.ts +2 -0
  39. package/openapi/gen/models/generationModelId.ts +2 -0
  40. package/openapi/gen/models/index.ts +8 -0
  41. package/openapi/gen/models/invalidFieldSchema.ts +6 -0
  42. package/openapi/gen/models/listGenerationModels200.ts +7 -0
  43. package/openapi/gen/models/listGenerationModels200DefaultGenerationModelId.ts +2 -0
  44. package/openapi/gen/models/pipelineGraphSnapshot.ts +2 -0
  45. package/package.json +2 -2
  46. package/skills/catalog.json +16 -12
  47. package/skills/toloka-build/SKILL.md +44 -58
  48. package/skills/toloka-build/references/human-work.md +156 -0
  49. package/skills/toloka-build/references/local-setup.md +136 -0
  50. package/skills/toloka-build/references/node-checks.md +11 -0
  51. package/skills/toloka-build/references/node-semantics.md +22 -213
  52. package/skills/toloka-build/references/task-ui.md +61 -0
  53. package/skills/toloka-build/references/workflow-and-cli.md +34 -228
  54. package/skills/toloka-fine-tuning/SKILL.md +1 -1
  55. package/skills/toloka-get-started/SKILL.md +1 -1
  56. package/skills/toloka-gisting/SKILL.md +1 -1
  57. package/src/authoring/index.ts +26 -7
  58. package/src/checks/task-ui-typecheck.ts +3 -3
  59. package/src/cli/analytics.ts +1 -0
  60. package/src/cli/check.ts +83 -12
  61. package/src/cli/commands/generation-model.ts +18 -0
  62. package/src/cli/commands/pipeline.ts +1 -1
  63. package/src/cli/commands/source.ts +14 -3
  64. package/src/cli/data.ts +5 -2
  65. package/src/cli/format-error.ts +3 -0
  66. package/src/cli/generation-models.ts +46 -0
  67. package/src/cli/program.ts +2 -0
  68. package/src/cli/project.ts +288 -138
  69. package/src/cli/trainings.ts +12 -0
  70. package/src/cli/ui.ts +3 -1
  71. package/src/client.ts +5 -0
  72. package/src/graph-rules.json +11 -0
  73. package/src/importer.ts +46 -40
  74. package/src/index.ts +4 -0
  75. package/src/transport.ts +23 -5
  76. package/src/validate.ts +183 -0
package/src/cli/check.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  type TestEvidence,
24
24
  type TestEvidenceKind,
25
25
  } from "../testing/test-evidence";
26
+ import { platformRuleIssues } from "../validate";
26
27
  import { cliRelaunchCommand } from "./cli-entry";
27
28
  import { bunSubprocessEnvironment } from "./native-runtime";
28
29
  import { CREDENTIAL_ENVIRONMENT_NAMES } from "./environment";
@@ -30,7 +31,13 @@ import { loadProjectSource, type ProjectSource } from "./source";
30
31
  import { assertCli, REPORT_HINT, TolokaCliError } from "./cli-error";
31
32
  import type { TolokaCliErrorOptions } from "./cli-error";
32
33
 
33
- export type CheckStatus = "passed" | "failed" | "missing" | "partial" | "infrastructure_error";
34
+ export type CheckStatus =
35
+ | "passed"
36
+ | "failed"
37
+ | "missing"
38
+ | "partial"
39
+ | "warning"
40
+ | "infrastructure_error";
34
41
  export type CheckKind =
35
42
  | "source"
36
43
  | "project-types"
@@ -38,6 +45,7 @@ export type CheckKind =
38
45
  | "code-test"
39
46
  | "ui-test"
40
47
  | "pipeline-flow"
48
+ | "platform-rules"
41
49
  | "test-runner";
42
50
 
43
51
  export type CheckDiagnostic = {
@@ -504,6 +512,39 @@ const uiTypeResults = async (
504
512
  return results;
505
513
  };
506
514
 
515
+ const platformRuleResults = (
516
+ source: ProjectSource,
517
+ selected: readonly CheckInventoryPipeline[],
518
+ ): CheckResult[] => {
519
+ const selectedDirectories = new Set(selected.map((pipeline) => pipeline.sourceDirectory));
520
+ return source.pipelines
521
+ .filter((pipeline) => selectedDirectories.has(pipeline.sourceDirectory))
522
+ .map((pipeline) => {
523
+ const issues = platformRuleIssues(pipeline.definition.graph);
524
+ return {
525
+ scope: "pipeline",
526
+ kind: "platform-rules",
527
+ status: issues.length === 0 ? "passed" : "warning",
528
+ sourceDirectory: pipeline.sourceDirectory,
529
+ pipelineFingerprint: pipelineFingerprint(pipeline.definition),
530
+ ...boundedDiagnostics(
531
+ issues.length === 0
532
+ ? []
533
+ : [
534
+ ...issues.map((issue) => ({
535
+ message: `node ${issue.node}: ${issue.reason}`,
536
+ severity: "warning" as const,
537
+ })),
538
+ {
539
+ message: "toloka apply refuses to write this pipeline until these are fixed.",
540
+ severity: "warning" as const,
541
+ },
542
+ ],
543
+ ),
544
+ };
545
+ });
546
+ };
547
+
507
548
  const evidenceStatus = (
508
549
  evidence: readonly TestEvidence[],
509
550
  ): Pick<CheckResult, "status" | "diagnostics" | "truncatedDiagnosticCount"> => {
@@ -536,9 +577,9 @@ const evidenceStatus = (
536
577
  status: "partial",
537
578
  diagnostics: [
538
579
  {
539
- message: `Hardware-backed fields not fully covered: ${[
580
+ message: `Hardware-backed fields not covered by a real device: ${[
540
581
  ...new Set(passing.flatMap((entry) => entry.untestedHardwareFields ?? [])),
541
- ].join(", ")}`,
582
+ ].join(", ")}. This does not block apply; try the task on a real device before launching a run.`,
542
583
  },
543
584
  ],
544
585
  };
@@ -559,6 +600,38 @@ const evidenceFor = (
559
600
  (nodeId === undefined || entry.nodeId === nodeId),
560
601
  );
561
602
 
603
+ const typedTestUsage: Record<TestEvidenceKind, { call: string; counts: string }> = {
604
+ code: { call: "test.code(pipeline, node, name, callback)", counts: "runs the node" },
605
+ ui: { call: "test.ui(pipeline, node, name, callback)", counts: "submits the rendered task" },
606
+ pipeline: { call: "test.pipeline(pipeline, name, callback)", counts: "runs the connected nodes" },
607
+ };
608
+
609
+ const coverageStatus = (
610
+ evidence: readonly TestEvidence[],
611
+ inventory: readonly CheckInventoryPipeline[],
612
+ pipeline: CheckInventoryPipeline,
613
+ kind: TestEvidenceKind,
614
+ nodeId?: string,
615
+ ): Pick<CheckResult, "status" | "diagnostics" | "truncatedDiagnosticCount"> => {
616
+ const status = evidenceStatus(evidenceFor(evidence, pipeline, kind, nodeId));
617
+ if (status.status !== "missing") return status;
618
+ const target = nodeId === undefined ? "this pipeline" : "this node";
619
+ const { call, counts } = typedTestUsage[kind];
620
+ const knownFingerprints = new Set(inventory.map((candidate) => candidate.pipelineFingerprint));
621
+ const strayTests = evidence.filter(
622
+ (entry) =>
623
+ entry.kind === kind &&
624
+ entry.status === "passed" &&
625
+ entry.nodeId === nodeId &&
626
+ !knownFingerprints.has(entry.pipelineFingerprint),
627
+ );
628
+ const message =
629
+ strayTests.length > 0
630
+ ? `Passing ${kind} tests for ${target} (${strayTests.length}) ran against a different pipeline definition than the one ${pipeline.sourceDirectory}/pipeline.ts exports and do not count. Pass that exported pipeline to the test instead of building one inside the test.`
631
+ : `No passing ${kind} test covers ${target}. Add ${call} with the pipeline ${pipeline.sourceDirectory}/pipeline.ts exports; it counts once it ${counts}, makes an assertion, and passes.`;
632
+ return { status: "missing", diagnostics: [{ message }] };
633
+ };
634
+
562
635
  const coverageResults = (
563
636
  inventory: readonly CheckInventoryPipeline[],
564
637
  evidence: readonly TestEvidence[],
@@ -576,7 +649,7 @@ const coverageResults = (
576
649
  sourceDirectory: pipeline.sourceDirectory,
577
650
  pipelineFingerprint: pipeline.pipelineFingerprint,
578
651
  nodeId: node.id,
579
- ...evidenceStatus(evidenceFor(evidence, pipeline, evidenceKind, node.id)),
652
+ ...coverageStatus(evidence, inventory, pipeline, evidenceKind, node.id),
580
653
  });
581
654
  }
582
655
  if (nodeSelector === undefined) {
@@ -585,7 +658,7 @@ const coverageResults = (
585
658
  kind: "pipeline-flow",
586
659
  sourceDirectory: pipeline.sourceDirectory,
587
660
  pipelineFingerprint: pipeline.pipelineFingerprint,
588
- ...evidenceStatus(evidenceFor(evidence, pipeline, "pipeline")),
661
+ ...coverageStatus(evidence, inventory, pipeline, "pipeline"),
589
662
  });
590
663
  }
591
664
  return result;
@@ -596,6 +669,7 @@ const statusLabel: Record<CheckStatus, string> = {
596
669
  failed: "FAIL",
597
670
  missing: "MISSING",
598
671
  partial: "PARTIAL",
672
+ warning: "WARN",
599
673
  infrastructure_error: "ERROR",
600
674
  };
601
675
 
@@ -629,13 +703,7 @@ export const formatCheckResults = (results: readonly CheckResult[]): string =>
629
703
 
630
704
  const exitCodeForResults = (results: readonly CheckResult[]): 0 | 1 | 2 => {
631
705
  if (results.some((result) => result.status === "infrastructure_error")) return 2;
632
- if (
633
- results.some(
634
- (result) =>
635
- result.status === "failed" || result.status === "missing" || result.status === "partial",
636
- )
637
- )
638
- return 1;
706
+ if (results.some((result) => result.status === "failed" || result.status === "missing")) return 1;
639
707
  return 0;
640
708
  };
641
709
 
@@ -723,6 +791,7 @@ export async function checkProject(
723
791
  ...uiResults,
724
792
  projectToolResult("test-runner", vitestRun),
725
793
  ...coverageResults(selected.pipelines, evidence, selected.nodeId),
794
+ ...(selected.nodeId === undefined ? platformRuleResults(source, selected.pipelines) : []),
726
795
  ]);
727
796
  const exitCode = exitCodeForResults(results);
728
797
  return {
@@ -787,6 +856,7 @@ const isCheckKind = (value: unknown): value is CheckKind =>
787
856
  value === "code-test" ||
788
857
  value === "ui-test" ||
789
858
  value === "pipeline-flow" ||
859
+ value === "platform-rules" ||
790
860
  value === "test-runner";
791
861
 
792
862
  const isCheckStatus = (value: unknown): value is CheckStatus =>
@@ -794,6 +864,7 @@ const isCheckStatus = (value: unknown): value is CheckStatus =>
794
864
  value === "failed" ||
795
865
  value === "missing" ||
796
866
  value === "partial" ||
867
+ value === "warning" ||
797
868
  value === "infrastructure_error";
798
869
 
799
870
  const parseFreshCheckDiagnostic = (value: unknown): CheckDiagnostic => {
@@ -0,0 +1,18 @@
1
+ import type { Command } from "commander";
2
+ import { resolveContext, type CommandHost } from "../context";
3
+ import { listGenerationModels } from "../generation-models";
4
+
5
+ export const registerGenerationModelCommands = (program: Command, host: CommandHost): void => {
6
+ const { context, globals } = host;
7
+
8
+ program
9
+ .command("generation-model")
10
+ .description("inspect the models synthetic generation nodes can use")
11
+ .command("list")
12
+ .option("--json", "print JSON", false)
13
+ .description("list generation models with reasoning efforts and maxCostPerAttempt bounds")
14
+ .action(async (options: { json: boolean }) => {
15
+ const { client } = await resolveContext(globals(), context);
16
+ await listGenerationModels(client, { json: options.json }, context.io);
17
+ });
18
+ };
@@ -38,7 +38,7 @@ export const registerPipelineCommands = (program: Command, host: CommandHost): v
38
38
  .argument("[name]", pipelineNameHelp)
39
39
  .option("--wait", "wait for the run to reach a terminal state", false)
40
40
  .option("--timeout <ms>", "how long to wait", positiveInteger)
41
- .description("start a run on a deployed pipeline")
41
+ .description("start a run on an applied pipeline")
42
42
  .action(async (name: string | undefined, options: { wait: boolean; timeout?: number }) => {
43
43
  const { client, state, projectId } = await resolveContext(globals(), context);
44
44
  assertProjectSelected(projectId);
@@ -14,6 +14,9 @@ import { applyProject, importProject, planProject, validateProject } from "../pr
14
14
  import { assertProjectSelected, resolveProjectContext } from "../project-context";
15
15
  import { readTolokaState } from "../../state";
16
16
  import { assertCli, REPORT_HINT } from "../cli-error";
17
+ import { writeOutput } from "../table";
18
+
19
+ const VALIDATE_WAIT = { timeoutMs: 5 * 60_000, pollIntervalMs: 5_000 };
17
20
 
18
21
  const runChecksInterruptibly = async (
19
22
  options: CheckProjectOptions,
@@ -88,24 +91,32 @@ export const registerSourceCommands = (program: Command, host: CommandHost): voi
88
91
 
89
92
  program
90
93
  .command("plan")
94
+ .option("--json", "print the planned changes and missing secrets as JSON", false)
91
95
  .description("show what apply would change")
92
- .action(async () => {
96
+ .action(async (options: { json: boolean }) => {
93
97
  const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
94
98
  const result = await planProject(client, {
95
99
  sourceRoot,
96
100
  ...(projectId === undefined ? {} : { projectId }),
97
101
  });
98
- context.io.stdout(result.output);
102
+ writeOutput(
103
+ context.io,
104
+ options.json,
105
+ { changes: result.changes, missingSecrets: result.missingSecrets },
106
+ result.output,
107
+ );
99
108
  });
100
109
 
101
110
  program
102
111
  .command("validate")
112
+ .option("--wait", "wait up to 5 minutes for running lints and checks to finish", false)
103
113
  .description("run platform spec validation and report matching checks")
104
- .action(async () => {
114
+ .action(async (options: { wait: boolean }) => {
105
115
  const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
106
116
  const result = await validateProject(client, {
107
117
  sourceRoot,
108
118
  ...(projectId === undefined ? {} : { projectId }),
119
+ ...(options.wait ? { wait: VALIDATE_WAIT } : {}),
109
120
  });
110
121
  context.io.stdout(result.output);
111
122
  if (!result.valid) exitCode.value = 1;
package/src/cli/data.ts CHANGED
@@ -132,7 +132,7 @@ const summarizeJsonSchema = (schema: unknown): string => {
132
132
  const formatInferredFields = (fields: ReadonlyArray<InferredField>): string => {
133
133
  if (fields.length === 0) return "No inferred fields are available yet.";
134
134
  return [
135
- "Inferred fields:",
135
+ "Types inferred from the uploaded rows (not the column schemas, which keep the constraints declared in source):",
136
136
  ...fields
137
137
  .toSorted((left, right) => left.jsonPath.localeCompare(right.jsonPath))
138
138
  .map(
@@ -459,6 +459,9 @@ export async function uploadDataset(
459
459
  };
460
460
  }
461
461
 
462
+ const INFERRED_DECLARATIONS_NOTE =
463
+ "// Inferred from the uploaded rows: enum, length, range and format constraints are not inferred, so these are not the dataset's column schemas.";
464
+
462
465
  export async function generateDatasetFieldDeclarations(
463
466
  client: PotatoClient,
464
467
  options: GenerateDatasetFieldDeclarationsOptions,
@@ -476,7 +479,7 @@ export async function generateDatasetFieldDeclarations(
476
479
  output:
477
480
  topLevelFields.length === 0
478
481
  ? `Dataset ${resolved.name} has no inferred fields yet.`
479
- : declarations,
482
+ : `${INFERRED_DECLARATIONS_NOTE}\n${declarations}`,
480
483
  };
481
484
  }
482
485
 
@@ -24,6 +24,9 @@ const apiErrorHint = (error: PotatoApiError): string | undefined => {
24
24
  if (error.status === 429) {
25
25
  return "Hint: The platform is rate limiting this key; wait a moment and retry.";
26
26
  }
27
+ if (error.status === 503 && error.body === "Tunnel unavailable; the request was not sent") {
28
+ return "Hint: The sandbox lost its connection to the platform (usually a platform restart) and it was not restored within 90 seconds; the request was not sent. Retry in a minute.";
29
+ }
27
30
  if (error.status !== undefined && error.status >= 500) {
28
31
  return "Hint: The platform failed to handle the request. Retry; if it keeps failing, write a report as the toloka-report-issue skill describes and send it with toloka report <report.md>.";
29
32
  }
@@ -0,0 +1,46 @@
1
+ import type { PotatoClient } from "../client";
2
+ import { formatTable, type TableIo, writeOutput } from "./table";
3
+
4
+ type GenerationModelApi = Pick<PotatoClient, "generationModels">;
5
+
6
+ const dollars = (amount: number): string => `$${amount.toFixed(2)}`;
7
+
8
+ export const listGenerationModels = async (
9
+ client: GenerationModelApi,
10
+ input: { json: boolean },
11
+ io: TableIo,
12
+ ): Promise<void> => {
13
+ const catalog = await client.generationModels.list();
14
+ const recommended = catalog.generationModels
15
+ .filter((model) => model.recommended)
16
+ .map((model) => model.id);
17
+ writeOutput(
18
+ io,
19
+ input.json,
20
+ catalog,
21
+ catalog.generationModels.length === 0
22
+ ? "No generation models available."
23
+ : [
24
+ formatTable([
25
+ ["ID", "NAME", "REASONING EFFORT", "TYPICAL", "MIN", "MAX"],
26
+ ...catalog.generationModels.flatMap((model) =>
27
+ model.budgets.map((budget) => [
28
+ model.id,
29
+ model.displayName,
30
+ budget.reasoningEffort === undefined
31
+ ? "-"
32
+ : budget.reasoningEffort === model.defaultReasoningEffort
33
+ ? `${budget.reasoningEffort} (default)`
34
+ : budget.reasoningEffort,
35
+ dollars(budget.typical),
36
+ dollars(budget.min),
37
+ dollars(budget.maxEstimate),
38
+ ]),
39
+ ),
40
+ ]),
41
+ "",
42
+ `Default model: ${catalog.defaultGenerationModelId}`,
43
+ ...(recommended.length === 0 ? [] : [`Recommended: ${recommended.join(", ")}`]),
44
+ ].join("\n"),
45
+ );
46
+ };
@@ -11,6 +11,7 @@ import { registerAudienceCommands } from "./commands/audience";
11
11
  import { registerAuthCommands } from "./commands/auth";
12
12
  import { registerConfigCommands } from "./commands/config";
13
13
  import { registerDatasetCommands } from "./commands/dataset";
14
+ import { registerGenerationModelCommands } from "./commands/generation-model";
14
15
  import { registerDeliveryOrderCommands } from "./commands/delivery-order";
15
16
  import { registerModelCommands } from "./commands/models";
16
17
  import { registerPipelineCommands } from "./commands/pipeline";
@@ -96,6 +97,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
96
97
  registerProjectCommands(program, host);
97
98
  registerDeliveryOrderCommands(program, host);
98
99
  registerAudienceCommands(program, host);
100
+ registerGenerationModelCommands(program, host);
99
101
  registerSourceCommands(program, host);
100
102
  registerSecretCommands(program, host);
101
103
  registerModelCommands(program, host);