runbios-sdk 0.4.0-rc.337 → 0.5.0-dev.338
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/CHANGELOG.md +37 -0
- package/README.md +124 -16
- package/dist/client.d.ts +36 -0
- package/dist/client.js +106 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.js +11 -5
- package/dist/resources/inference.js +1 -1
- package/dist/resources/loop-chat.d.ts +87 -0
- package/dist/resources/loop-chat.js +32 -0
- package/dist/resources/loop.d.ts +93 -7
- package/dist/resources/loop.js +218 -45
- package/dist/types.d.ts +494 -34
- package/package.json +1 -1
package/dist/resources/loop.d.ts
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
/**
|
|
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),
|
|
558
|
-
*
|
|
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
|
package/dist/resources/loop.js
CHANGED
|
@@ -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
|
|
4
|
-
* conversations
|
|
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
|
|
7
|
-
* to train
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* conversation, a rebuild without it that is
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* `
|
|
26
|
-
*
|
|
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.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
-
* //
|
|
53
|
-
*
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
/**
|
|
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),
|
|
871
|
-
*
|
|
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
|