runbios-sdk 0.4.0 → 0.5.0-dev.340

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.
@@ -1,11 +1,42 @@
1
1
  import { type HttpClient } from '../client.js';
2
- import type { LoopTrace, LoopTraceDeleteResponse, LoopTraceListResponse, LoopTraceListParams, LoopCaptureParams, LoopCaptureResult, LoopImportParams, LoopImportResult, LoopSignal, LoopSignalParams, LoopSignalManyItem, LoopSignalManyResult, LoopLabel, LoopLabelCount, LoopConfig, LoopConfigParams, LoopStats, LoopTracePipelinesResponse, LoopMetricsHistory, LoopMetricsHistoryParams, TrainingRuleDeleteResponse, TrainingRunListParams, TrainingRunListResponse, TrainingRunResponse, TrainingRunActionResponse, TrainingRunPromoteRequest, TrainingRunRejectRequest, TrainingRunRollbackRequest, TrainingRunCancelRequest, Evaluation, EvaluationItemListParams, EvaluationItemsResponse, Benchmark, BenchmarkCreateParams, BenchmarkHistoryParams, BenchmarkHistoryResponse, BenchmarkItemListParams, BenchmarkItemsResponse, BenchmarkListParams, BenchmarkListResponse, BenchmarkRetireParams, BenchmarkRun, BenchmarkRunItemListParams, BenchmarkRunItemsResponse, Pipeline, PipelineAttemptListParams, PipelineAttemptListResponse, PipelineListParams, PipelineListResponse, PipelineWriteRequest, PipelineMutationResponse, LoopModelsResponse, PipelineImportParams, PipelineBenchmarksResponse, PipelineBenchmarkAttachParams, PipelineBenchmarkMoveParams, VersionCompareResponse, VersionCompareParams, AttemptCompareResponse, AttemptCompareParams, LoopDataset, LoopDatasetWrite, LoopDatasetCreate, LoopDatasetListParams, LoopDatasetListResponse, LoopDatasetDeleteResponse, LoopDatasetPreview, LoopDatasetPreviewParams } from '../types.js';
2
+ import type { LoopTrace, LoopTraceDeleteResponse, LoopTraceListResponse, LoopTraceListParams, LoopCaptureParams, LoopCaptureResult, LoopImportParams, LoopImportResult, LoopSignal, LoopSignalParams, LoopSignalManyItem, LoopSignalManyResult, LoopLabel, LoopLabelCount, LoopConfig, LoopConfigParams, LoopStats, LoopTracePipelinesResponse, LoopMetricsHistory, LoopMetricsHistoryParams, TrainingRuleDeleteResponse, TrainingRunListParams, TrainingRunListResponse, TrainingRunResponse, TrainingRunActionResponse, TrainingRunPromoteRequest, TrainingRunRejectRequest, TrainingRunRollbackRequest, TrainingRunCancelRequest, Evaluation, EvaluationItemListParams, EvaluationItemsResponse, Benchmark, BenchmarkCreateParams, BenchmarkHistoryParams, BenchmarkHistoryResponse, BenchmarkQuestionsParams, BenchmarkQuestionsResponse, TrainingCurve, BenchmarkItemListParams, BenchmarkItemsResponse, BenchmarkListParams, BenchmarkListResponse, BenchmarkRetireParams, BenchmarkRun, BenchmarkRunItemListParams, BenchmarkRunItemsResponse, Pipeline, PipelineAttemptListParams, PipelineAttemptListResponse, PipelineListParams, PipelineListResponse, PipelineWriteRequest, PipelineMutationResponse, LoopModelsResponse, PipelineImportParams, PipelineBenchmarksResponse, PipelineBenchmarkAttachParams, PipelineBenchmarkMoveParams, VersionCompareResponse, VersionCompareParams, AttemptCompareResponse, AttemptCompareParams, LoopDataset, LoopDatasetWrite, LoopDatasetCreate, LoopDatasetListParams, LoopDatasetListResponse, LoopDatasetDeleteResponse, LoopDatasetPreview, LoopDatasetPreviewParams, LoopDatasetExport, LoopDatasetExportParams } from '../types.js';
3
+ import type { Inference } from './inference.js';
4
+ import { type LoopChatParams, type LoopChatResult } from './loop-chat.js';
3
5
  export declare class Loop {
4
6
  private readonly _http;
5
- /** @internal */
6
- constructor(_http: HttpClient);
7
+ private readonly _inference?;
8
+ /**
9
+ * @internal The SDK client hands Loop its Inference resource, which `chat`
10
+ * calls. A Loop made without one can do everything but `chat`.
11
+ */
12
+ constructor(_http: HttpClient, _inference?: Inference | undefined);
7
13
  /** @internal How feedback waits between tries; tests replace it so nothing sleeps. */
8
14
  _sleep: (ms: number) => Promise<void>;
15
+ /**
16
+ * Ask a model and save the conversation as a sample of a pipeline, in one
17
+ * call; the result carries the answer, its sample ID and the feedback for it.
18
+ *
19
+ * It is `client.inference.chatCompletions` with the pipeline ID in the
20
+ * `X-Loop-Pipeline` header: a normal, billed model call. `model` defaults to
21
+ * the pipeline's own model, `loop-<pipeline ID>`, which answers with the live
22
+ * version. While the pipeline has no live version (before its first one)
23
+ * nothing answers to that name, so pass `model` (the model your app uses
24
+ * now); left out then, this rejects with `LoopNoVersionError` (an
25
+ * `ApiError`) and no model answered. The pipeline ID is lowercased, as the
26
+ * platform reads it.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const reply = await client.loop.chat({
31
+ * pipeline: 'support-replies',
32
+ * messages: [{ role: 'user', content: 'Where is my order?' }],
33
+ * });
34
+ * console.log(reply.answer);
35
+ * await reply.good(); // or reply.bad()
36
+ * // await reply.correct('It ships on Monday.');
37
+ * ```
38
+ */
39
+ chat(params: LoopChatParams): Promise<LoopChatResult>;
9
40
  /**
10
41
  * Send one exchange to be captured.
11
42
  *
@@ -349,6 +380,17 @@ export declare class Loop {
349
380
  * the attempt as its pipeline's own list says it, `explanation` included.
350
381
  */
351
382
  listTrainingRuns(params?: TrainingRunListParams): Promise<TrainingRunListResponse>;
383
+ /**
384
+ * How a training went while it trained, step by step: its loss on the
385
+ * samples it trained on (`train_loss`) and on the practice questions it set
386
+ * aside (`check_loss`), at most 200 points with every practice check among
387
+ * them, and the step whose model it kept (`kept_step`). `keeps_last` is a
388
+ * training that keeps its last step rather than its best practice score.
389
+ * With nothing to chart -- no training job yet, or no practice check --
390
+ * `points` is empty and `reason` says why. 503
391
+ * `TRAINING_CURVE_UNAVAILABLE` when it could not be read just now.
392
+ */
393
+ getTrainingCurve(id: string): Promise<TrainingCurve>;
352
394
  /**
353
395
  * One run with its timeline, the addresses of everything it created, and
354
396
  * what you may do with it right now.
@@ -370,7 +412,9 @@ export declare class Loop {
370
412
  * refused rather than promote against a consent that moved underneath the
371
413
  * decision, and `force` only to promote a comparison the evaluation called
372
414
  * inconclusive -- without it that case is refused with
373
- * `INCONCLUSIVE_REQUIRES_FORCE`.
415
+ * `INCONCLUSIVE_REQUIRES_FORCE`. A training that was never tested (`tested`
416
+ * false) is refused `NOT_TESTED` whatever `force` says: nothing untested
417
+ * goes live; {@link retestTrainingRun} tests it again.
374
418
  */
375
419
  promoteTrainingRun(id: string, params?: TrainingRunPromoteRequest): Promise<TrainingRunActionResponse>;
376
420
  /** Retire the candidate. What serves your traffic does not change. */
@@ -397,6 +441,18 @@ export declare class Loop {
397
441
  * cancelling a training job does not refund the hours it burned.
398
442
  */
399
443
  cancelTrainingRun(id: string, params?: TrainingRunCancelRequest): Promise<TrainingRunActionResponse>;
444
+ /**
445
+ * Test again: book the comparison of a training that trained and was never
446
+ * tested (`tested` false, `available_actions.retest` true) again, on the
447
+ * same training. It is tested against what is live now, on the questions it
448
+ * was held back for, and spends what is left of the amount set aside for
449
+ * testing it -- never a second amount. Answers 202 with the run back in
450
+ * `checkpoint_ready`; a second call while that test is under way answers the
451
+ * same. Refusals are a `TrainingRunRetestRefusalCode`, with the reason in
452
+ * plain words. A training that was never tested can never be promoted:
453
+ * {@link promoteTrainingRun} answers `NOT_TESTED`.
454
+ */
455
+ retestTrainingRun(id: string): Promise<TrainingRunActionResponse>;
400
456
  /**
401
457
  * The workspace's pipelines, a page at a time: its versions (the attempts
402
458
  * that became the live model), its latest 5 attempts (`attempts_total` says
@@ -549,13 +605,19 @@ export declare class Loop {
549
605
  * that brought no verdict of its own as a good example to learn from.
550
606
  */
551
607
  importIntoPipeline(id: string, params: PipelineImportParams): Promise<LoopImportResult>;
552
- /** The workspace's datasets, the most recently changed first: `{ datasets, total, limit, offset }`. */
608
+ /**
609
+ * The workspace's datasets, the most recently changed first:
610
+ * `{ datasets, total, limit, offset }`. `q` keeps the ones whose name
611
+ * contains it, whatever its case; `total` counts those.
612
+ */
553
613
  listDatasets(params?: LoopDatasetListParams): Promise<LoopDatasetListResponse>;
554
614
  /**
555
615
  * One dataset: its conditions, how many samples it selects now (`samples`,
556
616
  * the same number `listTraces({ dataset: id })` totals) by feedback, how
557
- * many are in a benchmark (never trained on), and the pipelines that train
558
- * on it (`used_by`).
617
+ * many are in a benchmark (never trained on), the pipelines that train on
618
+ * it (`used_by`), who saved it (`created_by`) and the trainings that
619
+ * learned from it (`learned_by`, newest first, at most 50 of
620
+ * `learned_by_total`).
559
621
  */
560
622
  getDataset(id: string): Promise<LoopDataset>;
561
623
  /**
@@ -590,6 +652,20 @@ export declare class Loop {
590
652
  * numbers. It writes nothing.
591
653
  */
592
654
  previewDataset(params?: LoopDatasetPreviewParams): Promise<LoopDatasetPreview>;
655
+ /**
656
+ * Download a dataset's samples as a file: one line a sample, oldest first,
657
+ * in the import's chat format, with the feedback a person gave each in the
658
+ * import's words -- so `importRows` takes the lines back as they are.
659
+ * `jsonl` is the file exactly as sent, `lines` each line parsed. A sample
660
+ * whose text could not be read (kept in your own storage and refused just
661
+ * now, or gone) is left out; `stored_elsewhere` is how many of the
662
+ * dataset's samples have no conversation kept with them on the platform
663
+ * (kept in your own storage, or none at all), the most that can be left
664
+ * out. A long file keeps coming as long as lines keep arriving: the
665
+ * client's `timeout` is the longest wait for the next part, not for the
666
+ * whole file.
667
+ */
668
+ exportDataset(id: string, params?: LoopDatasetExportParams): Promise<LoopDatasetExport>;
593
669
  /** The pipeline's benchmarks in priority order (1 is the primary), the ones it stopped using, and its promotion policy. */
594
670
  listPipelineBenchmarks(id: string): Promise<PipelineBenchmarksResponse>;
595
671
  /** Add a benchmark to a pipeline, last unless `priority` says where. Every version from now on is replayed on it. */
@@ -747,6 +823,16 @@ export declare class Loop {
747
823
  * different set, which is the exact defect a standing benchmark removes.
748
824
  */
749
825
  getBenchmarkRun(id: string): Promise<BenchmarkRun>;
826
+ /**
827
+ * Every question of a benchmark with how one pipeline's newest trainings
828
+ * did on each -- right or wrong (`passed`: a score of 0.70 or more) and
829
+ * the score -- so you can see which questions a new version got wrong that
830
+ * the one before got right. `trainings` lists them oldest first and each
831
+ * question's `results` follows that order; a training that did not score a
832
+ * question has null there. `pipeline` is required (training numbers are
833
+ * per pipeline); `trainings` is 5 when absent, 1 to 20.
834
+ */
835
+ listBenchmarkQuestions(id: string, params: BenchmarkQuestionsParams): Promise<BenchmarkQuestionsResponse>;
750
836
  /**
751
837
  * The conversations behind one replay, in the benchmark's order: the pinned
752
838
  * prompt and its reference, both answers, what the judge made of each (its
@@ -1,56 +1,67 @@
1
1
  import { ApiError } from '../client.js';
2
+ import { LoopNoVersionError, PIPELINE_ID_PATTERN, PIPELINE_MODEL_PREFIX, chatAnswerText, } from './loop-chat.js';
2
3
  /**
3
- * The Conscious Loop -- pipelines that keep improving a model from the
4
- * conversations you send them.
4
+ * The Conscious Loop -- pipelines that keep improving a model from your app's
5
+ * conversations and your feedback.
5
6
  *
6
- * A pipeline is one use case: a model, the tags whose samples train it, when
7
- * to train, and how a new version is promoted. Every time it trains it makes
8
- * an attempt; an attempt that goes live becomes the next version: one that
9
- * beats the live model, one a member approves, or, after a deleted
10
- * conversation, a rebuild without it that is not clearly worse (a `manual`
11
- * pipeline waits for a member even then).
7
+ * A pipeline is one use case: a model, the samples it learns from, and when
8
+ * to train. Each time it trains, that is a training (#1, #2, ...; the API
9
+ * calls them attempts). A training that goes live becomes the next version:
10
+ * one that does better than the live version on its tests, one a member
11
+ * approves, or, after a deleted conversation, a rebuild without it that is
12
+ * not clearly worse (a `manual` pipeline waits for a member even then). The
13
+ * live version is the version your app gets answers from right now; it
14
+ * answers to the model name `loop-<pipeline ID>`, and each new version takes
15
+ * that name over.
12
16
  *
13
- * Nothing is captured until a source is switched on, and you can switch it
14
- * off again at any time. Creating a pipeline switches on the source named by
15
- * its own tag (`own_tag`), stamping that tag, and deleting it switches that
16
- * source off again. Anything that looks like a credential or a personal detail
17
- * is removed before the record is written, never afterwards.
17
+ * What is saved. A call that carries the pipeline ID in the `X-Loop-Pipeline`
18
+ * header -- `chat` here, or the `loop` option of `client.inference` -- is
19
+ * saved as a sample of that pipeline: the header is your consent for that one
20
+ * call, whatever the recording switches say. Without the header nothing is
21
+ * saved unless recording is switched on for a source (`setConfig`). Creating
22
+ * a pipeline switches on the source named by its pipeline ID (`own_tag`), so
23
+ * `capture` to it is saved too, and deleting the pipeline switches that source
24
+ * off again. Anything that looks like a credential or a personal detail is
25
+ * removed before the record is written, never afterwards.
18
26
  *
19
- * Requires an API key carrying `loop:read` and/or `loop:write`. Those scopes are
27
+ * Requires an API key carrying `loop:read` and/or `loop:write` (and, for
28
+ * `chat`, the scope that calls the model: `serverless`, or `deployments:read`
29
+ * for a deployment or a pipeline's own model). The loop scopes are
20
30
  * deliberately absent from the Read Only preset, because what they return is
21
31
  * your raw prompts and completions rather than catalog metadata.
22
32
  *
23
- * Offered on the development environment only for now. Anywhere else every
24
- * method here, reads included, rejects with a `ComingSoonError` whose
25
- * `code` is `LOOP_COMING_SOON` (HTTP 403): a deliberate product state, not an
26
- * outage, so retrying cannot succeed there.
33
+ * Where the platform has the Conscious Loop switched off, every method here,
34
+ * reads included, rejects with a `ComingSoonError` whose `code` is
35
+ * `LOOP_COMING_SOON` (HTTP 403): a deliberate product state, not an outage, so
36
+ * retrying cannot succeed there. The exceptions are the five a member needs to
37
+ * stop paying, which answer as usual: `listPipelines`, `getPipeline`,
38
+ * `updatePipeline(id, { enabled: false })` with nothing else but
39
+ * `expected_revision` (switch it off), `deletePipeline` (which stops its live
40
+ * version and removes its data) and `cancelTrainingRun`.
27
41
  *
28
42
  * @example
29
43
  * ```ts
30
- * // 1. A pipeline: a model it can train, and its own tag.
31
- * const { models } = await client.loop.listModels();
32
- * const { pipeline } = await client.loop.createPipeline({
33
- * name: 'Support replies',
34
- * model: { id: models[0].id },
44
+ * // 1. Send your app's conversations. One call asks the model and saves the
45
+ * // conversation as a sample of the pipeline. Before the pipeline's first
46
+ * // version, pass the model your app uses now.
47
+ * const reply = await client.loop.chat({
48
+ * pipeline: 'support-replies',
49
+ * messages: [{ role: 'user', content: 'What is our refund window?' }],
50
+ * model: 'meta-llama/Llama-3.1-8B-Instruct',
35
51
  * });
52
+ * console.log(reply.answer);
36
53
  *
37
- * // 2. Send samples from your app to the pipeline's own source: its create
38
- * // switched that source on, and it tags everything it records.
39
- * const sent = await client.loop.capture({
40
- * deployment_id: pipeline.own_tag!,
41
- * model: 'gpt-4o',
42
- * messages: [{ role: 'user', content: 'what is our refund window' }],
43
- * completion: 'thirty days',
44
- * });
45
- *
46
- * // 3. Say whether it was right, on the trace_id the capture answered with.
47
- * // A correction is the most valuable answer.
48
- * if (sent.trace_id) {
49
- * await client.loop.correct(sent.trace_id, 'Thirty days from delivery, no questions asked.');
50
- * }
54
+ * // 2. Tell it how the answer did: good, wrong, or wrong with the right answer.
55
+ * await reply.good();
56
+ * // await reply.bad();
57
+ * // await reply.correct('Thirty days from delivery, no questions asked.');
51
58
  *
52
- * // 4. Once it has its minimum, it trains; versions appear on the pipeline.
53
- * const p = await client.loop.getPipeline(pipeline.rule_id);
59
+ * // 3. Once a version is live, leave model out: loop-support-replies answers
60
+ * // with the live version.
61
+ * const next = await client.loop.chat({
62
+ * pipeline: 'support-replies',
63
+ * messages: [{ role: 'user', content: 'Can I return a gift?' }],
64
+ * });
54
65
  * ```
55
66
  */
56
67
  /**
@@ -69,12 +80,90 @@ const SIGNAL_MANY_CONCURRENCY = 8;
69
80
  const SIGNAL_MANY_CONCURRENCY_MAX = 32;
70
81
  export class Loop {
71
82
  _http;
72
- /** @internal */
73
- constructor(_http) {
83
+ _inference;
84
+ /**
85
+ * @internal The SDK client hands Loop its Inference resource, which `chat`
86
+ * calls. A Loop made without one can do everything but `chat`.
87
+ */
88
+ constructor(_http, _inference) {
74
89
  this._http = _http;
90
+ this._inference = _inference;
75
91
  }
76
92
  /** @internal How feedback waits between tries; tests replace it so nothing sleeps. */
77
93
  _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
94
+ // ── chat: ask, save the sample, give feedback ─────────────────────────
95
+ /**
96
+ * Ask a model and save the conversation as a sample of a pipeline, in one
97
+ * call; the result carries the answer, its sample ID and the feedback for it.
98
+ *
99
+ * It is `client.inference.chatCompletions` with the pipeline ID in the
100
+ * `X-Loop-Pipeline` header: a normal, billed model call. `model` defaults to
101
+ * the pipeline's own model, `loop-<pipeline ID>`, which answers with the live
102
+ * version. While the pipeline has no live version (before its first one)
103
+ * nothing answers to that name, so pass `model` (the model your app uses
104
+ * now); left out then, this rejects with `LoopNoVersionError` (an
105
+ * `ApiError`) and no model answered. The pipeline ID is lowercased, as the
106
+ * platform reads it.
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * const reply = await client.loop.chat({
111
+ * pipeline: 'support-replies',
112
+ * messages: [{ role: 'user', content: 'Where is my order?' }],
113
+ * });
114
+ * console.log(reply.answer);
115
+ * await reply.good(); // or reply.bad()
116
+ * // await reply.correct('It ships on Monday.');
117
+ * ```
118
+ */
119
+ async chat(params) {
120
+ if (!this._inference) {
121
+ throw new Error('loop.chat calls a model, so it needs the SDK client: new RunBiOS({ apiKey }).loop.chat(...)');
122
+ }
123
+ if (!params || typeof params !== 'object') {
124
+ throw new Error('loop.chat takes { pipeline, messages, model? }');
125
+ }
126
+ const { pipeline: asked, model: askedModel, loop: _ignored, ...rest } = params;
127
+ // Lowercased as the gateways read the header, so the default model name
128
+ // (matched case for case) is the pipeline's own.
129
+ const pipeline = typeof asked === 'string' ? asked.trim().toLowerCase() : '';
130
+ if (!PIPELINE_ID_PATTERN.test(pipeline)) {
131
+ throw new Error(`pipeline must be one pipeline ID, e.g. "support-replies": lowercase letters, digits, dot, dash or underscore (got ${JSON.stringify(asked)})`);
132
+ }
133
+ const explicit = typeof askedModel === 'string' && askedModel.trim() !== '';
134
+ const model = explicit ? askedModel.trim() : PIPELINE_MODEL_PREFIX + pipeline;
135
+ let raw;
136
+ try {
137
+ raw = await this._inference.chatCompletions({ ...rest, messages: params.messages, model, loop: { pipeline } });
138
+ }
139
+ catch (err) {
140
+ if (!explicit && err instanceof ApiError && err.status === 404) {
141
+ throw new LoopNoVersionError(pipeline, model, err);
142
+ }
143
+ throw err;
144
+ }
145
+ const sampleId = raw.loop_sample_id ?? null;
146
+ const needId = () => {
147
+ if (!sampleId) {
148
+ throw new Error('No sample ID came back with this answer, so it was not saved as a sample and cannot take feedback.');
149
+ }
150
+ return sampleId;
151
+ };
152
+ return {
153
+ answer: chatAnswerText(raw),
154
+ sample_id: sampleId,
155
+ raw,
156
+ good: async (reason) => this.signal(needId(), { verdict: 'good', reason }),
157
+ bad: async (reason) => this.signal(needId(), { verdict: 'bad', reason }),
158
+ correct: async (rightAnswer, reason) => {
159
+ const id = needId();
160
+ if (typeof rightAnswer !== 'string' || !rightAnswer.trim()) {
161
+ throw new Error('correct needs the right answer: the text the model should have given');
162
+ }
163
+ return this.signal(id, { verdict: 'corrected', correction: rightAnswer, reason });
164
+ },
165
+ };
166
+ }
78
167
  // ── capture ───────────────────────────────────────────────────────────
79
168
  /**
80
169
  * Send one exchange to be captured.
@@ -577,6 +666,20 @@ export class Loop {
577
666
  const qs = q.toString();
578
667
  return this._http.fetchGet(`/api/loop/training-runs${qs ? `?${qs}` : ''}`);
579
668
  }
669
+ /**
670
+ * How a training went while it trained, step by step: its loss on the
671
+ * samples it trained on (`train_loss`) and on the practice questions it set
672
+ * aside (`check_loss`), at most 200 points with every practice check among
673
+ * them, and the step whose model it kept (`kept_step`). `keeps_last` is a
674
+ * training that keeps its last step rather than its best practice score.
675
+ * With nothing to chart -- no training job yet, or no practice check --
676
+ * `points` is empty and `reason` says why. 503
677
+ * `TRAINING_CURVE_UNAVAILABLE` when it could not be read just now.
678
+ */
679
+ async getTrainingCurve(id) {
680
+ const res = await this._http.fetchGet(`/api/loop/training-runs/${encodeURIComponent(id)}/training-curve`);
681
+ return { ...res, points: res.points || [] };
682
+ }
580
683
  /**
581
684
  * One run with its timeline, the addresses of everything it created, and
582
685
  * what you may do with it right now.
@@ -600,7 +703,9 @@ export class Loop {
600
703
  * refused rather than promote against a consent that moved underneath the
601
704
  * decision, and `force` only to promote a comparison the evaluation called
602
705
  * inconclusive -- without it that case is refused with
603
- * `INCONCLUSIVE_REQUIRES_FORCE`.
706
+ * `INCONCLUSIVE_REQUIRES_FORCE`. A training that was never tested (`tested`
707
+ * false) is refused `NOT_TESTED` whatever `force` says: nothing untested
708
+ * goes live; {@link retestTrainingRun} tests it again.
604
709
  */
605
710
  async promoteTrainingRun(id, params = {}) {
606
711
  return this._http.fetchPost(`/api/loop/training-runs/${encodeURIComponent(id)}/promote`, params);
@@ -635,6 +740,20 @@ export class Loop {
635
740
  async cancelTrainingRun(id, params = {}) {
636
741
  return this._http.fetchPost(`/api/loop/training-runs/${encodeURIComponent(id)}/cancel`, params);
637
742
  }
743
+ /**
744
+ * Test again: book the comparison of a training that trained and was never
745
+ * tested (`tested` false, `available_actions.retest` true) again, on the
746
+ * same training. It is tested against what is live now, on the questions it
747
+ * was held back for, and spends what is left of the amount set aside for
748
+ * testing it -- never a second amount. Answers 202 with the run back in
749
+ * `checkpoint_ready`; a second call while that test is under way answers the
750
+ * same. Refusals are a `TrainingRunRetestRefusalCode`, with the reason in
751
+ * plain words. A training that was never tested can never be promoted:
752
+ * {@link promoteTrainingRun} answers `NOT_TESTED`.
753
+ */
754
+ async retestTrainingRun(id) {
755
+ return this._http.fetchPost(`/api/loop/training-runs/${encodeURIComponent(id)}/retest`, {});
756
+ }
638
757
  // ── pipelines ─────────────────────────────────────────────────────────
639
758
  /**
640
759
  * The workspace's pipelines, a page at a time: its versions (the attempts
@@ -852,9 +971,15 @@ export class Loop {
852
971
  // match later join it. Every pipeline already trains on its own tag; add a
853
972
  // dataset to a pipeline (`datasets` on create or edit) to train it on more.
854
973
  // One dataset can feed any number of pipelines.
855
- /** The workspace's datasets, the most recently changed first: `{ datasets, total, limit, offset }`. */
974
+ /**
975
+ * The workspace's datasets, the most recently changed first:
976
+ * `{ datasets, total, limit, offset }`. `q` keeps the ones whose name
977
+ * contains it, whatever its case; `total` counts those.
978
+ */
856
979
  async listDatasets(params = {}) {
857
980
  const q = new URLSearchParams();
981
+ if (params.q)
982
+ q.set('q', params.q);
858
983
  if (params.limit != null)
859
984
  q.set('limit', String(params.limit));
860
985
  if (params.offset)
@@ -867,8 +992,10 @@ export class Loop {
867
992
  /**
868
993
  * One dataset: its conditions, how many samples it selects now (`samples`,
869
994
  * the same number `listTraces({ dataset: id })` totals) by feedback, how
870
- * many are in a benchmark (never trained on), and the pipelines that train
871
- * on it (`used_by`).
995
+ * many are in a benchmark (never trained on), the pipelines that train on
996
+ * it (`used_by`), who saved it (`created_by`) and the trainings that
997
+ * learned from it (`learned_by`, newest first, at most 50 of
998
+ * `learned_by_total`).
872
999
  */
873
1000
  async getDataset(id) {
874
1001
  return this._http.fetchGet(`/api/loop/datasets/${encodeURIComponent(id)}`);
@@ -914,6 +1041,31 @@ export class Loop {
914
1041
  async previewDataset(params = {}) {
915
1042
  return this._http.fetchPost('/api/loop/datasets/preview', params);
916
1043
  }
1044
+ /**
1045
+ * Download a dataset's samples as a file: one line a sample, oldest first,
1046
+ * in the import's chat format, with the feedback a person gave each in the
1047
+ * import's words -- so `importRows` takes the lines back as they are.
1048
+ * `jsonl` is the file exactly as sent, `lines` each line parsed. A sample
1049
+ * whose text could not be read (kept in your own storage and refused just
1050
+ * now, or gone) is left out; `stored_elsewhere` is how many of the
1051
+ * dataset's samples have no conversation kept with them on the platform
1052
+ * (kept in your own storage, or none at all), the most that can be left
1053
+ * out. A long file keeps coming as long as lines keep arriving: the
1054
+ * client's `timeout` is the longest wait for the next part, not for the
1055
+ * whole file.
1056
+ */
1057
+ async exportDataset(id, params = {}) {
1058
+ const q = new URLSearchParams();
1059
+ q.set('format', params.format ?? 'jsonl');
1060
+ if (params.limit != null)
1061
+ q.set('limit', String(params.limit));
1062
+ const { text, headers } = await this._http.fetchText(`/api/loop/datasets/${encodeURIComponent(id)}/export?${q.toString()}`);
1063
+ const lines = text.split('\n').filter((line) => line.trim() !== '')
1064
+ .map((line) => JSON.parse(line));
1065
+ const elsewhere = headers.get('X-Loop-Stored-Elsewhere');
1066
+ const stored = elsewhere != null && /^\d+$/.test(elsewhere) ? Number(elsewhere) : null;
1067
+ return { jsonl: text, lines, stored_elsewhere: stored };
1068
+ }
917
1069
  // ── a pipeline's benchmarks, and its versions side by side ─────────────
918
1070
  /** The pipeline's benchmarks in priority order (1 is the primary), the ones it stopped using, and its promotion policy. */
919
1071
  async listPipelineBenchmarks(id) {
@@ -1153,6 +1305,27 @@ export class Loop {
1153
1305
  const res = await this._http.fetchGet(`/api/loop/benchmark-runs/${encodeURIComponent(id)}`);
1154
1306
  return res.benchmark_run;
1155
1307
  }
1308
+ /**
1309
+ * Every question of a benchmark with how one pipeline's newest trainings
1310
+ * did on each -- right or wrong (`passed`: a score of 0.70 or more) and
1311
+ * the score -- so you can see which questions a new version got wrong that
1312
+ * the one before got right. `trainings` lists them oldest first and each
1313
+ * question's `results` follows that order; a training that did not score a
1314
+ * question has null there. `pipeline` is required (training numbers are
1315
+ * per pipeline); `trainings` is 5 when absent, 1 to 20.
1316
+ */
1317
+ async listBenchmarkQuestions(id, params) {
1318
+ const q = new URLSearchParams();
1319
+ q.set('pipeline', params.pipeline);
1320
+ if (params.trainings != null)
1321
+ q.set('trainings', String(params.trainings));
1322
+ if (params.limit != null)
1323
+ q.set('limit', String(params.limit));
1324
+ if (params.offset != null)
1325
+ q.set('offset', String(params.offset));
1326
+ const res = await this._http.fetchGet(`/api/loop/benchmarks/${encodeURIComponent(id)}/questions?${q.toString()}`);
1327
+ return { ...res, trainings: res.trainings || [], items: res.items || [] };
1328
+ }
1156
1329
  /**
1157
1330
  * The conversations behind one replay, in the benchmark's order: the pinned
1158
1331
  * prompt and its reference, both answers, what the judge made of each (its