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
create/agent.py ADDED
@@ -0,0 +1,312 @@
1
+ import re
2
+ from pathlib import Path
3
+ from typing import Literal
4
+
5
+
6
+ BAT_ADK_VERSION = "2026.4.23"
7
+
8
+ TEMPLATES_DIR = Path(__file__).resolve().parent / "templates" / "agent"
9
+ _DYNAMIC_TEMPLATE_FILES = {
10
+ ".env.template",
11
+ "agent.json.template",
12
+ "agent.spec",
13
+ "Dockerfile",
14
+ "Makefile",
15
+ "llm_client.py.template",
16
+ "pyproject.toml.template",
17
+ "src/graph.py",
18
+ }
19
+
20
+
21
+ def _load_static_templates() -> dict[str, str]:
22
+ templates: dict[str, str] = {}
23
+ for template_path in sorted(TEMPLATES_DIR.rglob("*")):
24
+ if not template_path.is_file():
25
+ continue
26
+
27
+ relative_path = template_path.relative_to(TEMPLATES_DIR).as_posix()
28
+ if (
29
+ relative_path in _DYNAMIC_TEMPLATE_FILES
30
+ or "__pycache__" in template_path.parts
31
+ or template_path.suffix == ".pyc"
32
+ ):
33
+ continue
34
+
35
+ templates[relative_path] = template_path.read_text(encoding="utf-8")
36
+
37
+ return templates
38
+
39
+
40
+ def _render_template(template_file: str, replacements: dict[str, str]) -> str:
41
+ template_path = TEMPLATES_DIR / template_file
42
+ if not template_path.exists():
43
+ raise FileNotFoundError(f"Template file not found: {template_path}")
44
+
45
+ rendered = template_path.read_text(encoding="utf-8")
46
+ for key, value in replacements.items():
47
+ rendered = rendered.replace(f"__{key}__", value)
48
+
49
+ return rendered
50
+
51
+
52
+ def _normalize_name(raw: str, style: Literal["project", "snake", "pascal"]) -> str:
53
+ if style == "pascal":
54
+ name = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", raw)
55
+ name = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
56
+ name = re.sub(r"[^A-Za-z0-9]+", "_", name)
57
+ name = re.sub(r"_+", "_", name).strip("_")
58
+ parts = [part for part in name.split("_") if part]
59
+ if parts and parts[-1].lower() == "agent":
60
+ parts = parts[:-1]
61
+ if not parts:
62
+ return ""
63
+ return "".join(part[:1].upper() + part[1:] for part in parts)
64
+
65
+ separator = "-" if style == "project" else "_"
66
+ name = re.sub(r"([A-Z]+)([A-Z][a-z])", rf"\1{separator}\2", raw)
67
+ name = re.sub(r"([a-z0-9])([A-Z])", rf"\1{separator}\2", name)
68
+ name = re.sub(r"[^A-Za-z0-9]+", separator, name)
69
+ collapsed_separator = re.escape(separator)
70
+ name = re.sub(rf"{collapsed_separator}+", separator, name).strip(separator)
71
+
72
+ if style == "project":
73
+ project_name = (name or "agent").lower()
74
+ if project_name.endswith("-agent"):
75
+ project_name = project_name[: -len("-agent")]
76
+ return project_name or "agent"
77
+ return name.lower()
78
+
79
+
80
+ def _build_pyproject_content(agent_dir_name: str) -> str:
81
+ project_name = _normalize_name(agent_dir_name, "project")
82
+ return _render_template(
83
+ "pyproject.toml.template",
84
+ {
85
+ "BAT_ADK_VERSION": BAT_ADK_VERSION,
86
+ "PROJECT_DESCRIPTION": f"{project_name.upper()} Agent",
87
+ "PROJECT_NAME": project_name,
88
+ },
89
+ )
90
+
91
+
92
+ def _build_agent_spec_content(agent_dir_name: str) -> str:
93
+ return _render_template(
94
+ "agent.spec",
95
+ {
96
+ "PROJECT_NAME": _normalize_name(agent_dir_name, "project"),
97
+ },
98
+ )
99
+
100
+
101
+ def _build_dockerfile_content(agent_dir_name: str) -> str:
102
+ return _render_template(
103
+ "Dockerfile",
104
+ {
105
+ "PROJECT_NAME": _normalize_name(agent_dir_name, "project"),
106
+ },
107
+ )
108
+
109
+
110
+ def _build_makefile_content(agent_dir_name: str) -> str:
111
+ return _render_template(
112
+ "Makefile",
113
+ {
114
+ "PROJECT_NAME": _normalize_name(agent_dir_name, "project"),
115
+ },
116
+ )
117
+
118
+
119
+ def _build_graph_content(agent_dir_name: str, clients: list[str] | None) -> str:
120
+ resolved_clients = _resolve_client_specs(clients)
121
+ agent_class_name = _normalize_name(agent_dir_name, "pascal") or "Agent"
122
+
123
+ client_imports = "\n".join(
124
+ f"from .llm_clients.{file_stem} import {class_name}"
125
+ for file_stem, class_name in resolved_clients
126
+ )
127
+
128
+ setup_blocks: list[str] = []
129
+
130
+ for file_stem, class_name in resolved_clients:
131
+
132
+ setup_blocks.append(
133
+ "\n".join(
134
+ [
135
+ f" self.{file_stem} = {class_name}(",
136
+ " tools=[],",
137
+ " )",
138
+ ]
139
+ )
140
+ )
141
+
142
+ return _render_template(
143
+ "src/graph.py",
144
+ {
145
+ "AGENT_CLASS_NAME": agent_class_name,
146
+ "CLIENT_IMPORTS": client_imports,
147
+ "CLIENT_SETUP": "\n\n".join(setup_blocks),
148
+ },
149
+ )
150
+
151
+
152
+ def _build_agent_json_content(agent_dir_name: str) -> str:
153
+ return _render_template(
154
+ "agent.json.template",
155
+ {
156
+ "AGENT_NAME": agent_dir_name,
157
+ },
158
+ )
159
+
160
+
161
+ def _build_env_template_content(
162
+ *,
163
+ port: int,
164
+ model: str,
165
+ model_provider: str,
166
+ ) -> str:
167
+ return _render_template(
168
+ ".env.template",
169
+ {
170
+ "PORT": str(port),
171
+ "MODEL": model,
172
+ "MODEL_PROVIDER": model_provider,
173
+ },
174
+ )
175
+
176
+
177
+ def _build_llm_client_content(class_name: str) -> str:
178
+ return _render_template(
179
+ "llm_client.py.template",
180
+ {
181
+ "CLASS_NAME": class_name,
182
+ },
183
+ )
184
+
185
+
186
+ def _resolve_client_specs(clients: list[str] | None) -> list[tuple[str, str]]:
187
+ if not clients:
188
+ return [("example_client", "ExampleClient")]
189
+
190
+ resolved: list[tuple[str, str]] = []
191
+ seen: set[str] = set()
192
+
193
+ for raw_name in clients:
194
+ snake_name = _normalize_name(raw_name, "snake")
195
+ if not snake_name:
196
+ continue
197
+
198
+ file_stem = snake_name if snake_name.endswith("_client") else f"{snake_name}_client"
199
+ if file_stem in seen:
200
+ continue
201
+
202
+ pascal_name = _normalize_name(raw_name, "pascal")
203
+ if not pascal_name:
204
+ continue
205
+ class_name = pascal_name if pascal_name.endswith("Client") else f"{pascal_name}Client"
206
+
207
+ seen.add(file_stem)
208
+ resolved.append((file_stem, class_name))
209
+
210
+ return resolved or [("example_client", "ExampleClient")]
211
+
212
+
213
+ def _write_llm_clients(
214
+ llm_clients_dir: Path,
215
+ *,
216
+ clients: list[str] | None,
217
+ force: bool,
218
+ ) -> list[Path]:
219
+ created: list[Path] = []
220
+ for file_stem, class_name in _resolve_client_specs(clients):
221
+ client_path = llm_clients_dir / f"{file_stem}.py"
222
+ if client_path.exists() and not force:
223
+ continue
224
+
225
+ client_path.parent.mkdir(parents=True, exist_ok=True)
226
+ client_path.write_text(_build_llm_client_content(class_name), encoding="utf-8")
227
+ created.append(client_path)
228
+
229
+ return created
230
+
231
+
232
+ def create_agent_scaffold(
233
+ target_dir: Path,
234
+ *,
235
+ force: bool = False,
236
+ clients: list[str] | None = None,
237
+ port: int = 9900,
238
+ model: str = "gpt-4o-mini",
239
+ model_provider: str = "openai",
240
+ ) -> list[Path]:
241
+ if target_dir.exists() and any(target_dir.iterdir()) and not force:
242
+ raise FileExistsError(
243
+ f"Target directory '{target_dir}' already exists and is not empty. "
244
+ "Use --force to overwrite files."
245
+ )
246
+
247
+ target_dir.mkdir(parents=True, exist_ok=True)
248
+
249
+ created: list[Path] = []
250
+ for relative_path, content in _load_static_templates().items():
251
+ file_path = target_dir / relative_path
252
+ file_path.parent.mkdir(parents=True, exist_ok=True)
253
+
254
+ if file_path.exists() and not force:
255
+ continue
256
+
257
+ file_path.write_text(content, encoding="utf-8")
258
+ created.append(file_path)
259
+
260
+ pyproject_path = target_dir / "pyproject.toml"
261
+
262
+ if force or not pyproject_path.exists():
263
+ pyproject_path.write_text(_build_pyproject_content(target_dir.name), encoding="utf-8")
264
+ created.append(pyproject_path)
265
+
266
+ agent_json_path = target_dir / "agent.json"
267
+ if force or not agent_json_path.exists():
268
+ agent_json_path.write_text(_build_agent_json_content(target_dir.name), encoding="utf-8")
269
+ created.append(agent_json_path)
270
+
271
+ env_path = target_dir / ".env"
272
+ if force or not env_path.exists():
273
+ env_path.write_text(
274
+ _build_env_template_content(
275
+ port=port,
276
+ model=model,
277
+ model_provider=model_provider,
278
+ ),
279
+ encoding="utf-8",
280
+ )
281
+ created.append(env_path)
282
+
283
+ agent_spec_path = target_dir / "agent.spec"
284
+ if force or not agent_spec_path.exists():
285
+ agent_spec_path.write_text(_build_agent_spec_content(target_dir.name), encoding="utf-8")
286
+ created.append(agent_spec_path)
287
+
288
+ dockerfile_path = target_dir / "Dockerfile"
289
+ if force or not dockerfile_path.exists():
290
+ dockerfile_path.write_text(_build_dockerfile_content(target_dir.name), encoding="utf-8")
291
+ created.append(dockerfile_path)
292
+
293
+ makefile_path = target_dir / "Makefile"
294
+ if force or not makefile_path.exists():
295
+ makefile_path.write_text(_build_makefile_content(target_dir.name), encoding="utf-8")
296
+ created.append(makefile_path)
297
+
298
+ graph_path = target_dir / "src" / "graph.py"
299
+ if force or not graph_path.exists():
300
+ graph_path.write_text(_build_graph_content(target_dir.name, clients), encoding="utf-8")
301
+ created.append(graph_path)
302
+
303
+ created.extend(
304
+ _write_llm_clients(
305
+ target_dir / "src" / "llm_clients",
306
+ clients=clients,
307
+ force=force,
308
+ )
309
+ )
310
+
311
+ return created
312
+
@@ -0,0 +1,3 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
@@ -0,0 +1,4 @@
1
+ # Copy this file to .env and customize values for local runs.
2
+ PORT=__PORT__
3
+ MODEL=__MODEL__
4
+ MODEL_PROVIDER=__MODEL_PROVIDER__
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,37 @@
1
+ FROM ghcr.io/astral-sh/uv:python3.12-bookworm AS base
2
+ FROM debian:bookworm-slim AS runtime
3
+
4
+ # Stage 1: Builder
5
+ FROM base AS builder
6
+
7
+ # Install objdump
8
+ RUN apt-get update && \
9
+ apt-get install -y --no-install-recommends \
10
+ binutils
11
+
12
+ # Copy app files
13
+ WORKDIR /app
14
+ COPY . .
15
+
16
+ # Install dependencies
17
+ RUN uv lock
18
+ RUN uv sync --locked
19
+
20
+ # Build the binary
21
+ RUN uv add pyinstaller
22
+ RUN uv run pyinstaller agent.spec
23
+
24
+ # Strip the binary
25
+ RUN strip dist/__PROJECT_NAME__
26
+
27
+ # Stage 2: Runtime
28
+ FROM runtime
29
+
30
+ # Copy built binary
31
+ COPY --from=builder /app/dist/__PROJECT_NAME__ /app/
32
+ COPY --from=builder /app/agent.json /app/
33
+ COPY --from=builder /app/config.yaml /app/
34
+
35
+ # Entrypoint
36
+ WORKDIR /app
37
+ ENTRYPOINT ["./__PROJECT_NAME__"]
@@ -0,0 +1,34 @@
1
+ KUBE_CONFIG := ~/.kube/config
2
+ DOCKER_REGISTRY ?= INSERT_YOUR_DOCKER_REGISTRY_HERE
3
+ VERSION ?= $(shell git describe --tags --always --abbrev --dirty)
4
+
5
+ PORT ?= 9900
6
+ REPO ?= YOUR_REPOSITORY/__PROJECT_NAME__
7
+ TAG ?= latest
8
+ IMAGE := $(DOCKER_REGISTRY)/$(REPO):$(TAG)
9
+
10
+ .PHONY: build run push clean
11
+
12
+ build:
13
+ @echo "Building __PROJECT_NAME__ Agent Docker image with tag: $(IMAGE) - Version: $(VERSION)"
14
+ docker build $(if $(NO_CACHE),--no-cache) \
15
+ --build-arg VERSION=$(VERSION) \
16
+ --tag $(IMAGE) .
17
+ @echo "__PROJECT_NAME__ Agent Docker image built successfully."
18
+
19
+ run:
20
+ docker run --rm \
21
+ -it \
22
+ --env-file .env \
23
+ --network host \
24
+ -p $(PORT):$(PORT) \
25
+ -v $(KUBE_CONFIG):/root/.kube/config:ro \
26
+ $(IMAGE)
27
+
28
+ push:
29
+ @echo "Pushing __PROJECT_NAME__ Agent Docker image with tag: $(IMAGE)"
30
+ docker push $(IMAGE)
31
+ @echo "__PROJECT_NAME__ Agent Docker image pushed successfully."
32
+
33
+ clean:
34
+ docker rmi $(IMAGE) || true
@@ -0,0 +1 @@
1
+ # Example BAT Agent
@@ -0,0 +1,2 @@
1
+ def main() -> None:
2
+ print("BAT agent bootstrap")
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "__AGENT_NAME__",
3
+ "description": "",
4
+ "version": "1.0.0",
5
+ "capabilities": {
6
+ "streaming": true,
7
+ "pushNotifications": false
8
+ },
9
+ "defaultInputModes": ["text"],
10
+ "defaultOutputModes": ["text"],
11
+ "skills": []
12
+ }
@@ -0,0 +1,45 @@
1
+ # -*- mode: python ; coding: utf-8 -*-
2
+
3
+ datas = [
4
+ ('src', 'src'),
5
+ ]
6
+
7
+ a = Analysis(
8
+ ['__main__.py'],
9
+ pathex=[],
10
+ binaries=[],
11
+ datas=datas,
12
+ hiddenimports=[
13
+ 'br_rapp_sdk',
14
+ 'br_rapp_sdk.common',
15
+ ],
16
+ hookspath=[],
17
+ hooksconfig={},
18
+ runtime_hooks=[],
19
+ excludes=[],
20
+ noarchive=False,
21
+ optimize=0,
22
+ )
23
+
24
+ pyz = PYZ(a.pure)
25
+
26
+ exe = EXE(
27
+ pyz,
28
+ a.scripts,
29
+ a.binaries,
30
+ a.datas,
31
+ [],
32
+ name='__PROJECT_NAME__',
33
+ debug=False,
34
+ bootloader_ignore_signals=False,
35
+ strip=False,
36
+ upx=True,
37
+ upx_exclude=[],
38
+ runtime_tmpdir=None,
39
+ console=True,
40
+ disable_windowed_traceback=False,
41
+ argv_emulation=False,
42
+ target_arch=None,
43
+ codesign_identity=None,
44
+ entitlements_file=None,
45
+ )
@@ -0,0 +1 @@
1
+ checkpoints: false
@@ -0,0 +1,36 @@
1
+ from bat.chat_model_client import ChatModelClient, ChatModelClientConfig
2
+ from langchain_core.messages import HumanMessage
3
+
4
+
5
+ class __CLASS_NAME__(ChatModelClient):
6
+
7
+ SYSTEM_INSTRUCTIONS = (
8
+ "INSERT INSTRUCTIONS HERE"
9
+ )
10
+ USER_INSTRUCTIONS = (
11
+ "USER QUERY: {message}"
12
+ )
13
+
14
+ def __init__(
15
+ self,
16
+ tools,
17
+ ):
18
+ super().__init__(
19
+ system_instructions=self.SYSTEM_INSTRUCTIONS,
20
+ chat_model_config=ChatModelClientConfig.from_env(client_name="__CLASS_NAME__"),
21
+ tools=tools,
22
+ )
23
+
24
+ def invoke(
25
+ self,
26
+ query: str,
27
+ ) -> str:
28
+ """Format the query based on what the client needs to do."""
29
+ input_message = HumanMessage(
30
+ content=self.USER_INSTRUCTIONS.format(
31
+ message=query,
32
+ )
33
+ )
34
+ response = super().invoke(input_message)
35
+ response_content = response.content.strip()
36
+ return response_content
@@ -0,0 +1,9 @@
1
+ [project]
2
+ name = "__PROJECT_NAME__"
3
+ version = "1.0.0"
4
+ description = "__PROJECT_DESCRIPTION__"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "bat-adk>=__BAT_ADK_VERSION__"
9
+ ]
File without changes
@@ -0,0 +1,50 @@
1
+ from bat.agent import AgentGraph, AgentState, AgentTaskResult, AgentTaskStatus
2
+ from bat.prebuilt import ReActLoop
3
+ from langgraph.graph import START, END
4
+ from typing import Optional, Self
5
+ from typing_extensions import override
6
+
7
+ __CLIENT_IMPORTS__
8
+
9
+
10
+ class __AGENT_CLASS_NAME__AgentState(AgentState):
11
+ query: str
12
+ response: Optional[str] = None
13
+
14
+ @classmethod
15
+ @override
16
+ def from_query(
17
+ cls,
18
+ query: str,
19
+ ) -> Self:
20
+ return cls(query=query)
21
+
22
+ @override
23
+ def to_task_result(
24
+ self,
25
+ ) -> AgentTaskResult:
26
+ return AgentTaskResult(
27
+ task_status=(
28
+ AgentTaskStatus.AGENT_TASK_STATUS_COMPLETED
29
+ if self.response
30
+ else AgentTaskStatus.AGENT_TASK_STATUS_WORKING
31
+ ),
32
+ content=self.response or "Generating response...",
33
+ )
34
+
35
+
36
+ class __AGENT_CLASS_NAME__AgentGraph(AgentGraph):
37
+ @override
38
+ def setup(
39
+ self,
40
+ config,
41
+ ) -> None:
42
+ #Client setup
43
+ __CLIENT_SETUP__
44
+
45
+ # Graph wiring
46
+ self.graph_builder.add_edge(
47
+ START,
48
+ END,
49
+ )
50
+
File without changes
File without changes
eval/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Evaluation command package for bat-cli."""