world-model-optimizer 0.2.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.
- llm_waterfall/LICENSE +21 -0
- llm_waterfall/__init__.py +53 -0
- llm_waterfall/adapters/__init__.py +36 -0
- llm_waterfall/adapters/anthropic.py +105 -0
- llm_waterfall/adapters/aws_mantle.py +47 -0
- llm_waterfall/adapters/azure_openai.py +71 -0
- llm_waterfall/adapters/base.py +51 -0
- llm_waterfall/adapters/bedrock.py +309 -0
- llm_waterfall/adapters/openai.py +130 -0
- llm_waterfall/classify.py +184 -0
- llm_waterfall/pricing.py +110 -0
- llm_waterfall/py.typed +0 -0
- llm_waterfall/types.py +295 -0
- llm_waterfall/waterfall.py +255 -0
- wmo/__init__.py +38 -0
- wmo/agents/__init__.py +7 -0
- wmo/agents/default.py +29 -0
- wmo/agents/meta.py +55 -0
- wmo/agents/optimizer.py +55 -0
- wmo/agents/project.py +928 -0
- wmo/cli/__init__.py +5 -0
- wmo/cli/agent_session.py +1123 -0
- wmo/cli/app.py +2489 -0
- wmo/cli/e2b_cmds.py +212 -0
- wmo/cli/eval_closed_loop.py +207 -0
- wmo/cli/harness_app.py +1147 -0
- wmo/cli/harness_distill.py +659 -0
- wmo/cli/hosted_session.py +880 -0
- wmo/cli/ingest_cmd.py +165 -0
- wmo/cli/model_roles.py +82 -0
- wmo/cli/platform_cmds.py +372 -0
- wmo/cli/route_app.py +274 -0
- wmo/cli/session_state.py +243 -0
- wmo/cli/ui.py +1107 -0
- wmo/cli/workspace_sync.py +504 -0
- wmo/config/__init__.py +60 -0
- wmo/config/card.py +129 -0
- wmo/config/config.py +367 -0
- wmo/config/dotenv.py +67 -0
- wmo/config/settings.py +128 -0
- wmo/config/store.py +177 -0
- wmo/conftest.py +19 -0
- wmo/connect/__init__.py +88 -0
- wmo/connect/apps.py +78 -0
- wmo/connect/brave.py +284 -0
- wmo/connect/connector.py +79 -0
- wmo/connect/credentials.py +164 -0
- wmo/connect/github.py +321 -0
- wmo/connect/google.py +627 -0
- wmo/connect/notion.py +790 -0
- wmo/connect/oauth.py +461 -0
- wmo/connect/slack.py +555 -0
- wmo/connect/store.py +199 -0
- wmo/connect/types.py +156 -0
- wmo/core/__init__.py +21 -0
- wmo/core/parsing.py +281 -0
- wmo/core/render.py +271 -0
- wmo/core/text.py +40 -0
- wmo/core/types.py +116 -0
- wmo/distill/__init__.py +14 -0
- wmo/distill/agents.py +140 -0
- wmo/distill/config.py +1006 -0
- wmo/distill/cost.py +437 -0
- wmo/distill/data.py +921 -0
- wmo/distill/deadlines.py +254 -0
- wmo/distill/fake_tinker.py +734 -0
- wmo/distill/gate.py +122 -0
- wmo/distill/loop.py +3499 -0
- wmo/distill/renderers.py +399 -0
- wmo/distill/rendering.py +620 -0
- wmo/distill/rollouts.py +726 -0
- wmo/distill/samples.py +195 -0
- wmo/distill/store.py +829 -0
- wmo/distill/teacher.py +714 -0
- wmo/distill/tokens.py +535 -0
- wmo/distill/tracking.py +552 -0
- wmo/distill/tripwire.py +411 -0
- wmo/distill/xtoken/byte_offsets.py +152 -0
- wmo/distill/xtoken/chunks.py +457 -0
- wmo/distill/xtoken/prompt_logprobs.py +475 -0
- wmo/distill/xtoken/teacher_render.py +346 -0
- wmo/engine/__init__.py +28 -0
- wmo/engine/autoconfig.py +367 -0
- wmo/engine/build.py +346 -0
- wmo/engine/demo.py +77 -0
- wmo/engine/eval_suites.py +245 -0
- wmo/engine/grounding.py +491 -0
- wmo/engine/knowledge.py +291 -0
- wmo/engine/loader.py +36 -0
- wmo/engine/play.py +92 -0
- wmo/engine/prompts.py +99 -0
- wmo/engine/replay.py +443 -0
- wmo/engine/reporting.py +58 -0
- wmo/engine/workspace.py +468 -0
- wmo/engine/world_model.py +568 -0
- wmo/env/__init__.py +22 -0
- wmo/env/base.py +121 -0
- wmo/env/closed_loop.py +229 -0
- wmo/env/episode.py +107 -0
- wmo/env/llm_agent.py +93 -0
- wmo/env/scenarios.py +73 -0
- wmo/evals/__init__.py +52 -0
- wmo/evals/agreement.py +110 -0
- wmo/evals/base.py +45 -0
- wmo/evals/closed_loop.py +480 -0
- wmo/evals/failover.py +96 -0
- wmo/evals/gold.py +127 -0
- wmo/evals/grid.py +394 -0
- wmo/evals/grid_plot.py +205 -0
- wmo/evals/harbor/__init__.py +27 -0
- wmo/evals/harbor/agent.py +573 -0
- wmo/evals/harbor/ctrf.py +171 -0
- wmo/evals/harbor/e2b_environment.py +587 -0
- wmo/evals/harbor/e2b_template_policy.py +144 -0
- wmo/evals/harbor/scorer.py +875 -0
- wmo/evals/harbor/tasks.py +140 -0
- wmo/evals/open_loop.py +194 -0
- wmo/evals/tasks.py +53 -0
- wmo/harness/__init__.py +51 -0
- wmo/harness/code_runtime.py +288 -0
- wmo/harness/create.py +1191 -0
- wmo/harness/delta.py +220 -0
- wmo/harness/doc.py +556 -0
- wmo/harness/e2b_ledger.py +342 -0
- wmo/harness/e2b_reap.py +476 -0
- wmo/harness/e2b_sandbox.py +350 -0
- wmo/harness/environment.py +35 -0
- wmo/harness/live_session.py +543 -0
- wmo/harness/mutate.py +343 -0
- wmo/harness/pi_e2b.py +1710 -0
- wmo/harness/pi_entry/entry.ts +268 -0
- wmo/harness/pi_entry/runner_frames.ts +92 -0
- wmo/harness/pi_entry/runner_live.ts +587 -0
- wmo/harness/pi_entry/runner_service.ts +270 -0
- wmo/harness/pi_entry/runner_stdio.ts +374 -0
- wmo/harness/pi_entry/runner_termination.ts +142 -0
- wmo/harness/pi_local.py +262 -0
- wmo/harness/pi_runtime.py +495 -0
- wmo/harness/pi_vendor.py +65 -0
- wmo/harness/population.py +509 -0
- wmo/harness/project_proposer.py +569 -0
- wmo/harness/proposer.py +977 -0
- wmo/harness/runner_link.py +619 -0
- wmo/harness/runtime.py +389 -0
- wmo/harness/scoring.py +247 -0
- wmo/harness/skills.py +116 -0
- wmo/harness/source_tree.py +319 -0
- wmo/harness/store.py +176 -0
- wmo/harness/tools.py +105 -0
- wmo/harness/vendor/manifest.sha256 +58 -0
- wmo/harness/vendor/pi-agent/CHANGELOG.md +556 -0
- wmo/harness/vendor/pi-agent/LICENSE +21 -0
- wmo/harness/vendor/pi-agent/README.md +488 -0
- wmo/harness/vendor/pi-agent/VENDOR.md +39 -0
- wmo/harness/vendor/pi-agent/docs/agent-harness.md +486 -0
- wmo/harness/vendor/pi-agent/docs/durable-harness.md +212 -0
- wmo/harness/vendor/pi-agent/docs/hooks.md +445 -0
- wmo/harness/vendor/pi-agent/docs/models.md +966 -0
- wmo/harness/vendor/pi-agent/docs/observability.md +376 -0
- wmo/harness/vendor/pi-agent/package.json +60 -0
- wmo/harness/vendor/pi-agent/src/agent-loop.ts +748 -0
- wmo/harness/vendor/pi-agent/src/agent.ts +575 -0
- wmo/harness/vendor/pi-agent/src/harness/agent-harness.ts +1029 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/branch-summarization.ts +261 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/compaction.ts +747 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/utils.ts +144 -0
- wmo/harness/vendor/pi-agent/src/harness/env/nodejs.ts +550 -0
- wmo/harness/vendor/pi-agent/src/harness/messages.ts +164 -0
- wmo/harness/vendor/pi-agent/src/harness/prompt-templates.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-repo.ts +177 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-storage.ts +293 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-repo.ts +50 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-storage.ts +131 -0
- wmo/harness/vendor/pi-agent/src/harness/session/repo-utils.ts +51 -0
- wmo/harness/vendor/pi-agent/src/harness/session/session.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/uuid.ts +54 -0
- wmo/harness/vendor/pi-agent/src/harness/skills.ts +375 -0
- wmo/harness/vendor/pi-agent/src/harness/system-prompt.ts +34 -0
- wmo/harness/vendor/pi-agent/src/harness/types.ts +836 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/shell-output.ts +135 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/truncate.ts +344 -0
- wmo/harness/vendor/pi-agent/src/index.ts +44 -0
- wmo/harness/vendor/pi-agent/src/node.ts +2 -0
- wmo/harness/vendor/pi-agent/src/proxy.ts +367 -0
- wmo/harness/vendor/pi-agent/src/types.ts +428 -0
- wmo/harness/vendor/pi-agent/test/agent-loop.test.ts +1351 -0
- wmo/harness/vendor/pi-agent/test/agent.test.ts +699 -0
- wmo/harness/vendor/pi-agent/test/e2e.test.ts +404 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness-stream.test.ts +213 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness.test.ts +608 -0
- wmo/harness/vendor/pi-agent/test/harness/compaction.test.ts +655 -0
- wmo/harness/vendor/pi-agent/test/harness/nodejs-env.test.ts +321 -0
- wmo/harness/vendor/pi-agent/test/harness/prompt-templates.test.ts +90 -0
- wmo/harness/vendor/pi-agent/test/harness/repo.test.ts +68 -0
- wmo/harness/vendor/pi-agent/test/harness/resource-formatting.test.ts +24 -0
- wmo/harness/vendor/pi-agent/test/harness/session-test-utils.ts +55 -0
- wmo/harness/vendor/pi-agent/test/harness/session-uuid.test.ts +50 -0
- wmo/harness/vendor/pi-agent/test/harness/session.test.ts +156 -0
- wmo/harness/vendor/pi-agent/test/harness/skills.test.ts +116 -0
- wmo/harness/vendor/pi-agent/test/harness/storage.test.ts +299 -0
- wmo/harness/vendor/pi-agent/test/harness/system-prompt.test.ts +66 -0
- wmo/harness/vendor/pi-agent/test/harness/truncate.test.ts +169 -0
- wmo/harness/vendor/pi-agent/test/scratch/simple.ts +72 -0
- wmo/harness/vendor/pi-agent/test/utils/calculate.ts +32 -0
- wmo/harness/vendor/pi-agent/test/utils/get-current-time.ts +46 -0
- wmo/harness/vendor/pi-agent/tsconfig.build.json +13 -0
- wmo/harness/vendor/pi-agent/vitest.config.ts +19 -0
- wmo/harness/vendor/pi-agent/vitest.harness.config.ts +28 -0
- wmo/harness/vendor/vendor_pi.sh +59 -0
- wmo/harness/workspace_patch.py +270 -0
- wmo/ingest/__init__.py +47 -0
- wmo/ingest/adapter.py +72 -0
- wmo/ingest/base.py +114 -0
- wmo/ingest/braintrust.py +339 -0
- wmo/ingest/detect.py +126 -0
- wmo/ingest/langfuse.py +291 -0
- wmo/ingest/langsmith.py +444 -0
- wmo/ingest/mastra.py +330 -0
- wmo/ingest/messages.py +170 -0
- wmo/ingest/normalize.py +679 -0
- wmo/ingest/otel_genai.py +69 -0
- wmo/ingest/otel_writer.py +100 -0
- wmo/ingest/phoenix.py +150 -0
- wmo/ingest/postgres.py +246 -0
- wmo/ingest/posthog.py +320 -0
- wmo/ingest/quality.py +28 -0
- wmo/ingest/stream.py +209 -0
- wmo/ingest/testdata/sample_otlp.json +60 -0
- wmo/ingest/testdata/sample_spans.jsonl +3 -0
- wmo/optimize/__init__.py +25 -0
- wmo/optimize/base.py +143 -0
- wmo/optimize/gepa.py +806 -0
- wmo/optimize/judge.py +262 -0
- wmo/optimize/judge_quality.py +359 -0
- wmo/optimize/knn.py +468 -0
- wmo/optimize/numeric.py +152 -0
- wmo/optimize/outcomes.py +103 -0
- wmo/optimize/policy.py +669 -0
- wmo/optimize/report.py +231 -0
- wmo/optimize/reward.py +129 -0
- wmo/optimize/routing.py +373 -0
- wmo/platform/__init__.py +6 -0
- wmo/platform/auth.py +115 -0
- wmo/platform/client.py +551 -0
- wmo/platform/credentials.py +126 -0
- wmo/platform/transfer.py +158 -0
- wmo/providers/__init__.py +40 -0
- wmo/providers/_bedrock_chat.py +155 -0
- wmo/providers/_openai_common.py +182 -0
- wmo/providers/_responses_common.py +472 -0
- wmo/providers/anthropic.py +134 -0
- wmo/providers/azure_openai.py +296 -0
- wmo/providers/base.py +300 -0
- wmo/providers/bedrock.py +312 -0
- wmo/providers/models.py +205 -0
- wmo/providers/openai.py +143 -0
- wmo/providers/openai_responses.py +240 -0
- wmo/providers/pool.py +170 -0
- wmo/providers/registry.py +73 -0
- wmo/providers/retry.py +151 -0
- wmo/providers/tinker.py +936 -0
- wmo/providers/waterfall.py +336 -0
- wmo/research/__init__.py +81 -0
- wmo/research/ablation.py +133 -0
- wmo/research/concurrency_plot.py +523 -0
- wmo/research/concurrency_run.py +240 -0
- wmo/research/concurrency_scaling.py +270 -0
- wmo/research/gepa_scaling.py +274 -0
- wmo/research/pipeline.py +198 -0
- wmo/research/scaling_split.py +82 -0
- wmo/research/scenario_fidelity.py +198 -0
- wmo/research/scenario_recovery.py +92 -0
- wmo/research/seed_stability.py +90 -0
- wmo/research/trace_scaling.py +348 -0
- wmo/retrieval/__init__.py +6 -0
- wmo/retrieval/embedders.py +105 -0
- wmo/retrieval/leakfree.py +52 -0
- wmo/retrieval/retriever.py +173 -0
- wmo/scenarios/__init__.py +58 -0
- wmo/scenarios/builder.py +152 -0
- wmo/scenarios/mining/__init__.py +27 -0
- wmo/scenarios/mining/clustering.py +171 -0
- wmo/scenarios/mining/facets.py +226 -0
- wmo/scenarios/mining/selection.py +220 -0
- wmo/scenarios/synthesis/__init__.py +6 -0
- wmo/scenarios/synthesis/scenario_set.py +63 -0
- wmo/scenarios/synthesis/synthesizer.py +85 -0
- wmo/scenarios/verification/__init__.py +17 -0
- wmo/scenarios/verification/judge.py +97 -0
- wmo/scenarios/verification/verify.py +135 -0
- wmo/serving/__init__.py +5 -0
- wmo/serving/builds.py +451 -0
- wmo/serving/chat.py +878 -0
- wmo/serving/endpoint_config.py +64 -0
- wmo/serving/savings.py +250 -0
- wmo/serving/server.py +553 -0
- wmo/serving/traces_source.py +206 -0
- wmo/telemetry.py +213 -0
- wmo/tracking/__init__.py +36 -0
- wmo/tracking/clock.py +24 -0
- wmo/tracking/metered.py +125 -0
- wmo/tracking/pricing.py +99 -0
- wmo/tracking/store.py +31 -0
- wmo/tracking/tracker.py +149 -0
- world_model_optimizer-0.2.0.dist-info/METADATA +203 -0
- world_model_optimizer-0.2.0.dist-info/RECORD +308 -0
- world_model_optimizer-0.2.0.dist-info/WHEEL +4 -0
- world_model_optimizer-0.2.0.dist-info/entry_points.txt +2 -0
wmo/cli/ingest_cmd.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""`wmo ingest`: normalize traces from any source into OTel-GenAI JSONL, with live progress.
|
|
2
|
+
|
|
3
|
+
The standalone half of what `wmo build` does first: pick a source (auto-detected for files),
|
|
4
|
+
normalize to the harness's OTel span JSONL, and report progress. The output feeds any downstream
|
|
5
|
+
command (`wmo build --source otel-genai --file <out>`), and the same event stream drives the
|
|
6
|
+
platform's SSE trace-ingest endpoint (`wmo.ingest.stream.ingest_events`), so the CLI and the
|
|
7
|
+
hosted flow can't drift.
|
|
8
|
+
|
|
9
|
+
Output modes: a rich progress bar on a TTY, and `--json` for one D-INGEST event object per line
|
|
10
|
+
(machine-readable; what a wrapping process pipes to a wire).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from collections.abc import Iterator
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import typer
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.progress import BarColumn, Progress, TextColumn
|
|
22
|
+
|
|
23
|
+
from wmo.ingest import VendorPull, list_adapters
|
|
24
|
+
from wmo.ingest.stream import (
|
|
25
|
+
DetectedEvent,
|
|
26
|
+
DoneEvent,
|
|
27
|
+
ErrorEvent,
|
|
28
|
+
IngestEvent,
|
|
29
|
+
ProgressEvent,
|
|
30
|
+
event_json,
|
|
31
|
+
ingest_events,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_console = Console()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _default_out(file: str | None, source: str | None) -> Path:
|
|
38
|
+
if file is not None:
|
|
39
|
+
return Path(f"{Path(file).stem}.otel.jsonl")
|
|
40
|
+
return Path(f"{source or 'traces'}.otel.jsonl")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _render_rich(stream: Iterator[IngestEvent]) -> bool:
|
|
44
|
+
"""Consume the event stream behind a progress bar; returns True on success."""
|
|
45
|
+
ok = False
|
|
46
|
+
with Progress(
|
|
47
|
+
TextColumn("[progress.description]{task.description}"),
|
|
48
|
+
BarColumn(),
|
|
49
|
+
TextColumn("{task.completed}/{task.total} traces"),
|
|
50
|
+
console=_console,
|
|
51
|
+
) as progress:
|
|
52
|
+
task_id = None
|
|
53
|
+
for event in stream:
|
|
54
|
+
if isinstance(event, DetectedEvent):
|
|
55
|
+
_console.print(f"detected [bold]{event.format}[/bold] · {event.traces} traces")
|
|
56
|
+
task_id = progress.add_task("normalizing", total=event.traces)
|
|
57
|
+
elif isinstance(event, ProgressEvent) and task_id is not None:
|
|
58
|
+
progress.update(task_id, completed=event.normalized)
|
|
59
|
+
if event.note:
|
|
60
|
+
progress.update(task_id, description=event.note)
|
|
61
|
+
elif isinstance(event, DoneEvent):
|
|
62
|
+
ok = True
|
|
63
|
+
progress.stop()
|
|
64
|
+
_console.print(
|
|
65
|
+
f"[green]done[/green] · {event.traces} traces / {event.steps} steps "
|
|
66
|
+
f"→ [bold]{event.otel_object}[/bold]"
|
|
67
|
+
)
|
|
68
|
+
_console.print(
|
|
69
|
+
f"build from it: wmo build --name <name> --source otel-genai "
|
|
70
|
+
f"--file {event.otel_object}"
|
|
71
|
+
)
|
|
72
|
+
elif isinstance(event, ErrorEvent):
|
|
73
|
+
progress.stop()
|
|
74
|
+
code = f" [{event.code}]" if event.code else ""
|
|
75
|
+
_console.print(f"[red]error{code}[/red] {event.message}")
|
|
76
|
+
return ok
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def ingest(
|
|
80
|
+
file: str = typer.Option(
|
|
81
|
+
None, "--file", help="Path to an exported traces file (format auto-detected)."
|
|
82
|
+
),
|
|
83
|
+
source: str = typer.Option(
|
|
84
|
+
None,
|
|
85
|
+
"--source",
|
|
86
|
+
help="Trace source adapter (default: auto-detect from the file). One of: "
|
|
87
|
+
"otel-genai, chat-json, braintrust, phoenix, langfuse, langsmith, posthog, mastra, "
|
|
88
|
+
"postgres.",
|
|
89
|
+
),
|
|
90
|
+
pull: bool = typer.Option(
|
|
91
|
+
False, "--pull", help="Pull traces live from the source's API (instead of --file)."
|
|
92
|
+
),
|
|
93
|
+
project: str = typer.Option(None, "--project", help="Vendor project/workspace id (--pull)."),
|
|
94
|
+
api_key: str = typer.Option(None, "--api-key", help="Vendor API key (else env var)."),
|
|
95
|
+
since: str = typer.Option(None, "--since", help="Only pull traces since this ISO timestamp."),
|
|
96
|
+
limit: int = typer.Option(None, "--limit", help="Max number of traces to ingest."),
|
|
97
|
+
dsn: str = typer.Option(
|
|
98
|
+
None, "--dsn", help="Postgres connection string (implies --source postgres)."
|
|
99
|
+
),
|
|
100
|
+
table: str = typer.Option(None, "--table", help="Postgres table holding the trace rows."),
|
|
101
|
+
trace_id_column: str = typer.Option(
|
|
102
|
+
None, "--trace-id-column", help="Postgres column grouping rows into traces."
|
|
103
|
+
),
|
|
104
|
+
payload_column: str = typer.Option(
|
|
105
|
+
None, "--payload-column", help="Postgres JSON column holding the trace payloads."
|
|
106
|
+
),
|
|
107
|
+
order_column: str = typer.Option(
|
|
108
|
+
None, "--order-column", help="Postgres column ordering rows (default: created_at)."
|
|
109
|
+
),
|
|
110
|
+
out: str = typer.Option(
|
|
111
|
+
None, "--out", help="Output OTel JSONL path (default: <input>.otel.jsonl)."
|
|
112
|
+
),
|
|
113
|
+
json_events: bool = typer.Option(
|
|
114
|
+
False, "--json", help="Emit one JSON progress event per line instead of the rich UI."
|
|
115
|
+
),
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Normalize traces from a file, vendor API, or Postgres table into OTel JSONL.
|
|
118
|
+
|
|
119
|
+
No model is built: the output corpus feeds `wmo build --source otel-genai --file <out>` (or
|
|
120
|
+
any other command that reads a trace file). Progress events follow the D-INGEST vocabulary:
|
|
121
|
+
detected -> progress... -> done | error.
|
|
122
|
+
"""
|
|
123
|
+
if (dsn is not None or table is not None) and source is None:
|
|
124
|
+
source = "postgres"
|
|
125
|
+
is_pull = pull or dsn is not None or table is not None
|
|
126
|
+
if file is None and not is_pull:
|
|
127
|
+
raise typer.BadParameter(
|
|
128
|
+
"provide --file <export>, --pull (with --source), or --dsn/--table for postgres"
|
|
129
|
+
)
|
|
130
|
+
if file is not None and is_pull:
|
|
131
|
+
raise typer.BadParameter("pass either --file or a pull (--pull/--dsn/--table), not both")
|
|
132
|
+
if source is not None and source not in list_adapters():
|
|
133
|
+
raise typer.BadParameter(
|
|
134
|
+
f"unknown --source {source!r}; choose one of: {', '.join(list_adapters())}"
|
|
135
|
+
)
|
|
136
|
+
out_path = Path(out) if out else _default_out(file, source)
|
|
137
|
+
vendor_pull = (
|
|
138
|
+
VendorPull(
|
|
139
|
+
api_key=api_key,
|
|
140
|
+
project=project,
|
|
141
|
+
since=since,
|
|
142
|
+
limit=limit,
|
|
143
|
+
dsn=dsn,
|
|
144
|
+
table=table,
|
|
145
|
+
trace_id_column=trace_id_column,
|
|
146
|
+
payload_column=payload_column,
|
|
147
|
+
order_column=order_column,
|
|
148
|
+
)
|
|
149
|
+
if is_pull
|
|
150
|
+
else None
|
|
151
|
+
)
|
|
152
|
+
stream = ingest_events(file=file, pull=vendor_pull, source=source, out=out_path, limit=limit)
|
|
153
|
+
|
|
154
|
+
if json_events:
|
|
155
|
+
ok = False
|
|
156
|
+
for event in stream:
|
|
157
|
+
# soft_wrap: machine-readable lines must never be broken at terminal width.
|
|
158
|
+
_console.print(
|
|
159
|
+
json.dumps(event_json(event)), markup=False, highlight=False, soft_wrap=True
|
|
160
|
+
)
|
|
161
|
+
ok = isinstance(event, DoneEvent) or (ok and not isinstance(event, ErrorEvent))
|
|
162
|
+
else:
|
|
163
|
+
ok = _render_rich(stream)
|
|
164
|
+
if not ok:
|
|
165
|
+
raise typer.Exit(code=1)
|
wmo/cli/model_roles.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Provider resolution for opt-in model roles shared by CLI workflows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from wmo.config.settings import ModelRole, load_settings
|
|
10
|
+
from wmo.providers.base import Provider, ProviderConfig, ProviderKind
|
|
11
|
+
from wmo.providers.registry import get_provider
|
|
12
|
+
|
|
13
|
+
OptInModelRole = Literal["agent", "meta"]
|
|
14
|
+
|
|
15
|
+
# Azure OpenAI chat completions need an API version on every call. When an opt-in role does not
|
|
16
|
+
# pin one in settings, this shared default API version applies.
|
|
17
|
+
_DEFAULT_AZURE_API_VERSION = "2024-05-01-preview"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def resolve_opt_in_model_provider(
|
|
21
|
+
root: str,
|
|
22
|
+
role: OptInModelRole,
|
|
23
|
+
fallback: Provider,
|
|
24
|
+
) -> tuple[Provider, str | None]:
|
|
25
|
+
"""Resolve one opt-in model role, or return the caller's fallback provider.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
root: Project artifact root containing ``settings.toml``.
|
|
29
|
+
role: The opt-in role to resolve.
|
|
30
|
+
fallback: Provider retained when the role is not configured.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The resolved provider and configured model name, or the fallback and ``None``.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
typer.BadParameter: The configured provider kind is unknown.
|
|
37
|
+
"""
|
|
38
|
+
configured = load_settings(root).models.resolve(role)
|
|
39
|
+
if configured is None:
|
|
40
|
+
return fallback, None
|
|
41
|
+
config = _model_config(configured, role=role)
|
|
42
|
+
return get_provider(config), configured.model
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def resolve_required_model_config(root: str, role: OptInModelRole) -> ProviderConfig:
|
|
46
|
+
"""Resolve one opt-in role a workflow requires (no fallback provider exists for it).
|
|
47
|
+
|
|
48
|
+
The harbor optimize flow has no world model whose provider could stand in, so its
|
|
49
|
+
``agent`` (worker) and ``meta`` (proposer) roles must be configured explicitly.
|
|
50
|
+
"""
|
|
51
|
+
configured = load_settings(root).models.resolve(role)
|
|
52
|
+
if configured is None:
|
|
53
|
+
raise typer.BadParameter(
|
|
54
|
+
f"settings [models.{role}] must be configured in <root>/settings.toml for this "
|
|
55
|
+
f"workflow; add a [models.{role}] table with provider and model"
|
|
56
|
+
)
|
|
57
|
+
return _model_config(configured, role=role)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _model_config(configured: ModelRole, *, role: OptInModelRole) -> ProviderConfig:
|
|
61
|
+
"""Turn one configured role into provider-neutral config with the Azure default."""
|
|
62
|
+
try:
|
|
63
|
+
kind = ProviderKind(configured.provider)
|
|
64
|
+
except ValueError:
|
|
65
|
+
kinds = ", ".join(kind.value for kind in ProviderKind)
|
|
66
|
+
raise typer.BadParameter(
|
|
67
|
+
f"settings [models.{role}] has unknown provider {configured.provider!r}; "
|
|
68
|
+
f"choose one of: {kinds}"
|
|
69
|
+
) from None
|
|
70
|
+
api_version = configured.api_version
|
|
71
|
+
if api_version is None and kind is ProviderKind.AZURE_OPENAI:
|
|
72
|
+
api_version = _DEFAULT_AZURE_API_VERSION
|
|
73
|
+
return ProviderConfig(
|
|
74
|
+
kind=kind,
|
|
75
|
+
model=configured.model,
|
|
76
|
+
model_type=configured.model_type,
|
|
77
|
+
region=configured.region,
|
|
78
|
+
endpoint=configured.endpoint,
|
|
79
|
+
deployment=configured.deployment,
|
|
80
|
+
api_version=api_version,
|
|
81
|
+
reasoning_effort=configured.reasoning_effort,
|
|
82
|
+
)
|
wmo/cli/platform_cmds.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Platform commands: `wmo login`, `wmo logout`, `wmo status`, `wmo push`, `wmo pull`.
|
|
2
|
+
|
|
3
|
+
`login` connects this machine to a platform account (browser flow by default,
|
|
4
|
+
`--token` for headless); `push`/`pull` round-trip world models and harnesses
|
|
5
|
+
against the platform registry, auto-detecting the artifact kind from what
|
|
6
|
+
exists locally (or remotely, for pulls).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import socket
|
|
12
|
+
import tempfile
|
|
13
|
+
import webbrowser
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Annotated
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
from wmo.config.store import WorldModelStore
|
|
22
|
+
from wmo.harness.doc import HarnessDoc
|
|
23
|
+
from wmo.harness.store import CHAMPION_ALIAS, HarnessStore
|
|
24
|
+
from wmo.platform.auth import BrowserLogin
|
|
25
|
+
from wmo.platform.client import PlatformClient, PlatformError, WhoAmI, fetch_cli_config
|
|
26
|
+
from wmo.platform.credentials import (
|
|
27
|
+
DEFAULT_WEB_URL,
|
|
28
|
+
PlatformCredentials,
|
|
29
|
+
clear_credentials,
|
|
30
|
+
credentials_path,
|
|
31
|
+
load_credentials,
|
|
32
|
+
save_credentials,
|
|
33
|
+
)
|
|
34
|
+
from wmo.platform.transfer import extract_push_meta, pack_model_dir, unpack_model_bundle
|
|
35
|
+
|
|
36
|
+
_console = Console()
|
|
37
|
+
_CHECK = "[green]✓[/green]"
|
|
38
|
+
|
|
39
|
+
# Module-level singletons: typer.Option calls can't be defaults inline (ruff B008).
|
|
40
|
+
# Annotated-style options carry no default here; the parameter's own `=` does.
|
|
41
|
+
_LOGIN_URL = typer.Option(
|
|
42
|
+
"--url", help="Platform URL (defaults to the saved one, then the hosted platform)."
|
|
43
|
+
)
|
|
44
|
+
_LOGIN_API_URL = typer.Option(
|
|
45
|
+
"--api-url",
|
|
46
|
+
help="Platform API URL (skips web discovery; useful for protected previews).",
|
|
47
|
+
)
|
|
48
|
+
_LOGIN_TOKEN = typer.Option(
|
|
49
|
+
"--token", help="Paste an existing API key instead of using the browser."
|
|
50
|
+
)
|
|
51
|
+
_LOGIN_NO_BROWSER = typer.Option(
|
|
52
|
+
"--no-browser", help="Print the authorization URL instead of opening a browser."
|
|
53
|
+
)
|
|
54
|
+
_ORG = typer.Option("--org", help="Organization id (defaults to the login's default organization).")
|
|
55
|
+
_KIND = typer.Option(
|
|
56
|
+
"--kind", help="Artifact kind: model or harness (auto-detected when unambiguous)."
|
|
57
|
+
)
|
|
58
|
+
_PUSH_AS = typer.Option(
|
|
59
|
+
"--as", help="Remote name to publish under (local names may not be slug-safe)."
|
|
60
|
+
)
|
|
61
|
+
_PUSH_REF = typer.Option(
|
|
62
|
+
"--ref", help="Harness version or alias to push (default: champion, else latest)."
|
|
63
|
+
)
|
|
64
|
+
_PULL_VERSION = typer.Option("--version", help="Harness version to pull (default: latest).")
|
|
65
|
+
_PULL_FORCE = typer.Option("--force", help="Replace an existing local artifact.")
|
|
66
|
+
_ROOT = typer.Option("--root", help="Artifact root directory.")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def login(
|
|
70
|
+
url: Annotated[str | None, _LOGIN_URL] = None,
|
|
71
|
+
api_url: Annotated[str | None, _LOGIN_API_URL] = None,
|
|
72
|
+
token: Annotated[str | None, _LOGIN_TOKEN] = None,
|
|
73
|
+
no_browser: Annotated[bool, _LOGIN_NO_BROWSER] = False,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Connect this machine to a platform account."""
|
|
76
|
+
credentials = load_credentials()
|
|
77
|
+
web_url = (url or credentials.web_url or DEFAULT_WEB_URL).rstrip("/")
|
|
78
|
+
|
|
79
|
+
if api_url is None:
|
|
80
|
+
try:
|
|
81
|
+
api_url = fetch_cli_config(web_url)
|
|
82
|
+
except PlatformError as error:
|
|
83
|
+
raise typer.BadParameter(f"{web_url} does not look like a platform: {error}") from error
|
|
84
|
+
if api_url is None:
|
|
85
|
+
raise typer.BadParameter(f"{web_url} did not advertise a backend URL; is it deployed?")
|
|
86
|
+
else:
|
|
87
|
+
api_url = api_url.rstrip("/")
|
|
88
|
+
|
|
89
|
+
if token is None:
|
|
90
|
+
token = _browser_login(web_url, open_browser=not no_browser)
|
|
91
|
+
if token is None or not token.strip():
|
|
92
|
+
_console.print("[red]No key received; nothing saved.[/red]")
|
|
93
|
+
raise typer.Exit(code=1)
|
|
94
|
+
token = token.strip()
|
|
95
|
+
|
|
96
|
+
with PlatformClient(api_url, token) as client:
|
|
97
|
+
try:
|
|
98
|
+
identity = client.whoami()
|
|
99
|
+
except PlatformError as error:
|
|
100
|
+
_console.print(f"[red]The key was rejected:[/red] {error}")
|
|
101
|
+
raise typer.Exit(code=1) from error
|
|
102
|
+
|
|
103
|
+
# A relogin may land on a different account: keep the saved default
|
|
104
|
+
# organization only if the new identity can still see it.
|
|
105
|
+
visible_orgs = {org.id for org in identity.orgs}
|
|
106
|
+
default_org = credentials.default_org if credentials.default_org in visible_orgs else None
|
|
107
|
+
if default_org is None and len(identity.orgs) == 1:
|
|
108
|
+
default_org = identity.orgs[0].id
|
|
109
|
+
updated = credentials.model_copy(
|
|
110
|
+
update={
|
|
111
|
+
"web_url": web_url,
|
|
112
|
+
"api_url": api_url,
|
|
113
|
+
"token": token,
|
|
114
|
+
"default_org": default_org,
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
path = save_credentials(updated)
|
|
118
|
+
org_names = ", ".join(org.name for org in identity.orgs) or "no organizations"
|
|
119
|
+
_console.print(f"{_CHECK} Connected to [bold]{org_names}[/bold] ({path})")
|
|
120
|
+
_print_orgs(identity, updated.default_org)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def logout() -> None:
|
|
124
|
+
"""Disconnect: delete the saved credential."""
|
|
125
|
+
credentials = load_credentials()
|
|
126
|
+
removed = clear_credentials()
|
|
127
|
+
if not removed:
|
|
128
|
+
_console.print("Not logged in; nothing to remove.")
|
|
129
|
+
return
|
|
130
|
+
_console.print(f"{_CHECK} Logged out.")
|
|
131
|
+
if credentials.token:
|
|
132
|
+
_console.print("The key itself stays valid until revoked on the platform's API keys page.")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def status() -> None:
|
|
136
|
+
"""Show the platform connection: account and organizations."""
|
|
137
|
+
credentials = load_credentials()
|
|
138
|
+
if not credentials.is_complete():
|
|
139
|
+
_console.print(
|
|
140
|
+
f"Not connected (no credential at {credentials_path()}). Run [bold]wmo login[/bold]."
|
|
141
|
+
)
|
|
142
|
+
raise typer.Exit(code=1)
|
|
143
|
+
with _client(credentials) as client:
|
|
144
|
+
try:
|
|
145
|
+
identity = client.whoami()
|
|
146
|
+
except PlatformError as error:
|
|
147
|
+
_console.print(f"[red]Connection check failed:[/red] {error}")
|
|
148
|
+
raise typer.Exit(code=1) from error
|
|
149
|
+
_console.print(f"{_CHECK} Connected to [bold]{credentials.web_url}[/bold]")
|
|
150
|
+
_console.print(f" acting as: {identity.actor.kind} {identity.actor.id}")
|
|
151
|
+
_print_orgs(identity, credentials.default_org)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def push(
|
|
155
|
+
name: Annotated[str, typer.Argument(help="Local world model or harness name.")],
|
|
156
|
+
org: Annotated[str | None, _ORG] = None,
|
|
157
|
+
kind: Annotated[str | None, _KIND] = None,
|
|
158
|
+
push_as: Annotated[str | None, _PUSH_AS] = None,
|
|
159
|
+
ref: Annotated[str | None, _PUSH_REF] = None,
|
|
160
|
+
root: Annotated[str, _ROOT] = ".wmo",
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Publish a local world model or harness to the platform registry."""
|
|
163
|
+
model_dir = WorldModelStore(root).dir_for(name)
|
|
164
|
+
harness_exists = HarnessStore(root).exists(name)
|
|
165
|
+
resolved_kind = _resolve_kind(kind, model=model_dir is not None, harness=harness_exists)
|
|
166
|
+
remote_name = push_as or name
|
|
167
|
+
|
|
168
|
+
credentials, org_id = _require_connection(org)
|
|
169
|
+
with _client(credentials) as client:
|
|
170
|
+
if resolved_kind == "model" and model_dir is not None:
|
|
171
|
+
_push_model(client, org_id, remote_name, model_dir)
|
|
172
|
+
else:
|
|
173
|
+
_push_harness(client, org_id, remote_name, name, ref, root)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def pull(
|
|
177
|
+
name: Annotated[str, typer.Argument(help="Remote world model or harness name.")],
|
|
178
|
+
org: Annotated[str | None, _ORG] = None,
|
|
179
|
+
kind: Annotated[str | None, _KIND] = None,
|
|
180
|
+
version: Annotated[int | None, _PULL_VERSION] = None,
|
|
181
|
+
force: Annotated[bool, _PULL_FORCE] = False,
|
|
182
|
+
root: Annotated[str, _ROOT] = ".wmo",
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Fetch a world model or harness from the platform registry."""
|
|
185
|
+
if kind is not None and kind not in ("model", "harness"):
|
|
186
|
+
raise typer.BadParameter("--kind must be 'model' or 'harness'")
|
|
187
|
+
credentials, org_id = _require_connection(org)
|
|
188
|
+
with _client(credentials) as client:
|
|
189
|
+
resolved_kind = kind or _detect_remote_kind(client, org_id, name)
|
|
190
|
+
if resolved_kind == "model":
|
|
191
|
+
_pull_model(client, org_id, name, root, force=force)
|
|
192
|
+
else:
|
|
193
|
+
_pull_harness(client, org_id, name, root, version=version)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# -- helpers -------------------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _browser_login(web_url: str, *, open_browser: bool) -> str | None:
|
|
200
|
+
"""Run the loopback browser flow; fall back to a hidden paste prompt."""
|
|
201
|
+
login_attempt = BrowserLogin(web_url)
|
|
202
|
+
try:
|
|
203
|
+
login_attempt.start()
|
|
204
|
+
key_name = f"wmo on {socket.gethostname()}"
|
|
205
|
+
authorize_url = login_attempt.authorize_url(key_name=key_name)
|
|
206
|
+
_console.print(f"Approve the request in your browser:\n [bold]{authorize_url}[/bold]")
|
|
207
|
+
if open_browser:
|
|
208
|
+
webbrowser.open(authorize_url)
|
|
209
|
+
token = login_attempt.wait()
|
|
210
|
+
finally:
|
|
211
|
+
login_attempt.close()
|
|
212
|
+
if token is None:
|
|
213
|
+
_console.print("Timed out waiting for the browser.")
|
|
214
|
+
return typer.prompt("Paste an API key instead", hide_input=True, default="") or None
|
|
215
|
+
return token
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _client(credentials: PlatformCredentials) -> PlatformClient:
|
|
219
|
+
if credentials.api_url is None or credentials.token is None:
|
|
220
|
+
raise typer.BadParameter("not connected to a platform; run `wmo login` first")
|
|
221
|
+
return PlatformClient(credentials.api_url, credentials.token)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _require_connection(org: str | None) -> tuple[PlatformCredentials, str]:
|
|
225
|
+
credentials = load_credentials()
|
|
226
|
+
if not credentials.is_complete():
|
|
227
|
+
raise typer.BadParameter("not connected to a platform; run `wmo login` first")
|
|
228
|
+
org_id = org or credentials.default_org
|
|
229
|
+
if not org_id:
|
|
230
|
+
raise typer.BadParameter(
|
|
231
|
+
"no organization selected; pass --org <id> (see `wmo status` for your organizations)"
|
|
232
|
+
)
|
|
233
|
+
return credentials, org_id
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _resolve_kind(kind: str | None, *, model: bool, harness: bool) -> str:
|
|
237
|
+
if kind is not None:
|
|
238
|
+
if kind not in ("model", "harness"):
|
|
239
|
+
raise typer.BadParameter("--kind must be 'model' or 'harness'")
|
|
240
|
+
if kind == "model" and not model:
|
|
241
|
+
raise typer.BadParameter("no local world model has this name")
|
|
242
|
+
if kind == "harness" and not harness:
|
|
243
|
+
raise typer.BadParameter("no local harness has this name")
|
|
244
|
+
return kind
|
|
245
|
+
if model and harness:
|
|
246
|
+
raise typer.BadParameter("both a model and a harness have this name locally; pass --kind")
|
|
247
|
+
if model:
|
|
248
|
+
return "model"
|
|
249
|
+
if harness:
|
|
250
|
+
return "harness"
|
|
251
|
+
raise typer.BadParameter("no local world model or harness has this name")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _detect_remote_kind(client: PlatformClient, org_id: str, name: str) -> str:
|
|
255
|
+
model_names = {model.name for model in client.list_world_models(org_id)}
|
|
256
|
+
harness_names = {harness.name for harness in client.list_harnesses(org_id)}
|
|
257
|
+
if name in model_names and name in harness_names:
|
|
258
|
+
raise typer.BadParameter("both a model and a harness have this name remotely; pass --kind")
|
|
259
|
+
if name in model_names:
|
|
260
|
+
return "model"
|
|
261
|
+
if name in harness_names:
|
|
262
|
+
return "harness"
|
|
263
|
+
raise typer.BadParameter(f"the organization has no world model or harness named {name!r}")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _push_model(client: PlatformClient, org_id: str, remote_name: str, model_dir: Path) -> None:
|
|
267
|
+
meta = extract_push_meta(model_dir)
|
|
268
|
+
with tempfile.TemporaryDirectory(prefix="wmo-push-") as staging:
|
|
269
|
+
bundle = pack_model_dir(model_dir, Path(staging) / f"{remote_name}.tar.gz")
|
|
270
|
+
try:
|
|
271
|
+
pushed = client.push_model_bundle(
|
|
272
|
+
org_id,
|
|
273
|
+
remote_name,
|
|
274
|
+
bundle.path,
|
|
275
|
+
bundle.sha256,
|
|
276
|
+
bundle.byte_size,
|
|
277
|
+
meta,
|
|
278
|
+
)
|
|
279
|
+
except PlatformError as error:
|
|
280
|
+
if error.status_code == 422 and "name" in str(error):
|
|
281
|
+
raise typer.BadParameter(
|
|
282
|
+
f"{error} — publish under a slug-safe name with --as"
|
|
283
|
+
) from error
|
|
284
|
+
raise
|
|
285
|
+
_console.print(
|
|
286
|
+
f"{_CHECK} Pushed world model [bold]{pushed.name}[/bold] "
|
|
287
|
+
f"({bundle.byte_size:,} bytes, sha256 {bundle.sha256[:12]}…)"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _push_harness(
|
|
292
|
+
client: PlatformClient,
|
|
293
|
+
org_id: str,
|
|
294
|
+
remote_name: str,
|
|
295
|
+
local_name: str,
|
|
296
|
+
ref: str | None,
|
|
297
|
+
root: str,
|
|
298
|
+
) -> None:
|
|
299
|
+
doc = HarnessStore(root).load(local_name, ref)
|
|
300
|
+
if remote_name != local_name:
|
|
301
|
+
doc = doc.model_copy(update={"name": remote_name})
|
|
302
|
+
pushed = client.push_harness_version(
|
|
303
|
+
org_id, remote_name, doc.model_dump(mode="json"), doc.doc_hash
|
|
304
|
+
)
|
|
305
|
+
if pushed.created:
|
|
306
|
+
_console.print(
|
|
307
|
+
f"{_CHECK} Pushed harness [bold]{pushed.name}[/bold] as remote v{pushed.version}"
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
_console.print(
|
|
311
|
+
f"Remote [bold]{pushed.name}[/bold] v{pushed.version} already has this exact doc; "
|
|
312
|
+
"nothing to push."
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _pull_model(client: PlatformClient, org_id: str, name: str, root: str, *, force: bool) -> None:
|
|
317
|
+
dest_dir = WorldModelStore(root).model_dir(name)
|
|
318
|
+
with tempfile.TemporaryDirectory(prefix="wmo-pull-") as staging:
|
|
319
|
+
bundle_path = Path(staging) / f"{name}.tar.gz"
|
|
320
|
+
client.download_model_bundle(org_id, name, bundle_path)
|
|
321
|
+
try:
|
|
322
|
+
unpack_model_bundle(bundle_path, dest_dir, force=force)
|
|
323
|
+
except FileExistsError as error:
|
|
324
|
+
raise typer.BadParameter(str(error)) from error
|
|
325
|
+
_console.print(f"{_CHECK} Pulled world model [bold]{name}[/bold] into {dest_dir}")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _pull_harness(
|
|
329
|
+
client: PlatformClient, org_id: str, name: str, root: str, *, version: int | None
|
|
330
|
+
) -> None:
|
|
331
|
+
if version is None:
|
|
332
|
+
harness, _versions = client.get_harness(org_id, name)
|
|
333
|
+
if harness.latest_version < 1:
|
|
334
|
+
raise typer.BadParameter(f"remote harness {name!r} has no versions yet")
|
|
335
|
+
version = harness.latest_version
|
|
336
|
+
payload = client.get_harness_version(org_id, name, version)
|
|
337
|
+
doc = HarnessDoc.model_validate(payload.doc)
|
|
338
|
+
# Versions the platform recorded before doc_hash covered materialized paths carry the legacy
|
|
339
|
+
# hash; accept either so pre-change pathful (pi-node) records stay pullable.
|
|
340
|
+
if payload.doc_hash not in (doc.doc_hash, doc.legacy_doc_hash):
|
|
341
|
+
_console.print("[red]Pulled doc failed its integrity check; not saving.[/red]")
|
|
342
|
+
raise typer.Exit(code=1)
|
|
343
|
+
saved = HarnessStore(root).save_version(
|
|
344
|
+
doc.model_copy(update={"name": name}), alias=CHAMPION_ALIAS
|
|
345
|
+
)
|
|
346
|
+
_console.print(
|
|
347
|
+
f"{_CHECK} Pulled harness [bold]{name}[/bold] remote v{version} → local v{saved.version} "
|
|
348
|
+
f"(champion)"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _print_orgs(identity: WhoAmI, default_org: str | None) -> None:
|
|
353
|
+
if not identity.orgs:
|
|
354
|
+
_console.print(" no organizations visible to this key")
|
|
355
|
+
return
|
|
356
|
+
table = Table(show_header=True, header_style="bold")
|
|
357
|
+
table.add_column("organization")
|
|
358
|
+
table.add_column("id")
|
|
359
|
+
table.add_column("")
|
|
360
|
+
for org in identity.orgs:
|
|
361
|
+
marker = "default" if org.id == default_org else ""
|
|
362
|
+
table.add_row(org.name, org.id, marker)
|
|
363
|
+
_console.print(table)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def register(app: typer.Typer) -> None:
|
|
367
|
+
"""Attach the platform commands to the root CLI."""
|
|
368
|
+
app.command("login")(login)
|
|
369
|
+
app.command("logout")(logout)
|
|
370
|
+
app.command("status")(status)
|
|
371
|
+
app.command("push")(push)
|
|
372
|
+
app.command("pull")(pull)
|