bat-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. add/__init__.py +3 -0
  2. add/client.py +16 -0
  3. bat_cli-0.1.0.dist-info/METADATA +231 -0
  4. bat_cli-0.1.0.dist-info/RECORD +47 -0
  5. bat_cli-0.1.0.dist-info/WHEEL +5 -0
  6. bat_cli-0.1.0.dist-info/entry_points.txt +2 -0
  7. bat_cli-0.1.0.dist-info/top_level.txt +8 -0
  8. build/__init__.py +3 -0
  9. build/build.py +79 -0
  10. cli.py +260 -0
  11. create/__init__.py +3 -0
  12. create/agent.py +312 -0
  13. create/templates/agent/.dockerignore +3 -0
  14. create/templates/agent/.env.template +4 -0
  15. create/templates/agent/.python-version +1 -0
  16. create/templates/agent/Dockerfile +37 -0
  17. create/templates/agent/Makefile +34 -0
  18. create/templates/agent/README.md +1 -0
  19. create/templates/agent/__main__.py +2 -0
  20. create/templates/agent/agent.json.template +12 -0
  21. create/templates/agent/agent.spec +45 -0
  22. create/templates/agent/config.yaml +1 -0
  23. create/templates/agent/llm_client.py.template +36 -0
  24. create/templates/agent/pyproject.toml.template +9 -0
  25. create/templates/agent/src/__init__.py +0 -0
  26. create/templates/agent/src/graph.py +50 -0
  27. create/templates/agent/src/llm_clients/__init__.py +0 -0
  28. create/templates/agent/tests/__init__.py +0 -0
  29. eval/__init__.py +1 -0
  30. eval/commands.py +562 -0
  31. eval/engine/__init__.py +1 -0
  32. eval/engine/adapter.py +251 -0
  33. eval/engine/bench_runner.py +149 -0
  34. eval/engine/contracts.py +115 -0
  35. eval/engine/eval_config.py +294 -0
  36. eval/engine/evaluator.py +85 -0
  37. eval/engine/metrics/__init__.py +1 -0
  38. eval/engine/metrics/llm_evaluators.py +383 -0
  39. eval/engine/metrics/metrics.py +135 -0
  40. eval/engine/metrics/qualitative_helpers.py +64 -0
  41. eval/engine/orchestrator.py +157 -0
  42. eval/engine/plotter.py +347 -0
  43. image_defaults.py +80 -0
  44. push/__init__.py +3 -0
  45. push/push.py +58 -0
  46. set/__init__.py +3 -0
  47. set/env.py +50 -0
add/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .client import add_clients_to_existing_agent
2
+
3
+ __all__ = ["add_clients_to_existing_agent"]
add/client.py ADDED
@@ -0,0 +1,16 @@
1
+ from pathlib import Path
2
+ from create.agent import _write_llm_clients
3
+
4
+ def add_clients_to_existing_agent(
5
+ agent_dir: Path,
6
+ *,
7
+ clients: list[str],
8
+ force: bool = False,
9
+ ) -> list[Path]:
10
+ llm_clients_dir = agent_dir / "src" / "llm_clients"
11
+ if not llm_clients_dir.is_dir():
12
+ raise FileNotFoundError(
13
+ f"Directory '{llm_clients_dir}' not found. Run this command from an agent root containing src/llm_clients."
14
+ )
15
+
16
+ return _write_llm_clients(llm_clients_dir, clients=clients, force=force)
@@ -0,0 +1,231 @@
1
+ Metadata-Version: 2.4
2
+ Name: bat-cli
3
+ Version: 0.1.0
4
+ Summary: CLI tool to interact with BAT agents
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: typer>=0.12.3
8
+ Requires-Dist: bat-adk>=2026.06rc1
9
+ Requires-Dist: bat-adk[openai]
10
+ Requires-Dist: a2a-sdk>=1.0.0
11
+ Requires-Dist: python-dotenv>=1.0.1
12
+ Requires-Dist: typing-extensions>=4.12.0
13
+ Requires-Dist: matplotlib>=3.8
14
+
15
+ # bat-cli
16
+
17
+ A CLI tool for creating, building, and evaluating BAT agent projects.
18
+
19
+ ## Prerequisites
20
+
21
+ - Python and [uv](https://docs.astral.sh/uv/) installed
22
+ - Docker installed (required for `bat build` and `bat push`)
23
+ - For evaluation commands: an existing BAT agent root containing `agent.json`, `config.yaml`, and `src/graph.py`
24
+
25
+ ---
26
+
27
+ ## Installation
28
+
29
+ ### Option A — build and install a standalone binary (Linux/macOS)
30
+
31
+ Run the helper script from the repo root:
32
+
33
+ ```bash
34
+ bash cli/build_and_install.sh
35
+ ```
36
+
37
+ This will:
38
+
39
+ 1. Sync `dev` and `packaging` dependency groups via `uv`.
40
+ 2. Build a one-file executable with PyInstaller.
41
+ 3. Move it to `~/.local/bin/bat` (uses `sudo` only when necessary).
42
+
43
+ Make sure `~/.local/bin` is in your `PATH`:
44
+
45
+ ```bash
46
+ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc
47
+ source ~/.bashrc
48
+ ```
49
+
50
+ Then verify:
51
+
52
+ ```bash
53
+ bat --help
54
+ ```
55
+
56
+ ### Option B — build manually
57
+
58
+ ```bash
59
+ uv sync --group dev --group packaging
60
+ uv run pyinstaller --clean --noconfirm bat_cli.spec
61
+ # binary is at dist/bat (Linux/macOS) or dist/bat.exe (Windows)
62
+ ```
63
+
64
+ On **Windows**, copy `dist/bat.exe` to a folder on your `PATH` (e.g. `C:\tools\bat`) and open a new terminal.
65
+
66
+ > PyInstaller builds are OS-specific — build on each target OS.
67
+
68
+ ### Option C — run without installing (development)
69
+
70
+ ```bash
71
+ uv sync --group dev
72
+ uv run bat --help
73
+ ```
74
+
75
+ All examples below show `bat ...`; replace with `uv run bat ...` when using this option.
76
+
77
+ ---
78
+
79
+ ## Command Tree
80
+
81
+ ```
82
+ bat
83
+ ├── init
84
+ │ └── agent
85
+ │ ├── [name=default]
86
+ │ ├── --clients, -c
87
+ │ ├── --output-dir, -o
88
+ │ ├── --force, -f
89
+ │ ├── --port
90
+ │ ├── --model
91
+ │ └── --model-provider
92
+ ├── add
93
+ │ └── client
94
+ │ ├── <clients>
95
+ │ └── --force, -f
96
+ ├── set
97
+ │ └── env
98
+ │ ├── --port
99
+ │ ├── --model
100
+ │ ├── --model-provider
101
+ │ ├── --docker-registry
102
+ │ └── --repo
103
+ ├── eval
104
+ │ ├── init
105
+ │ │ └── --force, -f
106
+ │ └── run
107
+ ├── build
108
+ │ ├── --context, -C
109
+ │ ├── --docker-registry
110
+ │ ├── --repo
111
+ │ ├── --tag
112
+ │ ├── --version
113
+ │ └── --no-cache
114
+ └── push
115
+ ├── --context, -C
116
+ ├── --docker-registry
117
+ ├── --repo
118
+ └── --tag
119
+ ```
120
+
121
+ Built-in help is available at every level:
122
+
123
+ ```bash
124
+ bat --help
125
+ bat init agent --help
126
+ bat build --help
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Workflows
132
+
133
+ ### 1. Create a new agent
134
+
135
+ ```bash
136
+ # default name
137
+ bat init agent
138
+
139
+ # custom name
140
+ bat init agent my_agent
141
+
142
+ # specific output directory
143
+ bat init agent my_agent --output-dir .
144
+
145
+ # pre-generate LLM clients
146
+ bat init agent my_agent --clients reformulator,planner,executor
147
+ ```
148
+
149
+ ### 2. Add clients to an existing agent
150
+
151
+ Run from the agent root (must contain `src/llm_clients/`):
152
+
153
+ ```bash
154
+ bat add client planner,executor
155
+
156
+ # overwrite existing files
157
+ bat add client planner,executor --force
158
+ ```
159
+
160
+ ### 3. Update agent environment variables
161
+
162
+ Run from the agent root (updates or creates `.env`):
163
+
164
+ ```bash
165
+ bat set env --port 8080 --model gpt-4.1-mini --model-provider openai
166
+
167
+ # also set Docker defaults for build/push
168
+ bat set env --docker-registry hub.bubbleran.com --repo orama/labs/my-agent
169
+ ```
170
+
171
+ ### 4. Build and push a Docker image
172
+
173
+ ```bash
174
+ bat build --context ./my_agent --docker-registry hub.bubbleran.com --repo orama/labs/my-agent --tag latest
175
+
176
+ # no-cache build with version build-arg
177
+ bat build --context ./my_agent --repo orama/labs/my-agent --tag v1 --version 1.0.0 --no-cache
178
+
179
+ bat push --context ./my_agent --docker-registry hub.bubbleran.com --repo orama/labs/my-agent --tag latest
180
+ ```
181
+
182
+ If `BAT_DOCKER_REGISTRY` and `BAT_DOCKER_REPO` are already set in `.env` or the shell, `--docker-registry` and `--repo` can be omitted.
183
+
184
+ **Precedence** (both `--docker-registry` / `--repo`):
185
+
186
+ 1. CLI flag
187
+ 2. Shell environment variable (`BAT_DOCKER_REGISTRY` / `BAT_DOCKER_REPO`)
188
+ 3. `.env` file in the current directory
189
+ 4. Hardcoded default (`default_registry` / `default-repository/<project-name>`)
190
+
191
+ ### 5. Run evaluation
192
+
193
+ From an existing agent root:
194
+
195
+ ```bash
196
+ # scaffold evaluation files
197
+ bat eval init
198
+
199
+ # run evaluation
200
+ bat eval run
201
+ ```
202
+
203
+ `eval init` creates:
204
+
205
+ - `eval/eval.yaml`
206
+ - `eval/input/tasks.json`
207
+ - `eval/output/`
208
+
209
+ Minimal `eval/eval.yaml`:
210
+
211
+ ```yaml
212
+ evaluation:
213
+ dataset: eval/input/tasks.json
214
+ output_dir: eval/output
215
+ k: 1
216
+ qualitative: true
217
+ save_attempts: false
218
+
219
+ judge:
220
+ provider: ollama
221
+ model: your-judge-model
222
+ base_url: http://localhost:11434
223
+
224
+ models:
225
+ - provider: openai
226
+ model: your-model-name
227
+ ```
228
+
229
+ `eval run` requires the agent virtual environment at `.venv/bin/python` (`.venv/Scripts/python.exe` on Windows).
230
+
231
+ For model that requires API_KEYS set it into the agent `.env` under <PROVIDER>\_API_KEY.
@@ -0,0 +1,47 @@
1
+ cli.py,sha256=ATKa5kPdme4zS4wiOGlwSd9qRInxb0qqF7fCzXar1sk,8145
2
+ image_defaults.py,sha256=ItpLwN_gl7vVzM8Oyn_kxoAO-WtUFISBUYTHl2ZbheI,1807
3
+ add/__init__.py,sha256=KSwafPATDdeoEYHARrXDC6c4NGVdqfGvuAbFg8RbqWQ,95
4
+ add/client.py,sha256=dGEE7aw1FuALlLxOEGlIK4-EKAqMXYFNp_qtk60C09w,533
5
+ build/__init__.py,sha256=FB47dgFX4B5bacfCeEKA_7b0Bhpe0bgbNd8Zu7572kA,58
6
+ build/build.py,sha256=Zshd-Ymo8DjqnqvS8Fvbz3C4q28Mr0iePP4MdhxY2-U,2665
7
+ create/__init__.py,sha256=KABTrX_9ly8gWVlaYU1m8PeLErUv0WDt5O1JB0Z4a40,78
8
+ create/agent.py,sha256=IHpz8QL3iOBD_BOHBXT8rzC9U7BmfkbM5X9TGvnOlLw,9375
9
+ create/templates/agent/.dockerignore,sha256=TK0TPU8KbPS7oV8bi7rkjWpV8guU91SHxRqTzGpa9Z8,26
10
+ create/templates/agent/.env.template,sha256=7BKZIauBq77d5fMBoK1ePHqByipmk0RHl4D9utaNZTs,126
11
+ create/templates/agent/.python-version,sha256=MzB9pWrxP3kVhFGP7y5JZBGAv78u97LSVsmrb61WT4A,4
12
+ create/templates/agent/Dockerfile,sha256=rNbaEv6AG5UTFpZtmI4KDqYtV7Q_r3UM_zclnlmW3tk,716
13
+ create/templates/agent/Makefile,sha256=CgAhJ2nbkPbjmUv4QIikjPsZDXUmVntIz-bKO_xBaBc,911
14
+ create/templates/agent/README.md,sha256=I1MrCvRD2c7ER4PcoR5m11wNq6VL9CDYyhFgqSf0G44,20
15
+ create/templates/agent/__main__.py,sha256=bXSrkOUtyAeb_mmaDAoFcc1iMOHbGy8CiUWttFu6cnQ,53
16
+ create/templates/agent/agent.json.template,sha256=nprO37SgOJoyFVOZgxHDLPu43HlrI4pjxq6KNi2CqHw,236
17
+ create/templates/agent/agent.spec,sha256=YQU-Qtv4I_1Knp7sZLVvdxy3O0PcxwOt4FtrRJ_CWYg,661
18
+ create/templates/agent/config.yaml,sha256=5PnGziRDshBnIHPn5RiuvA1wa0SRgv1mAHwZpdEg_LQ,19
19
+ create/templates/agent/llm_client.py.template,sha256=fAuIqVv5Fs8wlnyh0ZZsiiQgxsE859Kay82FHVXmiEw,974
20
+ create/templates/agent/pyproject.toml.template,sha256=JUmq3CQhgEAcGv8GRokV5hPKNU2OP5KTLi2zKYyfiRc,196
21
+ create/templates/agent/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ create/templates/agent/src/graph.py,sha256=CYHb2EIBRkT9QA7eV2HHSg3dHJWfBmx-DvR3IKyzJZg,1114
23
+ create/templates/agent/src/llm_clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ create/templates/agent/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
+ eval/__init__.py,sha256=Mx15-MKFaDtmsc9ubisr-wXH0Hs57YX0QTr7183xz0g,46
26
+ eval/commands.py,sha256=FuVFrWC-rdU90Rame1MaQVXrgLOAr2nyplndeYuUB9U,18361
27
+ eval/engine/__init__.py,sha256=8fEKHuy3Ud4ep4fw762uu51DV1SBj1xlonolmNcQbeY,56
28
+ eval/engine/adapter.py,sha256=yynP98FRNx1LZgJme92cnCu6tE1sSBtd0mVgfoVRVIo,9303
29
+ eval/engine/bench_runner.py,sha256=IhcB_cBTxEL0u05jFvcHB4P7Na_2lzU9lo3kJrX1Aoo,5130
30
+ eval/engine/contracts.py,sha256=DAHpsD2x8x5_zuMQS9nKix8p4e9DltqMque26_7P9ug,3255
31
+ eval/engine/eval_config.py,sha256=9S6LV_Wbk7Sry0nRXP9S8fs7yNMYkKNSoSamfDQM0Wc,9559
32
+ eval/engine/evaluator.py,sha256=XZ2dmZS_VhjGTsISRf3twU3Iz9okDNU_PziCk1qqcE8,2877
33
+ eval/engine/orchestrator.py,sha256=PrCDmEyngI0dciPXS9-qAUcPbqNgt3dTQ-I7CVJdweo,5330
34
+ eval/engine/plotter.py,sha256=xdw1ppMOfI98yz5vEN4tZhQs8E-vnI7ORk-MKfjwiDU,15462
35
+ eval/engine/metrics/__init__.py,sha256=JAVJTRLC2M2Hv41BcImXk_ZYgDb_z3JEHxkepXtce90,48
36
+ eval/engine/metrics/llm_evaluators.py,sha256=Q9qy3usexnjzB8RZW-Mku63s_Ot7Jf1pZsmF_inGLNk,18080
37
+ eval/engine/metrics/metrics.py,sha256=U3IAKQ2H_dvVGQArWS8OwuAcNBhcfVbjMXRydiz5VGc,4877
38
+ eval/engine/metrics/qualitative_helpers.py,sha256=OYNehel5l8BAmbhXH4UxxQMdJC7cumK6kkFNG0HNIIA,2404
39
+ push/__init__.py,sha256=2zzcgnvuK-xfH2ArR4jQHyIjIFUwvyNx2hEPD1zb4f0,55
40
+ push/push.py,sha256=3cqwGL_JJc-cugM7QBTLr9Ud3-fen2TSJy4KHbG8CCs,1953
41
+ set/__init__.py,sha256=e57CrZacOKMkzwII1VdYhxvBWF3Tt02_qw7c0BJ-Dt8,62
42
+ set/env.py,sha256=mAuNfhXyji0ij0H2qdc-aIlPqYSE5lQlxpJ5IKF4UHc,1656
43
+ bat_cli-0.1.0.dist-info/METADATA,sha256=JU4kguzwW8wiF3NAKz3FZIPQ_3WIfMIZzm9Za2CmI60,5266
44
+ bat_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
45
+ bat_cli-0.1.0.dist-info/entry_points.txt,sha256=xsln8lLso51-g1XbUBxmAaFCNS1Hxlf611qrMLKYoxs,33
46
+ bat_cli-0.1.0.dist-info/top_level.txt,sha256=JE51f0xnE-1CGdjBjVh7QrmCww9GcHSj7EjzzaoBi8E,50
47
+ bat_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bat = cli:main
@@ -0,0 +1,8 @@
1
+ add
2
+ build
3
+ cli
4
+ create
5
+ eval
6
+ image_defaults
7
+ push
8
+ set
build/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .build import build_image
2
+
3
+ __all__ = ["build_image"]
build/build.py ADDED
@@ -0,0 +1,79 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+ import typer
5
+
6
+ from image_defaults import resolve_registry, resolve_repo_name
7
+
8
+
9
+ def build_image(
10
+ context: Path = typer.Option(
11
+ Path("."),
12
+ "--context",
13
+ "-C",
14
+ help="Directory used as docker build context and to infer the default repository name.",
15
+ ),
16
+ docker_registry: str | None = typer.Option(
17
+ None,
18
+ "--docker-registry",
19
+ help=(
20
+ "Docker registry hostname. Precedence: --docker-registry > "
21
+ "BAT_DOCKER_REGISTRY env var (or .env in current directory) > default_registry."
22
+ ),
23
+ ),
24
+ repo: str | None = typer.Option(
25
+ None,
26
+ "--repo",
27
+ help=(
28
+ "Image repository path. Precedence: --repo > BAT_DOCKER_REPO env var "
29
+ "(or .env in current directory) > default-repository/<project-name>."
30
+ ),
31
+ ),
32
+ tag: str = typer.Option(
33
+ "latest",
34
+ "--tag",
35
+ help="Image tag.",
36
+ ),
37
+ version: str | None = typer.Option(
38
+ None,
39
+ "--version",
40
+ help="Optional VERSION build arg passed to docker build.",
41
+ ),
42
+ no_cache: bool = typer.Option(
43
+ False,
44
+ "--no-cache",
45
+ help="Disable docker layer cache during build.",
46
+ ),
47
+ ) -> None:
48
+ context_dir = context.resolve()
49
+ if not context_dir.is_dir():
50
+ typer.secho(f"Context directory not found: {context_dir}", fg=typer.colors.RED, err=True)
51
+ raise typer.Exit(code=1)
52
+
53
+ dockerfile_path = context_dir / "Dockerfile"
54
+ if not dockerfile_path.is_file():
55
+ typer.secho(f"Dockerfile not found in context: {dockerfile_path}", fg=typer.colors.RED, err=True)
56
+ raise typer.Exit(code=1)
57
+
58
+ resolved_registry = resolve_registry(context_dir, docker_registry)
59
+ resolved_repo = resolve_repo_name(context_dir, repo)
60
+ image = f"{resolved_registry}/{resolved_repo}:{tag}"
61
+
62
+ command = ["docker", "build"]
63
+ if no_cache:
64
+ command.append("--no-cache")
65
+ if version:
66
+ command.extend(["--build-arg", f"VERSION={version}"])
67
+ command.extend(["--tag", image, "."])
68
+
69
+ typer.echo(f"Building Docker image: {image}")
70
+ try:
71
+ subprocess.run(command, check=True, cwd=context_dir)
72
+ except FileNotFoundError as exc:
73
+ typer.secho("Docker executable not found in PATH.", fg=typer.colors.RED, err=True)
74
+ raise typer.Exit(code=1) from exc
75
+ except subprocess.CalledProcessError as exc:
76
+ typer.secho("Docker build failed.", fg=typer.colors.RED, err=True)
77
+ raise typer.Exit(code=exc.returncode or 1) from exc
78
+
79
+ typer.secho(f"Docker image built successfully: {image}", fg=typer.colors.GREEN)
cli.py ADDED
@@ -0,0 +1,260 @@
1
+ import shutil
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ import click
6
+ import typer
7
+ from typer.core import TyperGroup
8
+
9
+ from add.client import add_clients_to_existing_agent
10
+ from build.build import build_image
11
+ from create.agent import create_agent_scaffold
12
+ from eval.commands import eval_init, eval_plot, eval_run, eval_show
13
+ from push.push import push_image
14
+ from set.env import set_env_values
15
+
16
+
17
+ _BANNER_COLORS = (51, 45, 39, 63, 99, 135)
18
+
19
+ _FALLBACK_BANNER = r"""
20
+ ____ _ _____ ____ _ ___
21
+ | __ ) / \|_ _| / ___| | |_ _|
22
+ | _ \ / _ \ | | | | | | | |
23
+ | |_) / ___ \| | | |___| |___ | |
24
+ |____/_/ \_\_| \____|_____|___|
25
+ """
26
+
27
+ _BANNER_MOTD = """
28
+ Welcome to BubbleRAN Agentic Toolkit CLI tool.
29
+
30
+ Scaffold, build, push, and evaluate BAT agents from one place.
31
+
32
+ """
33
+
34
+
35
+ def _figlet_banner(text: str) -> str | None:
36
+ if shutil.which("figlet") is None:
37
+ return None
38
+ try:
39
+ return subprocess.check_output(
40
+ ["figlet", "-f", "standard", text],
41
+ text=True,
42
+ stderr=subprocess.DEVNULL,
43
+ )
44
+ except (OSError, subprocess.SubprocessError):
45
+ return None
46
+
47
+
48
+ def _colorize_line(line: str) -> str:
49
+ n = len(line)
50
+ if n == 0:
51
+ return ""
52
+ palette = _BANNER_COLORS
53
+ pieces = []
54
+ for i, ch in enumerate(line):
55
+ idx = min(int(i * len(palette) / n), len(palette) - 1)
56
+ pieces.append(f"\033[38;5;{palette[idx]}m{ch}")
57
+ pieces.append("\033[0m")
58
+ return "".join(pieces)
59
+
60
+
61
+ def _render_banner() -> str:
62
+ art = _figlet_banner("BAT-CLI") or _FALLBACK_BANNER
63
+ colored = "\n".join(_colorize_line(line) for line in art.splitlines())
64
+ return f"{colored}\n{_BANNER_MOTD}"
65
+
66
+
67
+ class BannerGroup(TyperGroup):
68
+ def format_help(self, ctx, formatter):
69
+ click.echo(_render_banner().rstrip("\n") + "\n", nl=False)
70
+ super().format_help(ctx, formatter)
71
+
72
+
73
+ app = typer.Typer(
74
+ cls=BannerGroup
75
+ )
76
+ init_app = typer.Typer(help="Create new BAT resources.")
77
+ add_app = typer.Typer(help="Add new components to existing BAT agents.")
78
+ set_app = typer.Typer(help="Set configuration values for existing BAT agents.")
79
+ eval_app = typer.Typer(help="Run local evaluation workflows for existing BAT agents.")
80
+
81
+ app.add_typer(init_app, name="init")
82
+ app.add_typer(add_app, name="add")
83
+ app.add_typer(set_app, name="set")
84
+ app.add_typer(eval_app, name="eval")
85
+ app.command("build", help="Build the Docker image for the agent.")(build_image)
86
+ app.command("push", help="Push the Docker image to a registry.")(push_image)
87
+ eval_app.command("init", help="Initialize local evaluation scaffold.")(eval_init)
88
+ eval_app.command("run", help="Run evaluation using eval/eval.yaml.")(eval_run)
89
+ eval_app.command("show", help="Show the resolved evaluation configuration.")(eval_show)
90
+ eval_app.command("plot", help="Generate metric charts from an evaluation output folder.")(eval_plot)
91
+
92
+
93
+ def _parse_clients_option(raw_clients: str | None) -> list[str] | None:
94
+ if raw_clients is None:
95
+ return None
96
+ parsed_clients = [client.strip() for client in raw_clients.split(",") if client.strip()]
97
+ if not parsed_clients:
98
+ raise typer.BadParameter("Provide at least one client name, for example: reformulator,planner,executor")
99
+ return parsed_clients
100
+
101
+
102
+ @init_app.command("agent")
103
+ def create_new_agent(
104
+ name: str = typer.Argument("default", help="Name of the agent directory to create."),
105
+ clients: str | None = typer.Option(
106
+ None,
107
+ "--clients",
108
+ "-c",
109
+ help="Optional comma-separated LLM client names to generate, for example: reformulator,planner,executor",
110
+ ),
111
+ output_dir: Path = typer.Option(
112
+ Path("."),
113
+ "--output-dir",
114
+ "-o",
115
+ help="Directory where the agent folder will be created.",
116
+ ),
117
+ force: bool = typer.Option(
118
+ False,
119
+ "--force",
120
+ "-f",
121
+ help="Overwrite existing files when the target directory exists. Use with caution as this will delete existing files in the target directory.",
122
+ ),
123
+ port: int = typer.Option(
124
+ 9900,
125
+ "--port",
126
+ help="Port value written to .env.",
127
+ ),
128
+ model: str = typer.Option(
129
+ "gpt-4o-mini",
130
+ "--model",
131
+ help="Model value written to .env.",
132
+ ),
133
+ model_provider: str = typer.Option(
134
+ "openai",
135
+ "--model-provider",
136
+ "--model_provider",
137
+ help="Model provider value written to .env.",
138
+ ),
139
+ ) -> None:
140
+ target_dir = output_dir / name
141
+ parsed_clients = _parse_clients_option(clients)
142
+
143
+ try:
144
+ created_files = create_agent_scaffold(
145
+ target_dir,
146
+ force=force,
147
+ clients=parsed_clients,
148
+ port=port,
149
+ model=model,
150
+ model_provider=model_provider,
151
+ )
152
+ except FileExistsError as exc:
153
+ typer.secho(str(exc), fg=typer.colors.RED, err=True)
154
+ raise typer.Exit(code=1) from exc
155
+
156
+ typer.secho(f"Created BAT agent skeleton in: {target_dir.resolve()}", fg=typer.colors.GREEN)
157
+ typer.echo(f"Files written: {len(created_files)}")
158
+
159
+ @add_app.command("client")
160
+ def add_new_client(
161
+ clients: str = typer.Argument(
162
+ ...,
163
+ help="Comma-separated LLM client names to generate, for example: reformulator,planner,executor",
164
+ ),
165
+ force: bool = typer.Option(
166
+ False,
167
+ "--force",
168
+ "-f",
169
+ help="Overwrite existing client files if they already exist. Use with caution as this will delete existing client files with the same name.",
170
+ ),
171
+ ) -> None:
172
+ current_dir = Path.cwd()
173
+ llm_clients_dir = current_dir / "src" / "llm_clients"
174
+ if not llm_clients_dir.is_dir():
175
+ typer.secho(
176
+ "Current directory must contain src/llm_clients. Run this command from the root of an existing agent.",
177
+ fg=typer.colors.RED,
178
+ err=True,
179
+ )
180
+ raise typer.Exit(code=1)
181
+
182
+ parsed_clients = _parse_clients_option(clients) or []
183
+
184
+ try:
185
+ created_files = add_clients_to_existing_agent(current_dir, clients=parsed_clients, force=force)
186
+ except FileNotFoundError as exc:
187
+ typer.secho(str(exc), fg=typer.colors.RED, err=True)
188
+ raise typer.Exit(code=1) from exc
189
+
190
+ typer.secho(f"Updated LLM clients in: {llm_clients_dir.resolve()}", fg=typer.colors.GREEN)
191
+ typer.echo(f"Files written: {len(created_files)}")
192
+
193
+
194
+ @set_app.command("env")
195
+ def set_agent_env(
196
+ port: int | None = typer.Option(
197
+ None,
198
+ "--port",
199
+ help="Set PORT in .env.",
200
+ ),
201
+ model: str | None = typer.Option(
202
+ None,
203
+ "--model",
204
+ help="Set MODEL in .env.",
205
+ ),
206
+ model_provider: str | None = typer.Option(
207
+ None,
208
+ "--model-provider",
209
+ "--model_provider",
210
+ help="Set MODEL_PROVIDER in .env.",
211
+ ),
212
+ docker_registry: str | None = typer.Option(
213
+ None,
214
+ "--docker-registry",
215
+ help="Set BAT_DOCKER_REGISTRY in .env for build/push defaults.",
216
+ ),
217
+ repo: str | None = typer.Option(
218
+ None,
219
+ "--repo",
220
+ help="Set BAT_DOCKER_REPO in .env for build/push defaults.",
221
+ ),
222
+ ) -> None:
223
+ current_dir = Path.cwd()
224
+ env = current_dir / ".env"
225
+ if not env.is_file():
226
+ typer.secho(
227
+ "Current directory must contain .env. Run this command from the root of an existing agent.",
228
+ fg=typer.colors.RED,
229
+ err=True,
230
+ )
231
+ raise typer.Exit(code=1)
232
+
233
+ if all(value is None for value in [port, model, model_provider, docker_registry, repo]):
234
+ typer.secho(
235
+ "Provide at least one option to set: --port, --model, --model-provider, --docker-registry, --repo",
236
+ fg=typer.colors.RED,
237
+ err=True,
238
+ )
239
+ raise typer.Exit(code=1)
240
+
241
+ env_path, updated_keys = set_env_values(
242
+ current_dir,
243
+ port=port,
244
+ model=model,
245
+ model_provider=model_provider,
246
+ docker_registry=docker_registry,
247
+ repo=repo,
248
+ )
249
+
250
+ typer.secho(f"Updated env file: {env_path.resolve()}", fg=typer.colors.GREEN)
251
+ typer.echo(f"Keys updated: {', '.join(updated_keys)}")
252
+
253
+
254
+
255
+ def main() -> None:
256
+ app()
257
+
258
+
259
+ if __name__ == "__main__":
260
+ main()
create/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .agent import create_agent_scaffold
2
+
3
+ __all__ = ["create_agent_scaffold"]