corethink 1.0.0__py3-none-any.whl

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 (48) hide show
  1. corethink/__init__.py +17 -0
  2. corethink/__main__.py +3 -0
  3. corethink/_version.py +3 -0
  4. corethink/account.py +470 -0
  5. corethink/cli.py +1058 -0
  6. corethink/client.py +253 -0
  7. corethink/commands/__init__.py +0 -0
  8. corethink/commands/admin.py +703 -0
  9. corethink/commands/api.py +59 -0
  10. corethink/commands/archive.py +433 -0
  11. corethink/commands/auth.py +375 -0
  12. corethink/commands/billing.py +314 -0
  13. corethink/commands/capture.py +564 -0
  14. corethink/commands/connection.py +1266 -0
  15. corethink/commands/endpoint.py +1124 -0
  16. corethink/commands/env.py +1966 -0
  17. corethink/commands/hub.py +269 -0
  18. corethink/commands/library.py +347 -0
  19. corethink/commands/models.py +258 -0
  20. corethink/commands/train.py +2655 -0
  21. corethink/commands/workspace.py +622 -0
  22. corethink/config.py +181 -0
  23. corethink/context.py +46 -0
  24. corethink/env_template.py +117 -0
  25. corethink/environment_http_store.py +96 -0
  26. corethink/experiment_cli.py +2187 -0
  27. corethink/hosted_profiles.py +44 -0
  28. corethink/hosted_rl.py +688 -0
  29. corethink/hosted_rl_archive.py +74 -0
  30. corethink/hosted_rl_attribution.py +44 -0
  31. corethink/hosted_rl_billing.py +103 -0
  32. corethink/hosted_rl_state.py +182 -0
  33. corethink/jobs_cli.py +839 -0
  34. corethink/main.py +204 -0
  35. corethink/output.py +201 -0
  36. corethink/py.typed +0 -0
  37. corethink/rl_cli.py +997 -0
  38. corethink/rl_inspect.py +521 -0
  39. corethink/rl_playground.py +129 -0
  40. corethink/rl_reassessment.py +214 -0
  41. corethink/rl_settings.py +43 -0
  42. corethink/sdk.py +331 -0
  43. corethink/settings.py +162 -0
  44. corethink-1.0.0.dist-info/METADATA +120 -0
  45. corethink-1.0.0.dist-info/RECORD +48 -0
  46. corethink-1.0.0.dist-info/WHEEL +5 -0
  47. corethink-1.0.0.dist-info/entry_points.txt +2 -0
  48. corethink-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,59 @@
1
+ """`corethink api`: any Workbench API call, signed in. Everything the console can do is reachable through it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import typer
10
+
11
+ from corethink import output as ui
12
+ from corethink.context import client
13
+
14
+ METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE"}
15
+
16
+
17
+ def api(
18
+ method: str = typer.Argument(..., help="GET, POST, PUT, PATCH or DELETE."),
19
+ path: str = typer.Argument(..., help="The API path, e.g. /api/v1/harnesses/usecases"),
20
+ data: str = typer.Option(None, "--data", "-d", help="JSON body; @file reads it from a file, @- from stdin."),
21
+ param: list[str] = typer.Option(None, "--param", "-p", help="Query parameter key=value (repeatable)."),
22
+ ) -> None:
23
+ """Call any Workbench API route directly and print its JSON answer.
24
+
25
+ Example: corethink api GET /api/v1/harnesses/training-jobs -p qa=1
26
+ """
27
+ verb = method.upper()
28
+ if verb not in METHODS:
29
+ ui.error(f"Use one of {', '.join(sorted(METHODS))}.")
30
+ raise typer.Exit(2)
31
+ if not path.startswith("/api/"):
32
+ path = "/api/v1/" + path.lstrip("/")
33
+ body = None
34
+ if data is not None:
35
+ text = (
36
+ sys.stdin.read()
37
+ if data == "@-"
38
+ else Path(data[1:]).read_text(encoding="utf-8")
39
+ if data.startswith("@")
40
+ else data
41
+ )
42
+ try:
43
+ body = json.loads(text) if text.strip() else None
44
+ except ValueError as error:
45
+ ui.error(f"--data is not JSON: {error}")
46
+ raise typer.Exit(2) from None
47
+ params = {}
48
+ for item in param or []:
49
+ key, sep, value = item.partition("=")
50
+ if not sep:
51
+ ui.error(f"--param {item}: use key=value.")
52
+ raise typer.Exit(2)
53
+ params[key] = value
54
+ answer = client().request(verb, path, json=body, params=params or None)
55
+ ui.print_json(answer)
56
+
57
+
58
+ def register(root: typer.Typer) -> None:
59
+ root.command(rich_help_panel="Advanced")(api)
@@ -0,0 +1,433 @@
1
+ """`corethink archive`: archived runs, tests and environments; restore them, or (admins) delete them for good.
2
+
3
+ As the console's Archive page: restoring a run or test is for members, an environment for admins; deleting is for admins,
4
+ after the server's own preview of what would go. "Delete all" deletes a few at a time and says what did not go, and why.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ from typing import Any
11
+
12
+ import typer
13
+
14
+ from corethink import output as ui
15
+ from corethink.client import ApiError
16
+ from corethink.commands.train import (
17
+ ARCHIVE,
18
+ JOBS,
19
+ USECASES,
20
+ _test_result,
21
+ base_vs_trained,
22
+ fail,
23
+ pick_run,
24
+ plural,
25
+ run_environment,
26
+ run_name,
27
+ )
28
+ from corethink.context import client
29
+
30
+ app = typer.Typer(
31
+ help="Archived runs, tests and environments: restore them, or delete them for good.",
32
+ no_args_is_help=True,
33
+ rich_markup_mode="rich",
34
+ )
35
+
36
+ # how many ids one bulk delete request carries (the console's BULK_CHUNK; the server takes at most 25)
37
+ BULK_CHUNK = 10
38
+ DELETE_ALL_WORD = "delete all"
39
+
40
+
41
+ def load_archive() -> dict[str, Any]:
42
+ answer = client().get(ARCHIVE) or {}
43
+ return {
44
+ "runs": list(answer.get("runs") or []),
45
+ "environments": list(answer.get("environments") or []),
46
+ }
47
+
48
+
49
+ def runs_and_tests(runs: int, tests: int) -> str:
50
+ return " and ".join(p for p in (plural(runs, "run") if runs else "", plural(tests, "test") if tests else "") if p)
51
+
52
+
53
+ def _result(run: dict[str, Any]) -> str | None:
54
+ if run.get("kind") == "test":
55
+ return _test_result(run)
56
+ if run.get("state") != "SUCCEEDED":
57
+ return str(run.get("state") or "").lower() or None
58
+ scores = base_vs_trained(run.get("gate"))
59
+ if scores["base"] is None or scores["trained"] is None:
60
+ return None
61
+ return f"{ui.score(scores['base'])} → {ui.score(scores['trained'])}"
62
+
63
+
64
+ def _archived(at: Any, by: Any) -> str:
65
+ who = f" by {by}" if by and not str(by).startswith("local:") else ""
66
+ return f"{ui.when(at)}{who}" if at else (f"archived{who}" if who else "—")
67
+
68
+
69
+ @app.command("list")
70
+ def list_archive() -> None:
71
+ """Archived environments, runs and tests in this workspace.
72
+
73
+ Example: corethink archive list
74
+ """
75
+ archive = load_archive()
76
+
77
+ def human() -> None:
78
+ environments, runs = archive["environments"], archive["runs"]
79
+ if not environments and not runs:
80
+ ui.info(
81
+ "Nothing is archived. Archive a run with `corethink train archive RUN`: it waits here, with everything "
82
+ "it had, until it is restored or deleted for good."
83
+ )
84
+ return
85
+ if environments:
86
+ ui.table(
87
+ environments,
88
+ [
89
+ ("ID", "id"),
90
+ ("Name", "name"),
91
+ ("Runs", lambda e: str(e.get("runs", 0))),
92
+ ("Tests", lambda e: str(e.get("tests", 0))),
93
+ ("Archived", lambda e: _archived(e.get("archived_at"), e.get("archived_by"))),
94
+ ],
95
+ title=f"Environments · {len(environments)}",
96
+ )
97
+ ui.out.print()
98
+ if runs:
99
+ archived_envs = {e.get("id") for e in environments}
100
+ names = {e.get("id"): e.get("name") for e in environments}
101
+ ui.table(
102
+ runs,
103
+ [
104
+ ("ID", lambda r: ui.short_id(r.get("job_id"))),
105
+ ("Name", run_name),
106
+ (
107
+ "Kind",
108
+ lambda r: "Base-model test" if r.get("kind") == "test" else "Training run",
109
+ ),
110
+ ("Environment", lambda r: run_environment(r, names)),
111
+ ("Result", _result),
112
+ (
113
+ "Archived",
114
+ lambda r: (
115
+ _archived(r.get("archived_at"), r.get("archived_by"))
116
+ + (" with its environment" if r.get("archived_with") else "")
117
+ + (
118
+ " (environment archived)"
119
+ if r.get("usecase") in archived_envs and not r.get("archived_with")
120
+ else ""
121
+ )
122
+ ),
123
+ ),
124
+ ],
125
+ title=f"Runs and tests · {len(runs)}",
126
+ )
127
+ ui.info("An archived run's or environment's endpoint keeps serving what it served when it was archived.")
128
+
129
+ ui.emit(archive, human)
130
+
131
+
132
+ def _which(ref: str, archive: dict[str, Any], kind: str | None) -> tuple[str, dict[str, Any]]:
133
+ """("environment" | "run", the archived thing) that `ref` names."""
134
+ wanted = ref.strip()
135
+ if kind != "run":
136
+ envs = archive["environments"]
137
+ exact = [e for e in envs if e.get("id") == wanted]
138
+ found = (
139
+ exact
140
+ or [e for e in envs if str(e.get("id", "")).startswith(wanted)]
141
+ or [e for e in envs if str(e.get("name", "")).lower() == wanted.lower()]
142
+ )
143
+ if len(found) == 1:
144
+ return "environment", found[0]
145
+ if len(found) > 1:
146
+ raise fail(
147
+ f"{wanted!r} matches {len(found)} archived environments: {', '.join(e['id'] for e in found[:6])}."
148
+ )
149
+ if kind == "environment":
150
+ raise fail(f"No archived environment matches {wanted!r}. See `corethink archive list`.", 5)
151
+ return "run", pick_run(wanted, archive["runs"], where=" in the Archive")
152
+
153
+
154
+ def restore_run(ref: str) -> None:
155
+ """Restore an archived run or test (members): `corethink train restore` and `corethink archive restore`."""
156
+ archive = load_archive()
157
+ _restore("run", pick_run(ref, archive["runs"], where=" in the Archive"), archive)
158
+
159
+
160
+ def _restore(kind: str, thing: dict[str, Any], archive: dict[str, Any]) -> None:
161
+ if kind == "environment":
162
+ summary = client().post(f"{USECASES}/{thing['id']}:restore")
163
+ moved = (summary or {}).get("moved") or {}
164
+
165
+ def human() -> None:
166
+ ui.success(
167
+ f"Restored {thing.get('name') or thing['id']}, with {plural(moved.get('runs', 0), 'run')} and "
168
+ f"{plural(moved.get('tests', 0), 'test')}."
169
+ )
170
+
171
+ ui.emit(summary, human)
172
+ return
173
+ name = run_name(thing)
174
+ noun = "test" if thing.get("kind") == "test" else "run"
175
+ if thing.get("usecase") in {e.get("id") for e in archive["environments"]}:
176
+ if thing.get("archived_with"):
177
+ raise fail(
178
+ f"{noun.capitalize()} {name} was archived with its environment: restoring the environment brings it "
179
+ f"back (`corethink archive restore {thing['usecase']}`).",
180
+ 6,
181
+ )
182
+ raise fail(
183
+ f"Its environment is archived: restore the environment first (`corethink archive restore {thing['usecase']}`).",
184
+ 6,
185
+ )
186
+ summary = client().post(f"{JOBS}/{thing['job_id']}:restore")
187
+ ui.emit(
188
+ summary,
189
+ lambda: ui.success(f"Restored {noun} {name}. See it with `corethink train show {str(thing['job_id'])[:8]}`."),
190
+ )
191
+
192
+
193
+ @app.command("restore")
194
+ def restore(
195
+ ref: str = typer.Argument(
196
+ ..., help="An archived run's id (or the start of it, or its name), or an environment's id."
197
+ ),
198
+ environment: bool = typer.Option(False, "--env", help="It is an environment."),
199
+ run: bool = typer.Option(False, "--run", help="It is a run or test."),
200
+ ) -> None:
201
+ """Bring an archived run, test or environment back (an environment comes back with the runs archived with it).
202
+
203
+ Examples:
204
+ corethink archive restore 3f2a
205
+ corethink archive restore payroll-0ecb50 --env
206
+ """
207
+ if environment and run:
208
+ raise fail("Choose --env or --run, not both.")
209
+ archive = load_archive()
210
+ kind, thing = _which(ref, archive, "environment" if environment else "run" if run else None)
211
+ _restore(kind, thing, archive)
212
+
213
+
214
+ def deletion_lines(summary: dict[str, Any]) -> list[str]:
215
+ """What a delete removes, line by line, from the server's own count (DeleteDialog.tsx deletionLines)."""
216
+ d = summary.get("deleted") or {}
217
+ target = (summary.get("target") or {}).get("kind")
218
+ lines: list[str] = []
219
+ if target == "environment":
220
+ if d.get("runs") or d.get("tests"):
221
+ what = " and ".join(
222
+ p
223
+ for p in (
224
+ plural(d.get("runs", 0), "training run") if d.get("runs") else "",
225
+ plural(d.get("tests", 0), "base-model test") if d.get("tests") else "",
226
+ )
227
+ if p
228
+ )
229
+ lines.append(
230
+ f"{what}, with their logs, charts, final checks and files ({plural(d.get('run_files', 0), 'file')})"
231
+ )
232
+ else:
233
+ lines.append("No runs or tests")
234
+ else:
235
+ lines.append(
236
+ f"The {'test' if target == 'test' else 'run'} and its record: logs, charts, final check and files "
237
+ f"({plural(d.get('run_files', 0), 'file')})"
238
+ )
239
+ if d.get("packages"):
240
+ lines.append(
241
+ ("The learned program's package" if d["packages"] == 1 else f"{d['packages']} learned-program packages")
242
+ + f" and {'its' if d.get('gate_results') == 1 else 'their'} final-check result"
243
+ )
244
+ aliases = summary.get("cleared_aliases") or []
245
+ for alias in aliases:
246
+ if alias.endswith(" canary"):
247
+ lines.append(f"The canary on {alias[: -len(' canary')]}")
248
+ else:
249
+ lines.append(
250
+ f"What {alias} serves: the endpoint has nothing on {alias.split('@')[1] if '@' in alias else 'it'} afterwards"
251
+ )
252
+ if d.get("canary_plans") and not any(a.endswith(" canary") for a in aliases):
253
+ lines.append(f"Its endpoint's canary records ({d['canary_plans']})")
254
+ if d.get("endpoint_keys"):
255
+ lines.append(
256
+ f"{plural(d['endpoint_keys'], 'endpoint key')}: callers using {'it' if d['endpoint_keys'] == 1 else 'them'} stop working"
257
+ )
258
+ if target == "environment":
259
+ lines.append(
260
+ "The environment's own files: tasks, tools, traces, captured conversations and labels "
261
+ f"({plural(d.get('environment_files', 0), 'file')})"
262
+ )
263
+ for kept in summary.get("kept") or []:
264
+ lines.append(f"Kept: {kept}")
265
+ return lines
266
+
267
+
268
+ def _typed(expected: str, yes: bool, question: str) -> None:
269
+ """Ask for `expected` to be typed (skipped with --yes; refused without a terminal)."""
270
+ if yes:
271
+ return
272
+ if not sys.stdin.isatty():
273
+ ui.error(f"{question} Re-run with --yes to confirm without a prompt.")
274
+ raise typer.Exit(2)
275
+ typed = typer.prompt(f"Type {expected} to confirm", default="", show_default=False)
276
+ if typed.strip().lower() != expected.lower():
277
+ ui.error("That does not match; nothing was deleted.")
278
+ raise typer.Exit(1)
279
+
280
+
281
+ @app.command("delete")
282
+ def delete(
283
+ ref: str = typer.Argument(
284
+ ..., help="An archived run's id (or the start of it, or its name), or an environment's id."
285
+ ),
286
+ environment: bool = typer.Option(False, "--env", help="It is an environment."),
287
+ run: bool = typer.Option(False, "--run", help="It is a run or test."),
288
+ yes: bool = typer.Option(False, "--yes", "-y", help="Delete without asking."),
289
+ ) -> None:
290
+ """Delete an archived run, test or environment for good (admins). Shows what would go first.
291
+
292
+ An environment takes its archived runs and tests, its endpoint (staging, keys) and its files with it.
293
+ This cannot be undone.
294
+
295
+ Example: corethink archive delete 3f2a
296
+ """
297
+ if environment and run:
298
+ raise fail("Choose --env or --run, not both.")
299
+ archive = load_archive()
300
+ kind, thing = _which(ref, archive, "environment" if environment else "run" if run else None)
301
+ if kind == "environment":
302
+ path, name, typed = (
303
+ f"{ARCHIVE}/usecases/{thing['id']}",
304
+ thing.get("name") or thing["id"],
305
+ thing["id"],
306
+ )
307
+ noun = "environment"
308
+ else:
309
+ path, name, typed = (
310
+ f"{ARCHIVE}/training-jobs/{thing['job_id']}",
311
+ run_name(thing),
312
+ str(thing["job_id"])[:8],
313
+ )
314
+ noun = "test" if thing.get("kind") == "test" else "run"
315
+ preview = client().get(f"{path}/deletion") or {}
316
+ if not preview.get("deletable", False):
317
+ raise fail(
318
+ f"{noun.capitalize()} {name} cannot be deleted now: {preview.get('reason') or 'the Workbench refused'}.",
319
+ 6,
320
+ )
321
+ summary = preview.get("summary") or {}
322
+ ui.err.print(f"Deleting {noun} {name} for good removes:")
323
+ for line in deletion_lines(summary):
324
+ ui.err.print(f" • {line}")
325
+ ui.err.print("This cannot be undone.", style="bold red")
326
+ _typed(typed, yes, f"Delete {noun} {name} for good?")
327
+ done = client().delete(path)
328
+
329
+ def human() -> None:
330
+ deleted = (done or {}).get("deleted") or {}
331
+ if kind == "environment":
332
+ ui.success(
333
+ f"Deleted {name}, with {plural(deleted.get('runs', 0), 'run')} and {plural(deleted.get('tests', 0), 'test')}."
334
+ )
335
+ else:
336
+ cleared = (done or {}).get("cleared_aliases") or []
337
+ ui.success(f"Deleted {noun} {name}" + (f"; {', '.join(cleared)} is empty now" if cleared else "") + ".")
338
+
339
+ ui.emit(done, human)
340
+
341
+
342
+ def delete_in_chunks(ids: list[str], path: str) -> dict[str, Any]:
343
+ """Delete `ids` for good, BULK_CHUNK at a time; a request that fails as a whole stops the rest."""
344
+ result: dict[str, Any] = {"asked": len(ids), "deleted": [], "failed": [], "stopped": None}
345
+ for at in range(0, len(ids), BULK_CHUNK):
346
+ chunk = ids[at : at + BULK_CHUNK]
347
+ try:
348
+ answer = client().post(path, {"ids": chunk}) or {}
349
+ except ApiError as problem:
350
+ result["stopped"] = problem.message
351
+ break
352
+ result["deleted"].extend(answer.get("deleted") or [])
353
+ result["failed"].extend(answer.get("failed") or [])
354
+ ui.info(f"Deleting… {min(len(ids), at + len(chunk))} of {len(ids)}")
355
+ return result
356
+
357
+
358
+ @app.command("delete-all")
359
+ def delete_all(
360
+ what: str = typer.Argument(..., help="runs (archived runs and tests) or environments."),
361
+ yes: bool = typer.Option(False, "--yes", "-y", help="Delete without asking."),
362
+ ) -> None:
363
+ """Delete every archived run and test, or every archived environment, for good (admins).
364
+
365
+ Any that cannot be deleted are skipped and listed. Spent credit stays in the ledger. This cannot be undone.
366
+
367
+ Example: corethink archive delete-all runs
368
+ """
369
+ if what not in ("runs", "environments"):
370
+ raise fail("Say what to delete: runs or environments.")
371
+ archive = load_archive()
372
+ if what == "runs":
373
+ items = archive["runs"]
374
+ ids = [str(r["job_id"]) for r in items]
375
+ label = runs_and_tests(sum(r.get("kind") != "test" for r in items), sum(r.get("kind") == "test" for r in items))
376
+ lines = [
377
+ "Each one's record, logs, charts, final check and files",
378
+ "Its learned program's package, unless another run or endpoint still uses it",
379
+ "What its endpoint's staging serves, when that is its package",
380
+ ]
381
+ unit, path = "runs and tests", f"{ARCHIVE}/training-jobs:delete"
382
+ name_of = {str(r["job_id"]): run_name(r) for r in items}
383
+ else:
384
+ items = archive["environments"]
385
+ ids = [str(e["id"]) for e in items]
386
+ label = plural(len(items), "environment")
387
+ runs = sum(int(e.get("runs") or 0) for e in items)
388
+ tests = sum(int(e.get("tests") or 0) for e in items)
389
+ lines = [
390
+ f"Their archived runs and tests{f' ({runs_and_tests(runs, tests)})' if runs or tests else ''}, with their logs, charts and files",
391
+ "Their endpoints: staging, canaries and keys. Callers using those keys stop working",
392
+ "Their files: tasks, tools, traces, captured conversations and labels",
393
+ ]
394
+ unit, path = "environments", f"{ARCHIVE}/usecases:delete"
395
+ name_of = {str(e["id"]): e.get("name") or e["id"] for e in items}
396
+ if not ids:
397
+ ui.info(f"No archived {what}.")
398
+ ui.emit({"asked": 0, "deleted": [], "failed": [], "stopped": None}, lambda: None)
399
+ return
400
+ ui.err.print(f"Delete {label} for good? This removes:")
401
+ for line in lines:
402
+ ui.err.print(f" • {line}")
403
+ ui.err.print(
404
+ "Any that cannot be deleted are skipped and listed. Spent credit stays in the ledger.",
405
+ style="dim",
406
+ )
407
+ ui.err.print("This cannot be undone.", style="bold red")
408
+ if what == "environments":
409
+ _typed(DELETE_ALL_WORD, yes, f"Delete {label} for good?")
410
+ else:
411
+ ui.confirm(f"Delete {label} for good?", yes=yes)
412
+ result = delete_in_chunks(ids, path)
413
+
414
+ def human() -> None:
415
+ deleted = len(result["deleted"])
416
+ part = f"{deleted} of {result['asked']} {unit}"
417
+ if result["stopped"]:
418
+ ui.error(f"Deleted {part}, then stopped: {result['stopped']}")
419
+ elif deleted == result["asked"]:
420
+ ui.success(f"Deleted {label}.")
421
+ else:
422
+ failed = len(result["failed"])
423
+ ui.warn(f"Deleted {part}. {failed} {'was' if failed == 1 else 'were'} not deleted:")
424
+ for failure in result["failed"]:
425
+ ui.err.print(f" • {name_of.get(str(failure.get('id')), failure.get('id'))}: {failure.get('reason')}")
426
+
427
+ ui.emit(result, human)
428
+ if result["stopped"] or result["failed"]:
429
+ raise typer.Exit(1)
430
+
431
+
432
+ def register(root: typer.Typer) -> None:
433
+ root.add_typer(app, name="archive", rich_help_panel="Manage")