cogames 0.3.65__py3-none-any.whl → 0.3.69__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 (134) hide show
  1. cogames/cli/client.py +0 -3
  2. cogames/cli/docsync/docsync.py +7 -1
  3. cogames/cli/mission.py +44 -19
  4. cogames/cli/policy.py +26 -10
  5. cogames/cli/submit.py +201 -495
  6. cogames/cli/utils.py +5 -0
  7. cogames/cogs_vs_clips/clip_difficulty.py +57 -0
  8. cogames/cogs_vs_clips/clips.py +23 -6
  9. cogames/cogs_vs_clips/cog.py +16 -5
  10. cogames/cogs_vs_clips/cogsguard_curriculum.py +122 -0
  11. cogames/cogs_vs_clips/cogsguard_tutorial.py +5 -5
  12. cogames/cogs_vs_clips/config.py +1 -1
  13. cogames/cogs_vs_clips/docs/cogs_vs_clips_mapgen.md +2 -3
  14. cogames/cogs_vs_clips/evals/README.md +8 -32
  15. cogames/cogs_vs_clips/evals/diagnostic_evals.py +0 -1
  16. cogames/cogs_vs_clips/evals/difficulty_variants.py +7 -10
  17. cogames/cogs_vs_clips/mission.py +38 -10
  18. cogames/cogs_vs_clips/missions.py +1 -1
  19. cogames/cogs_vs_clips/reward_variants.py +173 -0
  20. cogames/cogs_vs_clips/sites.py +6 -5
  21. cogames/cogs_vs_clips/stations.py +13 -9
  22. cogames/cogs_vs_clips/team.py +3 -1
  23. cogames/cogs_vs_clips/terrain.py +2 -2
  24. cogames/cogs_vs_clips/variants.py +175 -4
  25. cogames/cogs_vs_clips/weather.py +52 -0
  26. cogames/docs/SCRIPTED_AGENT.md +3 -3
  27. cogames/evaluate.py +4 -2
  28. cogames/main.py +420 -84
  29. cogames/maps/canidate1_1000.map +1 -1
  30. cogames/maps/canidate1_1000_stations.map +2 -2
  31. cogames/maps/canidate1_500.map +1 -1
  32. cogames/maps/canidate1_500_stations.map +2 -2
  33. cogames/maps/canidate2_1000.map +1 -1
  34. cogames/maps/canidate2_1000_stations.map +2 -2
  35. cogames/maps/canidate2_500.map +1 -1
  36. cogames/maps/canidate2_500_stations.map +1 -1
  37. cogames/maps/canidate3_1000.map +1 -1
  38. cogames/maps/canidate3_1000_stations.map +2 -2
  39. cogames/maps/canidate3_500.map +1 -1
  40. cogames/maps/canidate3_500_stations.map +2 -2
  41. cogames/maps/canidate4_500.map +1 -1
  42. cogames/maps/canidate4_500_stations.map +2 -2
  43. cogames/maps/cave_base_50.map +2 -2
  44. cogames/maps/diagnostic_evals/diagnostic_agile.map +2 -2
  45. cogames/maps/diagnostic_evals/diagnostic_agile_hard.map +2 -2
  46. cogames/maps/diagnostic_evals/diagnostic_charge_up.map +6 -6
  47. cogames/maps/diagnostic_evals/diagnostic_charge_up_hard.map +6 -6
  48. cogames/maps/diagnostic_evals/diagnostic_chest_navigation1.map +6 -6
  49. cogames/maps/diagnostic_evals/diagnostic_chest_navigation1_hard.map +6 -6
  50. cogames/maps/diagnostic_evals/diagnostic_chest_navigation2.map +6 -6
  51. cogames/maps/diagnostic_evals/diagnostic_chest_navigation2_hard.map +6 -6
  52. cogames/maps/diagnostic_evals/diagnostic_chest_navigation3.map +6 -6
  53. cogames/maps/diagnostic_evals/diagnostic_chest_navigation3_hard.map +6 -6
  54. cogames/maps/diagnostic_evals/diagnostic_chest_near.map +6 -6
  55. cogames/maps/diagnostic_evals/diagnostic_chest_search.map +6 -6
  56. cogames/maps/diagnostic_evals/diagnostic_chest_search_hard.map +6 -6
  57. cogames/maps/diagnostic_evals/diagnostic_extract_lab.map +6 -6
  58. cogames/maps/diagnostic_evals/diagnostic_extract_lab_hard.map +6 -6
  59. cogames/maps/diagnostic_evals/diagnostic_memory.map +6 -6
  60. cogames/maps/diagnostic_evals/diagnostic_memory_hard.map +6 -6
  61. cogames/maps/diagnostic_evals/diagnostic_radial.map +2 -2
  62. cogames/maps/diagnostic_evals/diagnostic_radial_hard.map +2 -2
  63. cogames/maps/diagnostic_evals/diagnostic_resource_lab.map +6 -6
  64. cogames/maps/diagnostic_evals/diagnostic_unclip.map +6 -6
  65. cogames/maps/evals/eval_balanced_spread.map +6 -6
  66. cogames/maps/evals/eval_clip_oxygen.map +6 -6
  67. cogames/maps/evals/eval_collect_resources.map +6 -6
  68. cogames/maps/evals/eval_collect_resources_hard.map +6 -6
  69. cogames/maps/evals/eval_collect_resources_medium.map +6 -6
  70. cogames/maps/evals/eval_divide_and_conquer.map +6 -6
  71. cogames/maps/evals/eval_energy_starved.map +6 -6
  72. cogames/maps/evals/eval_multi_coordinated_collect_hard.map +6 -6
  73. cogames/maps/evals/eval_oxygen_bottleneck.map +6 -6
  74. cogames/maps/evals/eval_single_use_world.map +6 -6
  75. cogames/maps/evals/extractor_hub_100x100.map +6 -6
  76. cogames/maps/evals/extractor_hub_30x30.map +6 -6
  77. cogames/maps/evals/extractor_hub_50x50.map +6 -6
  78. cogames/maps/evals/extractor_hub_70x70.map +6 -6
  79. cogames/maps/evals/extractor_hub_80x80.map +6 -6
  80. cogames/maps/machina_100_stations.map +2 -2
  81. cogames/maps/machina_200_stations.map +2 -2
  82. cogames/maps/machina_200_stations_small.map +2 -2
  83. cogames/maps/machina_eval_exp01.map +2 -2
  84. cogames/maps/machina_eval_template_large.map +2 -2
  85. cogames/maps/machinatrainer4agents.map +2 -2
  86. cogames/maps/machinatrainer4agentsbase.map +2 -2
  87. cogames/maps/machinatrainerbig.map +2 -2
  88. cogames/maps/machinatrainersmall.map +2 -2
  89. cogames/maps/planky_evals/aligner_avoid_aoe.map +6 -6
  90. cogames/maps/planky_evals/aligner_full_cycle.map +6 -6
  91. cogames/maps/planky_evals/aligner_gear.map +6 -6
  92. cogames/maps/planky_evals/aligner_hearts.map +6 -6
  93. cogames/maps/planky_evals/aligner_junction.map +6 -6
  94. cogames/maps/planky_evals/exploration_distant.map +6 -6
  95. cogames/maps/planky_evals/maze.map +6 -6
  96. cogames/maps/planky_evals/miner_best_resource.map +6 -6
  97. cogames/maps/planky_evals/miner_deposit.map +6 -6
  98. cogames/maps/planky_evals/miner_extract.map +6 -6
  99. cogames/maps/planky_evals/miner_full_cycle.map +6 -6
  100. cogames/maps/planky_evals/miner_gear.map +6 -6
  101. cogames/maps/planky_evals/multi_role.map +6 -6
  102. cogames/maps/planky_evals/resource_chain.map +6 -6
  103. cogames/maps/planky_evals/scout_explore.map +6 -6
  104. cogames/maps/planky_evals/scout_gear.map +6 -6
  105. cogames/maps/planky_evals/scrambler_full_cycle.map +6 -6
  106. cogames/maps/planky_evals/scrambler_gear.map +6 -6
  107. cogames/maps/planky_evals/scrambler_target.map +6 -6
  108. cogames/maps/planky_evals/stuck_corridor.map +6 -6
  109. cogames/maps/planky_evals/survive_retreat.map +6 -6
  110. cogames/maps/training_facility_clipped.map +2 -2
  111. cogames/maps/training_facility_open_1.map +2 -2
  112. cogames/maps/training_facility_open_2.map +2 -2
  113. cogames/maps/training_facility_open_3.map +2 -2
  114. cogames/maps/training_facility_tight_4.map +2 -2
  115. cogames/maps/training_facility_tight_5.map +2 -2
  116. cogames/maps/vanilla_large.map +2 -2
  117. cogames/maps/vanilla_small.map +2 -2
  118. cogames/pickup.py +6 -5
  119. cogames/play.py +14 -16
  120. cogames/policy/nim_agents/__init__.py +0 -2
  121. cogames/policy/nim_agents/agents.py +0 -11
  122. cogames/policy/starter_agent.py +4 -1
  123. {cogames-0.3.65.dist-info → cogames-0.3.69.dist-info}/METADATA +45 -29
  124. cogames-0.3.69.dist-info/RECORD +160 -0
  125. metta_alo/scoring.py +7 -7
  126. cogames-0.3.65.dist-info/RECORD +0 -160
  127. metta_alo/job_specs.py +0 -17
  128. metta_alo/policy.py +0 -16
  129. metta_alo/pure_single_episode_runner.py +0 -75
  130. metta_alo/rollout.py +0 -322
  131. {cogames-0.3.65.dist-info → cogames-0.3.69.dist-info}/WHEEL +0 -0
  132. {cogames-0.3.65.dist-info → cogames-0.3.69.dist-info}/entry_points.txt +0 -0
  133. {cogames-0.3.65.dist-info → cogames-0.3.69.dist-info}/licenses/LICENSE +0 -0
  134. {cogames-0.3.65.dist-info → cogames-0.3.69.dist-info}/top_level.txt +0 -0
cogames/main.py CHANGED
@@ -8,6 +8,7 @@ from cogames.cli.utils import suppress_noisy_logs
8
8
 
9
9
  suppress_noisy_logs()
10
10
 
11
+ import importlib
11
12
  import importlib.metadata
12
13
  import importlib.util
13
14
  import json
@@ -17,6 +18,7 @@ import subprocess
17
18
  import sys
18
19
  import threading
19
20
  import time
21
+ from dataclasses import dataclass
20
22
  from pathlib import Path
21
23
  from typing import Literal, Optional, TypeVar
22
24
 
@@ -38,7 +40,6 @@ from cogames import play as play_module
38
40
  from cogames import train as train_module
39
41
  from cogames.cli.base import console
40
42
  from cogames.cli.client import SeasonInfo, TournamentServerClient, fetch_default_season, fetch_season_info
41
- from cogames.cli.docsync import docsync
42
43
  from cogames.cli.leaderboard import (
43
44
  leaderboard_cmd,
44
45
  parse_policy_identifier,
@@ -62,7 +63,14 @@ from cogames.cli.policy import (
62
63
  policy_arg_example,
63
64
  policy_arg_w_proportion_example,
64
65
  )
65
- from cogames.cli.submit import DEFAULT_SUBMIT_SERVER, results_url_for_season, upload_policy, validate_policy_spec
66
+ from cogames.cli.submit import (
67
+ DEFAULT_SUBMIT_SERVER,
68
+ RESULTS_URL,
69
+ create_bundle,
70
+ upload_policy,
71
+ validate_bundle,
72
+ )
73
+ from cogames.cogs_vs_clips.mission import CvCMission, NumCogsVariant
66
74
  from cogames.curricula import make_rotation
67
75
  from cogames.device import resolve_training_device
68
76
  from mettagrid.config.mettagrid_config import MettaGridConfig
@@ -87,6 +95,158 @@ logger = logging.getLogger("cogames.main")
87
95
  T = TypeVar("T")
88
96
 
89
97
 
98
+ @dataclass(frozen=True)
99
+ class DiagnoseCase:
100
+ name: str
101
+ env_cfg: MettaGridConfig
102
+
103
+
104
+ def _load_eval_missions(module_path: str) -> list[CvCMission]:
105
+ module = importlib.import_module(module_path)
106
+ missions = getattr(module, "EVAL_MISSIONS", None)
107
+ if missions is None:
108
+ raise AttributeError(f"Module '{module_path}' does not define EVAL_MISSIONS")
109
+ return list(missions)
110
+
111
+
112
+ def _load_diagnose_missions(mission_set: str) -> list[CvCMission]:
113
+ if mission_set == "thinky_evals":
114
+ return []
115
+
116
+ if mission_set == "all":
117
+ from cogames.cogs_vs_clips.evals.cogsguard_evals import COGSGUARD_EVAL_MISSIONS # noqa: PLC0415
118
+ from cogames.cogs_vs_clips.evals.diagnostic_evals import DIAGNOSTIC_EVALS # noqa: PLC0415
119
+ from cogames.cogs_vs_clips.missions import MISSIONS as ALL_MISSIONS # noqa: PLC0415
120
+
121
+ missions_list: list[CvCMission] = []
122
+ missions_list.extend(COGSGUARD_EVAL_MISSIONS)
123
+ missions_list.extend(_load_eval_missions("cogames.cogs_vs_clips.evals.integrated_evals"))
124
+ missions_list.extend(_load_eval_missions("cogames.cogs_vs_clips.evals.spanning_evals"))
125
+ missions_list.extend([mission_cls() for mission_cls in DIAGNOSTIC_EVALS]) # type: ignore[call-arg]
126
+ eval_mission_names = {mission.name for mission in missions_list}
127
+ for mission in ALL_MISSIONS:
128
+ if mission.name not in eval_mission_names:
129
+ missions_list.append(mission)
130
+ return missions_list
131
+
132
+ if mission_set == "cogsguard_evals":
133
+ from cogames.cogs_vs_clips.evals.cogsguard_evals import COGSGUARD_EVAL_MISSIONS # noqa: PLC0415
134
+
135
+ return list(COGSGUARD_EVAL_MISSIONS)
136
+
137
+ if mission_set == "diagnostic_evals":
138
+ from cogames.cogs_vs_clips.evals.diagnostic_evals import DIAGNOSTIC_EVALS # noqa: PLC0415
139
+
140
+ return [mission_cls() for mission_cls in DIAGNOSTIC_EVALS] # type: ignore[call-arg]
141
+
142
+ if mission_set == "tournament":
143
+ from cogames.cogs_vs_clips.evals.diagnostic_evals import DIAGNOSTIC_EVALS # noqa: PLC0415
144
+
145
+ missions_list = []
146
+ missions_list.extend(_load_eval_missions("cogames.cogs_vs_clips.evals.integrated_evals"))
147
+ missions_list.extend([mission_cls() for mission_cls in DIAGNOSTIC_EVALS]) # type: ignore[call-arg]
148
+ return missions_list
149
+
150
+ if mission_set == "integrated_evals":
151
+ return _load_eval_missions("cogames.cogs_vs_clips.evals.integrated_evals")
152
+
153
+ if mission_set == "spanning_evals":
154
+ return _load_eval_missions("cogames.cogs_vs_clips.evals.spanning_evals")
155
+
156
+ raise ValueError(f"Unknown mission set: {mission_set}")
157
+
158
+
159
+ def _build_thinky_mission_map() -> dict[str, CvCMission]:
160
+ from cogames.cogs_vs_clips.evals.cogsguard_evals import COGSGUARD_EVAL_MISSIONS # noqa: PLC0415
161
+ from cogames.cogs_vs_clips.evals.diagnostic_evals import DIAGNOSTIC_EVALS # noqa: PLC0415
162
+ from cogames.cogs_vs_clips.missions import MISSIONS as ALL_MISSIONS # noqa: PLC0415
163
+
164
+ missions: list[CvCMission] = []
165
+ missions.extend(_load_eval_missions("cogames.cogs_vs_clips.evals.integrated_evals"))
166
+ missions.extend(_load_eval_missions("cogames.cogs_vs_clips.evals.spanning_evals"))
167
+ missions.extend([mission_cls() for mission_cls in DIAGNOSTIC_EVALS]) # type: ignore[call-arg]
168
+ missions.extend(COGSGUARD_EVAL_MISSIONS)
169
+ missions.extend(ALL_MISSIONS)
170
+
171
+ mission_map: dict[str, CvCMission] = {}
172
+ for mission in missions:
173
+ mission_map.setdefault(mission.name, mission)
174
+ return mission_map
175
+
176
+
177
+ def _matches_experiment(mission_name: str, experiment_filters: set[str]) -> bool:
178
+ if not experiment_filters:
179
+ return True
180
+ if mission_name in experiment_filters:
181
+ return True
182
+ suffix = f".{mission_name}"
183
+ return any(name.endswith(suffix) for name in experiment_filters)
184
+
185
+
186
+ def _cogs_for_mission(mission: CvCMission, cogs_list: list[int], respect_cogs_list: bool) -> list[int]:
187
+ fixed_cogs = getattr(mission, "num_cogs", None)
188
+ if fixed_cogs is not None:
189
+ if respect_cogs_list and fixed_cogs not in cogs_list:
190
+ return []
191
+ return [fixed_cogs]
192
+ site = getattr(mission, "site", None)
193
+ if site is None:
194
+ return list(cogs_list)
195
+ min_cogs = getattr(site, "min_cogs", None)
196
+ max_cogs = getattr(site, "max_cogs", None)
197
+ return [
198
+ num_cogs
199
+ for num_cogs in cogs_list
200
+ if (min_cogs is None or num_cogs >= min_cogs) and (max_cogs is None or num_cogs <= max_cogs)
201
+ ]
202
+
203
+
204
+ def _build_diagnose_case(mission: CvCMission, num_cogs: int, steps: int) -> DiagnoseCase:
205
+ mission_with_cogs = mission.with_variants([NumCogsVariant(num_cogs=num_cogs)])
206
+ env_cfg = mission_with_cogs.make_env()
207
+ env_cfg.game.max_steps = steps
208
+ name = f"{mission.full_name()} (cogs={num_cogs})"
209
+ return DiagnoseCase(name=name, env_cfg=env_cfg)
210
+
211
+
212
+ def _build_diagnose_cases(
213
+ *,
214
+ mission_set: str,
215
+ experiments: Optional[list[str]],
216
+ cogs: Optional[list[int]],
217
+ steps: int,
218
+ ) -> list[DiagnoseCase]:
219
+ experiment_filters = set(experiments or [])
220
+ cogs_list = cogs if cogs else [1, 2, 4]
221
+ respect_cogs_list = cogs is not None
222
+ cases: list[DiagnoseCase] = []
223
+
224
+ if mission_set == "thinky_evals":
225
+ from cogames_agents.policy.nim_agents.thinky_eval import EVALS as THINKY_EVALS # noqa: PLC0415
226
+
227
+ mission_map = _build_thinky_mission_map()
228
+ for exp_name, _tag, num_cogs in THINKY_EVALS:
229
+ if not _matches_experiment(exp_name, experiment_filters):
230
+ continue
231
+ if respect_cogs_list and num_cogs not in cogs_list:
232
+ continue
233
+ base_mission = mission_map.get(exp_name)
234
+ if base_mission is None:
235
+ logger.warning("Thinky eval mission '%s' not found; skipping.", exp_name)
236
+ continue
237
+ cases.append(_build_diagnose_case(base_mission, num_cogs, steps))
238
+ return cases
239
+
240
+ missions = _load_diagnose_missions(mission_set)
241
+ for mission in missions:
242
+ if not _matches_experiment(mission.name, experiment_filters):
243
+ continue
244
+ for num_cogs in _cogs_for_mission(mission, cogs_list, respect_cogs_list):
245
+ cases.append(_build_diagnose_case(mission, num_cogs, steps))
246
+
247
+ return cases
248
+
249
+
90
250
  def _resolve_mettascope_script() -> Path:
91
251
  spec = importlib.util.find_spec("mettagrid")
92
252
  if spec is None or spec.origin is None:
@@ -132,7 +292,18 @@ tutorial_app = typer.Typer(
132
292
  if register_tribal_cli is not None:
133
293
  register_tribal_cli(app)
134
294
 
135
- app.add_typer(docsync.app, name="docsync", hidden=True)
295
+
296
+ @app.command(
297
+ name="docsync",
298
+ hidden=True,
299
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
300
+ add_help_option=False,
301
+ )
302
+ def docsync_cmd(ctx: typer.Context) -> None:
303
+ """Sync cogames docs between .ipynb, .py, and .md formats (dev-only)."""
304
+ from cogames.cli.docsync import docsync # noqa: PLC0415
305
+
306
+ docsync.app(prog_name="cogames docsync", standalone_mode=False, args=list(ctx.args))
136
307
 
137
308
 
138
309
  @tutorial_app.command(
@@ -160,7 +331,7 @@ def tutorial_cmd(
160
331
  console.print("[dim]Initializing Mettascope...[/dim]")
161
332
 
162
333
  # Load tutorial mission (CogsGuard)
163
- from cogames.cogs_vs_clips.missions import make_cogsguard_mission
334
+ from cogames.cogs_vs_clips.missions import make_cogsguard_mission # noqa: PLC0415
164
335
 
165
336
  # Create environment config
166
337
  env_cfg = make_cogsguard_mission(num_agents=1, max_steps=1000).make_env()
@@ -310,7 +481,7 @@ def cogsguard_tutorial_cmd(
310
481
  console.print("[dim]Initializing Mettascope...[/dim]")
311
482
 
312
483
  # Load CogsGuard tutorial mission
313
- from cogames.cogs_vs_clips.cogsguard_tutorial import CogsGuardTutorialMission
484
+ from cogames.cogs_vs_clips.cogsguard_tutorial import CogsGuardTutorialMission # noqa: PLC0415
314
485
 
315
486
  # Create environment config
316
487
  env_cfg = CogsGuardTutorialMission.make_env()
@@ -526,6 +697,13 @@ def games_cmd(
526
697
  help="Apply variant (requires -m, repeatable)",
527
698
  rich_help_panel="Describe",
528
699
  ),
700
+ difficulty: Optional[str] = typer.Option(
701
+ None,
702
+ "--difficulty",
703
+ metavar="LEVEL",
704
+ help="Difficulty (easy, medium, hard) controlling clips events (requires -m)",
705
+ rich_help_panel="Describe",
706
+ ),
529
707
  format_: Optional[Literal["yaml", "json"]] = typer.Option(
530
708
  None,
531
709
  "--format",
@@ -569,7 +747,13 @@ def games_cmd(
569
747
  return
570
748
 
571
749
  try:
572
- resolved_mission, env_cfg, mission_cfg = get_mission_name_and_config(ctx, mission, variant, cogs)
750
+ resolved_mission, env_cfg, mission_cfg = get_mission_name_and_config(
751
+ ctx,
752
+ mission,
753
+ variants_arg=variant,
754
+ cogs=cogs,
755
+ difficulty=difficulty,
756
+ )
573
757
  except typer.Exit as exc:
574
758
  if exc.exit_code != 1:
575
759
  raise
@@ -653,6 +837,13 @@ def describe_cmd(
653
837
  help="Apply variant (repeatable)",
654
838
  rich_help_panel="Configuration",
655
839
  ),
840
+ difficulty: Optional[str] = typer.Option(
841
+ None,
842
+ "--difficulty",
843
+ metavar="LEVEL",
844
+ help="Difficulty (easy, medium, hard) controlling clips events",
845
+ rich_help_panel="Configuration",
846
+ ),
656
847
  _help: bool = typer.Option(
657
848
  False,
658
849
  "--help",
@@ -663,7 +854,13 @@ def describe_cmd(
663
854
  rich_help_panel="Other",
664
855
  ),
665
856
  ) -> None:
666
- resolved_mission, env_cfg, mission_cfg = get_mission_name_and_config(ctx, mission, variant, cogs)
857
+ resolved_mission, env_cfg, mission_cfg = get_mission_name_and_config(
858
+ ctx,
859
+ mission,
860
+ variants_arg=variant,
861
+ cogs=cogs,
862
+ difficulty=difficulty,
863
+ )
667
864
  describe_mission(resolved_mission, env_cfg, mission_cfg)
668
865
 
669
866
 
@@ -711,6 +908,13 @@ def play_cmd(
711
908
  help="Apply variant modifier (repeatable)",
712
909
  rich_help_panel="Game Setup",
713
910
  ),
911
+ difficulty: Optional[str] = typer.Option(
912
+ None,
913
+ "--difficulty",
914
+ metavar="LEVEL",
915
+ help="Difficulty (easy, medium, hard) controlling clips events",
916
+ rich_help_panel="Game Setup",
917
+ ),
714
918
  cogs: Optional[int] = typer.Option(
715
919
  None,
716
920
  "--cogs",
@@ -729,6 +933,13 @@ def play_cmd(
729
933
  help="Policy controlling cogs ([bold]noop[/bold], [bold]random[/bold], [bold]lstm[/bold], or path)",
730
934
  rich_help_panel="Policy",
731
935
  ),
936
+ device: str = typer.Option(
937
+ "auto",
938
+ "--device",
939
+ metavar="DEVICE",
940
+ help="Policy device (auto, cpu, cuda, cuda:0, etc.)",
941
+ rich_help_panel="Policy",
942
+ ),
732
943
  # --- Simulation ---
733
944
  steps: int = typer.Option(
734
945
  1000,
@@ -762,6 +973,12 @@ def play_cmd(
762
973
  show_default="same as --seed",
763
974
  rich_help_panel="Simulation",
764
975
  ),
976
+ autostart: bool = typer.Option(
977
+ False,
978
+ "--autostart",
979
+ help="Start simulation immediately without waiting for user input",
980
+ rich_help_panel="Simulation",
981
+ ),
765
982
  # --- Output ---
766
983
  save_replay_dir: Optional[Path] = typer.Option( # noqa: B008
767
984
  None,
@@ -796,7 +1013,13 @@ def play_cmd(
796
1013
  rich_help_panel="Other",
797
1014
  ),
798
1015
  ) -> None:
799
- resolved_mission, env_cfg, mission_cfg = get_mission_name_and_config(ctx, mission, variant, cogs)
1016
+ resolved_mission, env_cfg, mission_cfg = get_mission_name_and_config(
1017
+ ctx,
1018
+ mission,
1019
+ variants_arg=variant,
1020
+ cogs=cogs,
1021
+ difficulty=difficulty,
1022
+ )
800
1023
 
801
1024
  if print_cvc_config or print_mg_config:
802
1025
  try:
@@ -811,9 +1034,8 @@ def play_cmd(
811
1034
  if isinstance(map_builder, MapGen.Config):
812
1035
  map_builder.seed = map_seed
813
1036
 
814
- policy_spec = get_policy_spec(ctx, policy)
815
- console.print(f"[cyan]Playing {resolved_mission}[/cyan]")
816
- console.print(f"Max Steps: {steps}, Render: {render}")
1037
+ resolved_device = resolve_training_device(console, device)
1038
+ policy_spec = get_policy_spec(ctx, policy, device=str(resolved_device))
817
1039
 
818
1040
  if ctx.get_parameter_source("steps") in (
819
1041
  ParameterSource.COMMANDLINE,
@@ -822,14 +1044,19 @@ def play_cmd(
822
1044
  ):
823
1045
  env_cfg.game.max_steps = steps
824
1046
 
1047
+ console.print(f"[cyan]Playing {resolved_mission}[/cyan]")
1048
+ console.print(f"Max Steps: {env_cfg.game.max_steps}, Render: {render}")
1049
+
825
1050
  play_module.play(
826
1051
  console,
827
1052
  env_cfg=env_cfg,
828
1053
  policy_spec=policy_spec,
829
1054
  seed=seed,
1055
+ device=str(resolved_device),
830
1056
  render_mode=render,
831
1057
  game_name=resolved_mission,
832
1058
  save_replay=save_replay_dir,
1059
+ autostart=autostart,
833
1060
  )
834
1061
 
835
1062
 
@@ -1151,6 +1378,13 @@ def train_cmd(
1151
1378
  help="Mission variant (repeatable)",
1152
1379
  rich_help_panel="Mission Setup",
1153
1380
  ),
1381
+ difficulty: Optional[str] = typer.Option(
1382
+ None,
1383
+ "--difficulty",
1384
+ metavar="LEVEL",
1385
+ help="Difficulty (easy, medium, hard) controlling clips events",
1386
+ rich_help_panel="Mission Setup",
1387
+ ),
1154
1388
  # --- Policy ---
1155
1389
  policy: str = typer.Option(
1156
1390
  "class=lstm",
@@ -1261,7 +1495,13 @@ def train_cmd(
1261
1495
  rich_help_panel="Other",
1262
1496
  ),
1263
1497
  ) -> None:
1264
- selected_missions = get_mission_names_and_configs(ctx, missions, variants_arg=variant, cogs=cogs)
1498
+ selected_missions = get_mission_names_and_configs(
1499
+ ctx,
1500
+ missions,
1501
+ variants_arg=variant,
1502
+ cogs=cogs,
1503
+ difficulty=difficulty,
1504
+ )
1265
1505
  if len(selected_missions) == 1:
1266
1506
  mission_name, env_cfg = selected_missions[0]
1267
1507
  supplier = None
@@ -1380,6 +1620,13 @@ def run_cmd(
1380
1620
  help="Mission variant (repeatable)",
1381
1621
  rich_help_panel="Mission",
1382
1622
  ),
1623
+ difficulty: Optional[str] = typer.Option(
1624
+ None,
1625
+ "--difficulty",
1626
+ metavar="LEVEL",
1627
+ help="Difficulty (easy, medium, hard) controlling clips events",
1628
+ rich_help_panel="Mission",
1629
+ ),
1383
1630
  # --- Policy ---
1384
1631
  policies: Optional[list[str]] = typer.Option( # noqa: B008
1385
1632
  None,
@@ -1389,6 +1636,13 @@ def run_cmd(
1389
1636
  help=f"Policies to evaluate: ({policy_arg_w_proportion_example}...)",
1390
1637
  rich_help_panel="Policy",
1391
1638
  ),
1639
+ device: str = typer.Option(
1640
+ "auto",
1641
+ "--device",
1642
+ metavar="DEVICE",
1643
+ help="Policy device (auto, cpu, cuda, cuda:0, etc.)",
1644
+ rich_help_panel="Policy",
1645
+ ),
1392
1646
  # --- Simulation ---
1393
1647
  episodes: int = typer.Option(
1394
1648
  10,
@@ -1400,12 +1654,13 @@ def run_cmd(
1400
1654
  rich_help_panel="Simulation",
1401
1655
  ),
1402
1656
  steps: Optional[int] = typer.Option(
1403
- 1000,
1657
+ None,
1404
1658
  "--steps",
1405
1659
  "-s",
1406
1660
  metavar="N",
1407
1661
  help="Max steps per episode",
1408
1662
  min=1,
1663
+ show_default="from mission",
1409
1664
  rich_help_panel="Simulation",
1410
1665
  ),
1411
1666
  seed: int = typer.Option(
@@ -1465,7 +1720,7 @@ def run_cmd(
1465
1720
  raise typer.Exit(1)
1466
1721
 
1467
1722
  if mission_set:
1468
- from cogames.cli.mission import load_mission_set
1723
+ from cogames.cli.mission import load_mission_set # noqa: PLC0415
1469
1724
 
1470
1725
  try:
1471
1726
  mission_objs = load_mission_set(mission_set)
@@ -1479,7 +1734,14 @@ def run_cmd(
1479
1734
  if cogs is None:
1480
1735
  cogs = 4
1481
1736
 
1482
- selected_missions = get_mission_names_and_configs(ctx, missions, variants_arg=variant, cogs=cogs, steps=steps)
1737
+ selected_missions = get_mission_names_and_configs(
1738
+ ctx,
1739
+ missions,
1740
+ variants_arg=variant,
1741
+ cogs=cogs,
1742
+ steps=steps,
1743
+ difficulty=difficulty,
1744
+ )
1483
1745
 
1484
1746
  # Optional MapGen seed override for procedural maps.
1485
1747
  if map_seed is not None:
@@ -1488,7 +1750,8 @@ def run_cmd(
1488
1750
  if isinstance(map_builder, MapGen.Config):
1489
1751
  map_builder.seed = map_seed
1490
1752
 
1491
- policy_specs = get_policy_specs_with_proportions(ctx, policies)
1753
+ resolved_device = resolve_training_device(console, device)
1754
+ policy_specs = get_policy_specs_with_proportions(ctx, policies, device=str(resolved_device))
1492
1755
 
1493
1756
  if ctx.info_name == "scrimmage":
1494
1757
  if len(policy_specs) != 1:
@@ -1510,6 +1773,7 @@ def run_cmd(
1510
1773
  action_timeout_ms=action_timeout_ms,
1511
1774
  episodes=episodes,
1512
1775
  seed=seed,
1776
+ device=str(resolved_device),
1513
1777
  output_format=format_,
1514
1778
  save_replay=str(save_replay_dir) if save_replay_dir else None,
1515
1779
  )
@@ -1552,6 +1816,13 @@ def pickup_cmd(
1552
1816
  help="Mission variant (repeatable)",
1553
1817
  rich_help_panel="Mission",
1554
1818
  ),
1819
+ difficulty: Optional[str] = typer.Option(
1820
+ None,
1821
+ "--difficulty",
1822
+ metavar="LEVEL",
1823
+ help="Difficulty (easy, medium, hard) controlling clips events",
1824
+ rich_help_panel="Mission",
1825
+ ),
1555
1826
  # --- Policy ---
1556
1827
  policy: Optional[str] = typer.Option(
1557
1828
  None,
@@ -1568,6 +1839,13 @@ def pickup_cmd(
1568
1839
  help="Pool policy (repeatable)",
1569
1840
  rich_help_panel="Policy",
1570
1841
  ),
1842
+ device: str = typer.Option(
1843
+ "auto",
1844
+ "--device",
1845
+ metavar="DEVICE",
1846
+ help="Policy device (auto, cpu, cuda, cuda:0, etc.)",
1847
+ rich_help_panel="Policy",
1848
+ ),
1571
1849
  # --- Simulation ---
1572
1850
  episodes: int = typer.Option(
1573
1851
  1,
@@ -1631,7 +1909,7 @@ def pickup_cmd(
1631
1909
  rich_help_panel="Other",
1632
1910
  ),
1633
1911
  ) -> None:
1634
- import httpx
1912
+ import httpx # noqa: PLC0415
1635
1913
 
1636
1914
  if policy is None:
1637
1915
  console.print(ctx.get_help())
@@ -1644,15 +1922,22 @@ def pickup_cmd(
1644
1922
  raise typer.Exit(1)
1645
1923
 
1646
1924
  # Resolve mission
1647
- resolved_mission, env_cfg, _ = get_mission_name_and_config(ctx, mission, variants_arg=variant, cogs=cogs)
1925
+ resolved_mission, env_cfg, _ = get_mission_name_and_config(
1926
+ ctx,
1927
+ mission,
1928
+ variants_arg=variant,
1929
+ cogs=cogs,
1930
+ difficulty=difficulty,
1931
+ )
1648
1932
  if steps is not None:
1649
1933
  env_cfg.game.max_steps = steps
1650
1934
 
1651
1935
  candidate_label = policy
1652
1936
  pool_labels = pool
1653
- candidate_spec = get_policy_spec(ctx, policy)
1937
+ resolved_device = resolve_training_device(console, device)
1938
+ candidate_spec = get_policy_spec(ctx, policy, device=str(resolved_device))
1654
1939
  try:
1655
- pool_specs = [parse_policy_spec(spec).to_policy_spec() for spec in pool]
1940
+ pool_specs = [parse_policy_spec(spec, device=str(resolved_device)).to_policy_spec() for spec in pool]
1656
1941
  except (ValueError, ModuleNotFoundError, httpx.HTTPError) as exc:
1657
1942
  translated = _translate_error(exc)
1658
1943
  console.print(f"[yellow]Error parsing pool policy: {translated}[/yellow]\n")
@@ -1669,6 +1954,7 @@ def pickup_cmd(
1669
1954
  map_seed=map_seed,
1670
1955
  action_timeout_ms=action_timeout_ms,
1671
1956
  save_replay_dir=save_replay_dir,
1957
+ device=str(resolved_device),
1672
1958
  candidate_label=candidate_label,
1673
1959
  pool_labels=pool_labels,
1674
1960
  )
@@ -1762,10 +2048,10 @@ def login_cmd(
1762
2048
  rich_help_panel="Other",
1763
2049
  ),
1764
2050
  ) -> None:
1765
- from urllib.parse import urlparse
2051
+ from urllib.parse import urlparse # noqa: PLC0415
1766
2052
 
1767
2053
  # Check if we already have a token
1768
- from cogames.auth import BaseCLIAuthenticator
2054
+ from cogames.auth import BaseCLIAuthenticator # noqa: PLC0415
1769
2055
 
1770
2056
  temp_auth = BaseCLIAuthenticator(
1771
2057
  token_file_name="cogames.yaml",
@@ -1823,7 +2109,9 @@ app.command(
1823
2109
  rich_help_panel="Evaluate",
1824
2110
  epilog="""[dim]Examples:[/dim]
1825
2111
 
1826
- [cyan]cogames diagnose ./train_dir/my_run[/cyan] Default diagnostics
2112
+ [cyan]cogames diagnose ./train_dir/my_run[/cyan] Default CogsGuard evals
2113
+
2114
+ [cyan]cogames diagnose lstm -S diagnostic_evals[/cyan] Diagnostic evals (non-CogsGuard)
1827
2115
 
1828
2116
  [cyan]cogames diagnose lstm -S tournament[/cyan] Tournament suite
1829
2117
 
@@ -1831,6 +2119,7 @@ app.command(
1831
2119
  add_help_option=False,
1832
2120
  )
1833
2121
  def diagnose_cmd(
2122
+ ctx: typer.Context,
1834
2123
  policy: str = typer.Argument(
1835
2124
  ...,
1836
2125
  metavar="POLICY",
@@ -1838,6 +2127,7 @@ def diagnose_cmd(
1838
2127
  ),
1839
2128
  # --- Evaluation ---
1840
2129
  mission_set: Literal[
2130
+ "cogsguard_evals",
1841
2131
  "diagnostic_evals",
1842
2132
  "integrated_evals",
1843
2133
  "spanning_evals",
@@ -1845,7 +2135,7 @@ def diagnose_cmd(
1845
2135
  "tournament",
1846
2136
  "all",
1847
2137
  ] = typer.Option(
1848
- "diagnostic_evals",
2138
+ "cogsguard_evals",
1849
2139
  "--mission-set",
1850
2140
  "-S",
1851
2141
  metavar="SET",
@@ -1867,6 +2157,13 @@ def diagnose_cmd(
1867
2157
  help="Agent counts to test (repeatable)",
1868
2158
  rich_help_panel="Evaluation",
1869
2159
  ),
2160
+ device: str = typer.Option(
2161
+ "auto",
2162
+ "--device",
2163
+ metavar="DEVICE",
2164
+ help="Policy device (auto, cpu, cuda, cuda:0, etc.)",
2165
+ rich_help_panel="Evaluation",
2166
+ ),
1870
2167
  # --- Simulation ---
1871
2168
  steps: int = typer.Option(
1872
2169
  1000,
@@ -1895,28 +2192,30 @@ def diagnose_cmd(
1895
2192
  rich_help_panel="Other",
1896
2193
  ),
1897
2194
  ) -> None:
1898
- script_path = Path(__file__).resolve().parents[2] / "scripts" / "run_evaluation.py"
1899
-
1900
- cmd = [sys.executable, str(script_path)]
1901
- cmd.extend(["--mission-set", mission_set])
1902
-
1903
- if experiments:
1904
- cmd.append("--experiments")
1905
- cmd.extend(experiments)
1906
-
1907
- if cogs:
1908
- cmd.append("--cogs")
1909
- cmd.extend(str(c) for c in cogs)
1910
-
1911
- cmd.extend(["--steps", str(steps)])
1912
- cmd.extend(["--repeats", str(episodes)])
1913
- cmd.append("--no-plots")
1914
-
1915
- cmd.extend(["--policy", policy])
2195
+ resolved_device = resolve_training_device(console, device)
2196
+ policy_spec = get_policy_spec(ctx, policy, device=str(resolved_device))
2197
+
2198
+ cases = _build_diagnose_cases(
2199
+ mission_set=mission_set,
2200
+ experiments=experiments,
2201
+ cogs=cogs,
2202
+ steps=steps,
2203
+ )
2204
+ if not cases:
2205
+ console.print("[red]No evaluation cases matched your filters.[/red]")
2206
+ raise typer.Exit(1)
1916
2207
 
1917
- console.print("[cyan]Running diagnostic evaluation...[/cyan]")
1918
- console.print(f"[dim]{' '.join(cmd)}[/dim]")
1919
- subprocess.run(cmd, check=True)
2208
+ console.print(f"[cyan]Running diagnostic evaluation ({len(cases)} cases)...[/cyan]")
2209
+ evaluate_module.evaluate(
2210
+ console,
2211
+ missions=[(case.name, case.env_cfg) for case in cases],
2212
+ policy_specs=[policy_spec],
2213
+ proportions=[1.0],
2214
+ action_timeout_ms=10000,
2215
+ episodes=episodes,
2216
+ seed=42,
2217
+ device=str(resolved_device),
2218
+ )
1920
2219
 
1921
2220
 
1922
2221
  def _resolve_season(server: str, season_name: str | None = None) -> SeasonInfo:
@@ -1935,12 +2234,12 @@ def _resolve_season(server: str, season_name: str | None = None) -> SeasonInfo:
1935
2234
 
1936
2235
 
1937
2236
  @app.command(
1938
- name="validate-policy",
1939
- help="Validate the policy loads and runs for at least a single step",
2237
+ name="create-bundle",
2238
+ help="Create a submission bundle zip from a policy",
1940
2239
  rich_help_panel="Policies",
1941
2240
  add_help_option=False,
1942
2241
  )
1943
- def validate_policy_cmd(
2242
+ def create_bundle_cmd(
1944
2243
  ctx: typer.Context,
1945
2244
  policy: str = typer.Option(
1946
2245
  ...,
@@ -1950,11 +2249,77 @@ def validate_policy_cmd(
1950
2249
  help=f"Policy specification: {policy_arg_example}",
1951
2250
  rich_help_panel="Policy",
1952
2251
  ),
2252
+ output: Path = typer.Option( # noqa: B008
2253
+ Path("submission.zip"),
2254
+ "--output",
2255
+ "-o",
2256
+ metavar="PATH",
2257
+ help="Output path for the bundle zip",
2258
+ rich_help_panel="Output",
2259
+ ),
2260
+ init_kwarg: Optional[list[str]] = typer.Option( # noqa: B008
2261
+ None,
2262
+ "--init-kwarg",
2263
+ "-k",
2264
+ metavar="KEY=VAL",
2265
+ help="Policy init kwargs (can be repeated)",
2266
+ rich_help_panel="Policy",
2267
+ ),
2268
+ include_files: Optional[list[str]] = typer.Option( # noqa: B008
2269
+ None,
2270
+ "--include-files",
2271
+ "-f",
2272
+ metavar="PATH",
2273
+ help="Files or directories to include (can be repeated)",
2274
+ rich_help_panel="Files",
2275
+ ),
1953
2276
  setup_script: Optional[str] = typer.Option(
1954
2277
  None,
1955
2278
  "--setup-script",
1956
- help="Path to a Python setup script to run before loading the policy",
1957
- rich_help_panel="Policy",
2279
+ metavar="PATH",
2280
+ help="Python setup script to include in the bundle",
2281
+ rich_help_panel="Files",
2282
+ ),
2283
+ _help: bool = typer.Option(
2284
+ False,
2285
+ "--help",
2286
+ "-h",
2287
+ help="Show this message and exit",
2288
+ is_eager=True,
2289
+ callback=_help_callback,
2290
+ rich_help_panel="Other",
2291
+ ),
2292
+ ) -> None:
2293
+ init_kwargs: dict[str, str] = {}
2294
+ if init_kwarg:
2295
+ for kv in init_kwarg:
2296
+ key, val = _parse_init_kwarg(kv)
2297
+ init_kwargs[key] = val
2298
+
2299
+ result_path = create_bundle(
2300
+ ctx=ctx,
2301
+ policy=policy,
2302
+ output=output.resolve(),
2303
+ include_files=include_files,
2304
+ init_kwargs=init_kwargs if init_kwargs else None,
2305
+ setup_script=setup_script,
2306
+ )
2307
+ console.print(f"[green]Bundle created:[/green] {result_path}")
2308
+
2309
+
2310
+ @app.command(
2311
+ name="validate-bundle",
2312
+ help="Validate a policy bundle runs correctly in process isolation",
2313
+ rich_help_panel="Policies",
2314
+ add_help_option=False,
2315
+ )
2316
+ def validate_bundle_cmd(
2317
+ policy: str = typer.Option(
2318
+ ...,
2319
+ "--policy",
2320
+ "-p",
2321
+ metavar="URI",
2322
+ help="Bundle URI (file://, s3://, or local path to .zip or directory)",
1958
2323
  ),
1959
2324
  season: Optional[str] = typer.Option(
1960
2325
  None,
@@ -1989,31 +2354,8 @@ def validate_policy_cmd(
1989
2354
  with TournamentServerClient(server_url=server) as client:
1990
2355
  config_data = client.get_config(entry_pool_info.config_id)
1991
2356
  env_cfg = MettaGridConfig.model_validate(config_data)
2357
+ validate_bundle(policy, env_cfg)
1992
2358
 
1993
- if setup_script:
1994
- import subprocess
1995
- import sys
1996
- from pathlib import Path
1997
-
1998
- script_path = Path(setup_script)
1999
- if not script_path.exists():
2000
- console.print(f"[red]Setup script not found: {setup_script}[/red]")
2001
- raise typer.Exit(1)
2002
- console.print(f"[yellow]Running setup script: {setup_script}[/yellow]")
2003
- result = subprocess.run(
2004
- [sys.executable, str(script_path)],
2005
- cwd=Path.cwd(),
2006
- capture_output=True,
2007
- text=True,
2008
- timeout=300,
2009
- )
2010
- if result.returncode != 0:
2011
- console.print(f"[red]Setup script failed:[/red]\n{result.stderr}")
2012
- raise typer.Exit(1)
2013
- console.print("[green]Setup script completed[/green]")
2014
-
2015
- policy_spec = get_policy_spec(ctx, policy)
2016
- validate_policy_spec(policy_spec, env_cfg)
2017
2359
  console.print("[green]Policy validated successfully[/green]")
2018
2360
  raise typer.Exit(0)
2019
2361
 
@@ -2140,11 +2482,6 @@ def upload_cmd(
2140
2482
  ) -> None:
2141
2483
  season_info = _resolve_season(server, season)
2142
2484
 
2143
- has_entry_config = any(p.config_id for p in season_info.pools if p.name == season_info.entry_pool)
2144
- if not has_entry_config and not skip_validation:
2145
- console.print("[yellow]Warning: No entry config found for season. Skipping validation.[/yellow]")
2146
- skip_validation = True
2147
-
2148
2485
  init_kwargs: dict[str, str] = {}
2149
2486
  if init_kwarg:
2150
2487
  for kv in init_kwarg:
@@ -2162,7 +2499,6 @@ def upload_cmd(
2162
2499
  skip_validation=skip_validation,
2163
2500
  init_kwargs=init_kwargs if init_kwargs else None,
2164
2501
  setup_script=setup_script,
2165
- validation_season=season_info.name,
2166
2502
  season=season_info.name if not no_submit else None,
2167
2503
  )
2168
2504
 
@@ -2170,7 +2506,7 @@ def upload_cmd(
2170
2506
  console.print(f"[green]Upload complete: {result.name}:v{result.version}[/green]")
2171
2507
  if result.pools:
2172
2508
  console.print(f"[dim]Added to pools: {', '.join(result.pools)}[/dim]")
2173
- console.print(f"[dim]Results:[/dim] {results_url_for_season(server, season_info.name)}")
2509
+ console.print(f"[dim]Results:[/dim] {RESULTS_URL}")
2174
2510
  elif no_submit:
2175
2511
  console.print(f"\nTo submit to a tournament: cogames submit {result.name}:v{result.version}")
2176
2512
 
@@ -2224,7 +2560,7 @@ def submit_cmd(
2224
2560
  rich_help_panel="Other",
2225
2561
  ),
2226
2562
  ) -> None:
2227
- import httpx
2563
+ import httpx # noqa: PLC0415
2228
2564
 
2229
2565
  season_info = _resolve_season(server, season)
2230
2566
  season_name = season_info.name
@@ -2268,7 +2604,7 @@ def submit_cmd(
2268
2604
  console.print(f"\n[bold green]Submitted to season '{season_name}'[/bold green]")
2269
2605
  if result.pools:
2270
2606
  console.print(f"[dim]Added to pools: {', '.join(result.pools)}[/dim]")
2271
- console.print(f"[dim]Results:[/dim] {results_url_for_season(server, season_name)}")
2607
+ console.print(f"[dim]Results:[/dim] {RESULTS_URL}")
2272
2608
  console.print(f"[dim]CLI:[/dim] cogames leaderboard --season {season_name}")
2273
2609
 
2274
2610
 
@@ -2322,7 +2658,7 @@ def docs_cmd(
2322
2658
 
2323
2659
  # If no argument provided, show available documents
2324
2660
  if doc_name is None:
2325
- from rich.table import Table
2661
+ from rich.table import Table # noqa: PLC0415
2326
2662
 
2327
2663
  console.print("\n[bold cyan]Available Documents:[/bold cyan]\n")
2328
2664
  table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED, padding=(0, 1))