uro-cli 0.6.0__tar.gz → 0.7.0__tar.gz
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.
- {uro_cli-0.6.0 → uro_cli-0.7.0}/PKG-INFO +3 -3
- {uro_cli-0.6.0 → uro_cli-0.7.0}/pyproject.toml +3 -3
- {uro_cli-0.6.0 → uro_cli-0.7.0}/uro_cli/main.py +101 -7
- {uro_cli-0.6.0 → uro_cli-0.7.0}/.gitignore +0 -0
- {uro_cli-0.6.0 → uro_cli-0.7.0}/LICENSE +0 -0
- {uro_cli-0.6.0 → uro_cli-0.7.0}/README.md +0 -0
- {uro_cli-0.6.0 → uro_cli-0.7.0}/tests/test_cli_scaffold.py +0 -0
- {uro_cli-0.6.0 → uro_cli-0.7.0}/tests/test_registry_router.py +0 -0
- {uro_cli-0.6.0 → uro_cli-0.7.0}/tests/test_wiring.py +0 -0
- {uro_cli-0.6.0 → uro_cli-0.7.0}/uro_cli/__init__.py +0 -0
- {uro_cli-0.6.0 → uro_cli-0.7.0}/uro_cli/py.typed +0 -0
- {uro_cli-0.6.0 → uro_cli-0.7.0}/uro_cli/wiring.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: uro-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Uro Engine reference client — play, dry-run, and dev tools against the engine
|
|
5
5
|
Project-URL: Homepage, https://github.com/cupskeee/uro
|
|
6
6
|
Project-URL: Repository, https://github.com/cupskeee/uro
|
|
@@ -12,8 +12,8 @@ Requires-Python: >=3.12
|
|
|
12
12
|
Requires-Dist: python-dotenv>=1.0
|
|
13
13
|
Requires-Dist: rich>=13.9
|
|
14
14
|
Requires-Dist: typer>=0.27.2
|
|
15
|
-
Requires-Dist: uro-core[llm,postgres]==0.
|
|
16
|
-
Requires-Dist: uro-server==0.
|
|
15
|
+
Requires-Dist: uro-core[llm,postgres]==0.7.0
|
|
16
|
+
Requires-Dist: uro-server==0.7.0
|
|
17
17
|
Requires-Dist: websockets>=16.1
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "uro-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "Uro Engine reference client — play, dry-run, and dev tools against the engine"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
license-files = ["LICENSE"]
|
|
@@ -13,8 +13,8 @@ requires-python = ">=3.12"
|
|
|
13
13
|
# pair a new uro-cli with an old uro-core; `just release` bumps all three together, and the
|
|
14
14
|
# `{workspace = true}` sources make a drifted pin fail `uv sync` locally.
|
|
15
15
|
dependencies = [
|
|
16
|
-
"uro-core[postgres,llm]==0.
|
|
17
|
-
"uro-server==0.
|
|
16
|
+
"uro-core[postgres,llm]==0.7.0",
|
|
17
|
+
"uro-server==0.7.0",
|
|
18
18
|
"typer>=0.27.2",
|
|
19
19
|
"rich>=13.9",
|
|
20
20
|
"websockets>=16.1",
|
|
@@ -11,7 +11,7 @@ import json
|
|
|
11
11
|
import sys
|
|
12
12
|
import time
|
|
13
13
|
import webbrowser
|
|
14
|
-
from collections.abc import Awaitable
|
|
14
|
+
from collections.abc import AsyncIterator, Awaitable
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import Any
|
|
17
17
|
|
|
@@ -88,6 +88,27 @@ _ROLES = tuple(sorted(ROLES))
|
|
|
88
88
|
PARTICIPANT = "player-1" # Phase 0 is single-player; participants arrive in Phase 5.
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
@contextlib.asynccontextmanager
|
|
92
|
+
async def _metered(router: ProviderRouter) -> AsyncIterator[ProviderRouter]:
|
|
93
|
+
"""Meter a command's model calls when the database is reachable (D-66). Best effort: `world
|
|
94
|
+
backfill` and `world probe` work on a pack file alone, so a down database never stops them —
|
|
95
|
+
their calls just go unrecorded."""
|
|
96
|
+
store: PostgresEventStore | None = None
|
|
97
|
+
try:
|
|
98
|
+
connecting = build_store()
|
|
99
|
+
await asyncio.wait_for(connect_store(connecting), timeout=5)
|
|
100
|
+
store = connecting
|
|
101
|
+
except Exception:
|
|
102
|
+
pass # no database: run unmetered
|
|
103
|
+
if store is not None:
|
|
104
|
+
router.recorder = store.record_llm_call
|
|
105
|
+
try:
|
|
106
|
+
yield router
|
|
107
|
+
finally:
|
|
108
|
+
if store is not None:
|
|
109
|
+
await store.close()
|
|
110
|
+
|
|
111
|
+
|
|
91
112
|
def _run_async(coro_factory) -> None: # type: ignore[no-untyped-def]
|
|
92
113
|
"""Run an async command, turning config/credential/lookup errors into a clean message."""
|
|
93
114
|
try:
|
|
@@ -278,6 +299,8 @@ def world_create(
|
|
|
278
299
|
needs_fill = backfill and check_sufficiency(pack).grade != "runnable"
|
|
279
300
|
router = build_router(provider, model) if needs_fill else None
|
|
280
301
|
await connect_store(store)
|
|
302
|
+
if router is not None: # the backfill's calls are metered like any other (D-66)
|
|
303
|
+
router.recorder = store.record_llm_call
|
|
281
304
|
result = await import_pack(store, pack, backfill_router=router)
|
|
282
305
|
except PackError as exc:
|
|
283
306
|
typer.echo(f"error: {exc}", err=True)
|
|
@@ -353,7 +376,8 @@ def world_backfill(
|
|
|
353
376
|
if before.grade == "runnable":
|
|
354
377
|
typer.echo("pack is already runnable — nothing to backfill")
|
|
355
378
|
return
|
|
356
|
-
|
|
379
|
+
async with _metered(build_router(provider, model)) as router:
|
|
380
|
+
augmented, added = await backfill_gaps(pack, router, report=before)
|
|
357
381
|
after = check_sufficiency(augmented)
|
|
358
382
|
typer.echo(f"backfill: {before.grade} → {after.grade}")
|
|
359
383
|
for a in added:
|
|
@@ -384,7 +408,8 @@ def world_probe(
|
|
|
384
408
|
except PackError as exc:
|
|
385
409
|
typer.echo(f"error: {exc}", err=True)
|
|
386
410
|
raise typer.Exit(1) from exc
|
|
387
|
-
|
|
411
|
+
async with _metered(build_router(provider, model)) as router:
|
|
412
|
+
report = await run_probes(pack.manifest, router, tries=tries)
|
|
388
413
|
typer.echo(f"probe report for {report.world}: {'OK' if report.ok else 'ISSUES'}")
|
|
389
414
|
for r in report.results:
|
|
390
415
|
gate = f" (gates {r.gate_for})" if r.gate_for else ""
|
|
@@ -552,6 +577,13 @@ def serve(
|
|
|
552
577
|
"(consensus). UNSET = auto (solo for 1 token, party for >1). Setting it EXPLICITLY binds "
|
|
553
578
|
"that shape even for a 1-token launch, so a runtime-added player still gets turns (D-39).",
|
|
554
579
|
),
|
|
580
|
+
nats_url: str = typer.Option(
|
|
581
|
+
None,
|
|
582
|
+
"--nats",
|
|
583
|
+
help="push every commit — from any process — to WebSocket clients live, via the NATS "
|
|
584
|
+
"subjects `uro outbox relay` publishes to (D-67)",
|
|
585
|
+
),
|
|
586
|
+
subject_prefix: str = typer.Option("uro", "--subject-prefix", help="the relay's prefix"),
|
|
555
587
|
cors_origin: list[str] = typer.Option( # noqa: B008
|
|
556
588
|
None,
|
|
557
589
|
"--cors-origin",
|
|
@@ -616,7 +648,14 @@ def serve(
|
|
|
616
648
|
else:
|
|
617
649
|
arbiter = shapes[arbiter_kind]()
|
|
618
650
|
deps = engine_deps(store, engine, tokens, admin_tokens, router=router)
|
|
619
|
-
|
|
651
|
+
commit_feed = None
|
|
652
|
+
if nats_url:
|
|
653
|
+
from uro_core.adapters.nats import NatsCommitFeed
|
|
654
|
+
|
|
655
|
+
commit_feed = NatsCommitFeed(nats_url, subject_prefix=subject_prefix)
|
|
656
|
+
fastapi_app = create_app(
|
|
657
|
+
deps, arbiter=arbiter, cors_origins=cors_origin or None, commit_feed=commit_feed
|
|
658
|
+
)
|
|
620
659
|
|
|
621
660
|
@fastapi_app.on_event("startup")
|
|
622
661
|
async def _startup() -> None:
|
|
@@ -641,6 +680,8 @@ def serve(
|
|
|
641
680
|
typer.echo(f" token {t!r} → {p}")
|
|
642
681
|
if cors_origin:
|
|
643
682
|
typer.echo(f" CORS: allowing {', '.join(cors_origin)}")
|
|
683
|
+
if nats_url:
|
|
684
|
+
typer.echo(f" live commits: {nats_url} ({subject_prefix}.>) — run `uro outbox relay` too")
|
|
644
685
|
uvicorn.run(fastapi_app, host=host, port=port, log_level="warning")
|
|
645
686
|
|
|
646
687
|
|
|
@@ -699,6 +740,11 @@ def connect(
|
|
|
699
740
|
)
|
|
700
741
|
elif kind == "beat_failed":
|
|
701
742
|
typer.echo(f"\n ✗ beat failed: {msg['error']}")
|
|
743
|
+
elif kind == "commit": # made elsewhere: another server, the CLI (D-67)
|
|
744
|
+
for beat in msg.get("beats") or []:
|
|
745
|
+
typer.echo(f"\n[{beat['participant_id']}] {beat['narration']}")
|
|
746
|
+
if not msg.get("beats"):
|
|
747
|
+
typer.echo("\n · the world changed (no new narration)")
|
|
702
748
|
elif kind in ("participant_joined", "participant_left"):
|
|
703
749
|
typer.echo(f" · {msg['participant_id']} {kind.split('_')[1]}")
|
|
704
750
|
|
|
@@ -1174,15 +1220,29 @@ def chronicle(
|
|
|
1174
1220
|
_run_async(_run)
|
|
1175
1221
|
|
|
1176
1222
|
|
|
1223
|
+
async def _call_scope(
|
|
1224
|
+
store: PostgresEventStore, world: str | None, campaign: str | None
|
|
1225
|
+
) -> tuple[str | None, list[str] | None]:
|
|
1226
|
+
"""--world / --campaign → (world_id, branch_ids); a campaign's calls are its branch's (D-66)."""
|
|
1227
|
+
world_id = (await _world_or_exit(store, world)).world_id if world else None
|
|
1228
|
+
branch_ids = [(await _campaign_or_exit(store, campaign)).branch_id] if campaign else None
|
|
1229
|
+
return world_id, branch_ids
|
|
1230
|
+
|
|
1231
|
+
|
|
1177
1232
|
@app.command()
|
|
1178
|
-
def usage(
|
|
1179
|
-
|
|
1233
|
+
def usage(
|
|
1234
|
+
stage: str = typer.Option(None, "--stage", help="only this pipeline stage"),
|
|
1235
|
+
world: str = typer.Option(None, "--world", help="only calls that served this world"),
|
|
1236
|
+
campaign: str = typer.Option(None, "--campaign", help="only calls on this campaign's branch"),
|
|
1237
|
+
) -> None:
|
|
1238
|
+
"""LLM-call telemetry by stage: calls, models, token counts, average latency (D-66)."""
|
|
1180
1239
|
|
|
1181
1240
|
async def _run() -> None:
|
|
1182
1241
|
store = build_store()
|
|
1183
1242
|
await connect_store(store)
|
|
1184
1243
|
try:
|
|
1185
|
-
|
|
1244
|
+
world_id, branch_ids = await _call_scope(store, world, campaign)
|
|
1245
|
+
rows = await store.usage_by_stage(stage, world_id=world_id, branch_ids=branch_ids)
|
|
1186
1246
|
finally:
|
|
1187
1247
|
await store.close()
|
|
1188
1248
|
typer.echo(f"total calls: {sum(int(r['calls']) for r in rows)}")
|
|
@@ -1192,6 +1252,40 @@ def usage(stage: str = typer.Option(None, "--stage", help="only this pipeline st
|
|
|
1192
1252
|
_run_async(_run)
|
|
1193
1253
|
|
|
1194
1254
|
|
|
1255
|
+
@app.command()
|
|
1256
|
+
def calls(
|
|
1257
|
+
stage: str = typer.Option(None, "--stage", help="e.g. extractor, planner"),
|
|
1258
|
+
world: str = typer.Option(None, "--world", help="only calls that served this world"),
|
|
1259
|
+
campaign: str = typer.Option(None, "--campaign", help="only calls on this campaign's branch"),
|
|
1260
|
+
limit: int = typer.Option(5, "--limit", min=1, max=200, help="how many, newest first"),
|
|
1261
|
+
) -> None:
|
|
1262
|
+
"""Recent LLM calls with what each structured one returned (D-66) — e.g. the extractor's raw
|
|
1263
|
+
JSON for a beat that committed less than its narration said."""
|
|
1264
|
+
|
|
1265
|
+
async def _run() -> None:
|
|
1266
|
+
store = build_store()
|
|
1267
|
+
await connect_store(store)
|
|
1268
|
+
try:
|
|
1269
|
+
world_id, branch_ids = await _call_scope(store, world, campaign)
|
|
1270
|
+
rows = await store.recent_llm_calls(
|
|
1271
|
+
stage=stage, world_id=world_id, branch_ids=branch_ids, limit=limit
|
|
1272
|
+
)
|
|
1273
|
+
finally:
|
|
1274
|
+
await store.close()
|
|
1275
|
+
if not rows:
|
|
1276
|
+
typer.echo("(no metered calls)")
|
|
1277
|
+
for r in rows:
|
|
1278
|
+
tokens = f"{r['tokens_in'] or '?'}→{r['tokens_out'] or '?'} tokens"
|
|
1279
|
+
typer.echo(
|
|
1280
|
+
f"{r['created_at']:%Y-%m-%d %H:%M:%S} {r['stage_tag']:<10} {r['model'] or '-'} "
|
|
1281
|
+
f"{tokens} {r['latency_ms']}ms"
|
|
1282
|
+
)
|
|
1283
|
+
if r["response"] is not None:
|
|
1284
|
+
typer.echo(f" {r['response']}")
|
|
1285
|
+
|
|
1286
|
+
_run_async(_run)
|
|
1287
|
+
|
|
1288
|
+
|
|
1195
1289
|
@ruleset_app.command("list")
|
|
1196
1290
|
def ruleset_list() -> None:
|
|
1197
1291
|
"""The rulesets a world can declare (`[ruleset] id` in world.toml): the built-ins and installed
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|