bench2agent 0.3.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.
- bench2agent/__init__.py +3 -0
- bench2agent/__main__.py +3 -0
- bench2agent/cli.py +515 -0
- bench2agent/core/__init__.py +1 -0
- bench2agent/core/__main__.py +3 -0
- bench2agent/core/benchmarks.py +562 -0
- bench2agent/core/chat.py +632 -0
- bench2agent/core/cli.py +454 -0
- bench2agent/core/datasets.py +220 -0
- bench2agent/core/export.py +248 -0
- bench2agent/core/figures.py +253 -0
- bench2agent/core/mcp.py +1194 -0
- bench2agent/core/paths.py +30 -0
- bench2agent/core/policy.py +17 -0
- bench2agent/core/server.py +326 -0
- bench2agent/core/translate.py +149 -0
- bench2agent/core/verify.py +87 -0
- bench2agent/data.py +123 -0
- bench2agent/migration.py +58 -0
- bench2agent/providers.py +142 -0
- bench2agent/session.py +97 -0
- bench2agent-0.3.0.dist-info/METADATA +184 -0
- bench2agent-0.3.0.dist-info/RECORD +27 -0
- bench2agent-0.3.0.dist-info/WHEEL +5 -0
- bench2agent-0.3.0.dist-info/entry_points.txt +2 -0
- bench2agent-0.3.0.dist-info/licenses/LICENSE +202 -0
- bench2agent-0.3.0.dist-info/top_level.txt +1 -0
bench2agent/__init__.py
ADDED
bench2agent/__main__.py
ADDED
bench2agent/cli.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""Bench2Agent terminal conversations and a local MCP service."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import getpass
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shlex
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from . import __version__, providers
|
|
16
|
+
from .session import Session, conversations, write_json
|
|
17
|
+
|
|
18
|
+
HELP = "/new /chats /resume ID /sources /status /help /exit"
|
|
19
|
+
TOOL_LABELS = {"benchmark_scope": "Checking data coverage", "benchmark_usage": "Counting benchmark use",
|
|
20
|
+
"benchmark_trend": "Comparing editions", "new_benchmarks": "Finding introductions",
|
|
21
|
+
"benchmark_adoption": "Counting adoption", "benchmark_evidence": "Checking paper evidence"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def safe_text(text) -> str:
|
|
25
|
+
# Model text, paper titles and saved questions can contain terminal controls.
|
|
26
|
+
# Keep newlines/tabs and printable Unicode, remove escape/control sequences.
|
|
27
|
+
text = re.sub(r"\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))", "", str(text))
|
|
28
|
+
return "".join(c for c in text if c in "\n\t" or (c.isprintable() and c != "\x7f"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def render(turn: dict, *, sources: bool = False) -> str:
|
|
32
|
+
pieces, evidence = [], []
|
|
33
|
+
for seg in turn.get("segs", []):
|
|
34
|
+
if seg.get("t") == "p":
|
|
35
|
+
pieces.append(seg.get("s", ""))
|
|
36
|
+
elif seg.get("t") == "n":
|
|
37
|
+
mark = " [unverified figure]" if seg.get("v") in {"no", "na"} else ""
|
|
38
|
+
pieces.append(seg.get("s", "") + mark)
|
|
39
|
+
elif seg.get("t") == "c":
|
|
40
|
+
evidence.append(seg)
|
|
41
|
+
pieces.append(f"[{len(evidence)}]" + (" [unverified quote]" if not seg.get("v") else ""))
|
|
42
|
+
if sources:
|
|
43
|
+
for i, seg in enumerate(evidence, 1):
|
|
44
|
+
verdict = "✓" if seg.get("v") else "✗"
|
|
45
|
+
pieces.append(f"\n[{i}] {verdict} {seg.get('title', '')}\n {seg.get('url') or seg.get('paper_id') or seg.get('gid')}\n {seg.get('q', '')}\n")
|
|
46
|
+
else:
|
|
47
|
+
for i, seg in enumerate(evidence, 1):
|
|
48
|
+
pieces.append(f"\n[{i}] {seg.get('title', '')} · {seg.get('url') or seg.get('paper_id') or seg.get('gid')}")
|
|
49
|
+
checked = turn.get("verified") or {}
|
|
50
|
+
if checked.get("checked") or checked.get("fchecked"):
|
|
51
|
+
pieces.append(f"\n\nQuotes {checked.get('passed', 0)}/{checked.get('checked', 0)} · "
|
|
52
|
+
f"Figures {checked.get('fpassed', 0)}/{checked.get('fchecked', 0)}")
|
|
53
|
+
return safe_text("".join(pieces))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def settings_path(root: Path) -> Path:
|
|
57
|
+
return root / "data/bench2agent/settings.json"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def settings(root: Path) -> dict:
|
|
61
|
+
try:
|
|
62
|
+
with settings_path(root).open(encoding="utf-8") as source:
|
|
63
|
+
value = json.load(source)
|
|
64
|
+
if not isinstance(value, dict):
|
|
65
|
+
raise ValueError("Invalid settings. Run bench2agent init.")
|
|
66
|
+
return value
|
|
67
|
+
except FileNotFoundError:
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def configuration(args, root: Path, *, interactive: bool = False) -> dict:
|
|
72
|
+
saved = settings(root)
|
|
73
|
+
provider = args.provider or saved.get("provider")
|
|
74
|
+
if not provider:
|
|
75
|
+
provider = "anthropic" if os.environ.get("ANTHROPIC_API_KEY") and not os.environ.get("OPENAI_API_KEY") else "openai"
|
|
76
|
+
if interactive and not any(os.environ.get(name) for name in providers.KEY_NAMES.values()):
|
|
77
|
+
choice = input("Model provider [openai / anthropic]: ").strip().lower()
|
|
78
|
+
provider = choice or provider
|
|
79
|
+
if provider not in providers.KEY_NAMES:
|
|
80
|
+
raise ValueError("Choose --provider openai or anthropic.")
|
|
81
|
+
model = args.model or (saved.get("model") if saved.get("provider") == provider else None) or providers.DEFAULT_MODELS[provider]
|
|
82
|
+
language = args.language or saved.get("language", "auto")
|
|
83
|
+
return {"provider": provider, "model": model, "language": language}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def ensure_key(config: dict, *, interactive: bool):
|
|
87
|
+
name = providers.KEY_NAMES[config["provider"]]
|
|
88
|
+
if not os.environ.get(name):
|
|
89
|
+
if not interactive:
|
|
90
|
+
raise ValueError(f"Set {name} or start bench2agent interactively to enter a key for this session.")
|
|
91
|
+
key = getpass.getpass(name + " (this session only): ").strip()
|
|
92
|
+
if not key:
|
|
93
|
+
raise ValueError("API key is required.")
|
|
94
|
+
os.environ[name] = key
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def cmd_init(args, root: Path) -> int:
|
|
98
|
+
config = configuration(args, root, interactive=sys.stdin.isatty())
|
|
99
|
+
if sys.stdin.isatty() and not args.model:
|
|
100
|
+
config["model"] = input(f"Model [{config['model']}]: ").strip() or config["model"]
|
|
101
|
+
write_json(settings_path(root), config)
|
|
102
|
+
print(f"Saved {config['provider']} / {config['model']} · data: {root}")
|
|
103
|
+
print(f"Set {providers.KEY_NAMES[config['provider']]} in your environment, or enter it when bench2agent starts.")
|
|
104
|
+
print("Next: bench2agent")
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
CLIENT_LABELS = {"claude": "Claude Code", "codex": "Codex"}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def client_status(run=subprocess.run) -> dict:
|
|
112
|
+
"""Is Bench2Agent reachable through Claude Code and Codex?
|
|
113
|
+
|
|
114
|
+
Asked of the clients' own CLIs (`<client> mcp get bench2agent`), never read
|
|
115
|
+
from their config or credential files. `signed_in` comes from the same
|
|
116
|
+
login probes the browser settings use; None means the probe gave no answer.
|
|
117
|
+
"""
|
|
118
|
+
from bench2agent.core.chat import _agent_status
|
|
119
|
+
out = {}
|
|
120
|
+
for name in ("claude", "codex"):
|
|
121
|
+
info = _agent_status(name)
|
|
122
|
+
entry = {"installed": info["installed"], "signed_in": info["authenticated"],
|
|
123
|
+
"mcp_registered": None, "mcp_scope": None, "mcp_connected": None}
|
|
124
|
+
if info["installed"]:
|
|
125
|
+
try:
|
|
126
|
+
done = run([name, "mcp", "get", "bench2agent"], capture_output=True, text=True,
|
|
127
|
+
timeout=20, stdin=subprocess.DEVNULL)
|
|
128
|
+
entry["mcp_registered"] = done.returncode == 0
|
|
129
|
+
if name == "claude" and done.returncode == 0:
|
|
130
|
+
scope = re.search(r"Scope:\s*(\w+)", done.stdout or "")
|
|
131
|
+
state = re.search(r"Status:\s*\S*\s*(\w+)", done.stdout or "")
|
|
132
|
+
entry["mcp_scope"] = scope.group(1).lower() if scope else None
|
|
133
|
+
entry["mcp_connected"] = (state.group(1).lower() == "connected") if state else None
|
|
134
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
135
|
+
pass
|
|
136
|
+
out[name] = entry
|
|
137
|
+
return out
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _client_line(name: str, c: dict) -> str:
|
|
141
|
+
label = CLIENT_LABELS[name]
|
|
142
|
+
if not c["installed"]:
|
|
143
|
+
return f"{label}: not installed"
|
|
144
|
+
parts = ["installed",
|
|
145
|
+
{True: "signed in", False: "not signed in", None: "sign-in unknown"}[c["signed_in"]]]
|
|
146
|
+
if c["mcp_registered"]:
|
|
147
|
+
where = f" ({c['mcp_scope']} scope)" if c["mcp_scope"] else ""
|
|
148
|
+
parts.append("bench2agent MCP server registered" + where)
|
|
149
|
+
if c["mcp_connected"] is True:
|
|
150
|
+
parts.append("connected")
|
|
151
|
+
elif c["mcp_connected"] is False:
|
|
152
|
+
parts.append("NOT connecting — check `bench2agent data status` and the registered command")
|
|
153
|
+
elif c["mcp_registered"] is False:
|
|
154
|
+
parts.append(f"bench2agent MCP server not registered → run: bench2agent mcp --connect {name}")
|
|
155
|
+
else:
|
|
156
|
+
parts.append("MCP registration unknown")
|
|
157
|
+
return f"{label}: " + " · ".join(parts)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def cmd_status(args, root: Path) -> int:
|
|
161
|
+
from .data import status
|
|
162
|
+
result = {"version": __version__, "home": str(root), "data": status(root),
|
|
163
|
+
"settings": settings(root), "api_keys_present": {
|
|
164
|
+
provider: bool(os.environ.get(name)) for provider, name in providers.KEY_NAMES.items()},
|
|
165
|
+
"clients": client_status()}
|
|
166
|
+
if args.json:
|
|
167
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
168
|
+
else:
|
|
169
|
+
print(f"Bench2Agent {__version__} · {root}")
|
|
170
|
+
print(json.dumps(result["data"], ensure_ascii=False, indent=2))
|
|
171
|
+
keys = ", ".join(f"{p}: {'set' if v else 'not set'}" for p, v in result["api_keys_present"].items())
|
|
172
|
+
print(f"Standalone conversation (`bench2agent`): needs an API key — {keys}")
|
|
173
|
+
print("Through an AI client (no API key; uses the client's own login):")
|
|
174
|
+
for name, c in result["clients"].items():
|
|
175
|
+
print(" " + _client_line(name, c))
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def cmd_data(args, root: Path) -> int:
|
|
180
|
+
from . import data
|
|
181
|
+
if args.action == "status":
|
|
182
|
+
result = data.status(root)
|
|
183
|
+
elif args.action == "install":
|
|
184
|
+
result = data.install(root, file=args.file, url=args.url, sha256=args.sha256)
|
|
185
|
+
else:
|
|
186
|
+
result = data.bundle(root, Path(args.out))
|
|
187
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def cmd_migrate(args, root: Path) -> int:
|
|
192
|
+
from .migration import migrate
|
|
193
|
+
print(json.dumps(migrate(root, Path(args.source)), ensure_ascii=False, indent=2))
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def print_chats(root: Path):
|
|
198
|
+
rows = conversations(root)
|
|
199
|
+
for row in rows:
|
|
200
|
+
print(safe_text(f"{row['id']} {row['provider']} {row['title']}"))
|
|
201
|
+
if not rows:
|
|
202
|
+
print("No saved conversations.")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def load_session(args, root: Path, *, interactive: bool) -> Session:
|
|
206
|
+
cid = args.resume
|
|
207
|
+
if cid == "latest":
|
|
208
|
+
rows = conversations(root)
|
|
209
|
+
if not rows:
|
|
210
|
+
raise ValueError("No saved conversations to resume.")
|
|
211
|
+
cid = rows[0]["id"]
|
|
212
|
+
config = configuration(args, root, interactive=interactive and not cid)
|
|
213
|
+
if interactive and not cid and not settings(root):
|
|
214
|
+
if not args.model:
|
|
215
|
+
config["model"] = input(f"Model [{config['model']}]: ").strip() or config["model"]
|
|
216
|
+
write_json(settings_path(root), config)
|
|
217
|
+
session = Session(root, **config, cid=cid)
|
|
218
|
+
if cid and ((args.provider and args.provider != session.doc["provider"]) or
|
|
219
|
+
(args.model and args.model != session.doc["model"])):
|
|
220
|
+
raise ValueError("A resumed conversation keeps its provider and model. Start a new conversation to change them.")
|
|
221
|
+
return session
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def require_data(root: Path, *, interactive: bool):
|
|
225
|
+
if (root / "data/processed/benchmark_snapshot.json").is_file():
|
|
226
|
+
return
|
|
227
|
+
if interactive:
|
|
228
|
+
from .data import install
|
|
229
|
+
print("Downloading benchmark data (16 MB); checking its checksum…", file=sys.stderr, flush=True)
|
|
230
|
+
install(root)
|
|
231
|
+
return
|
|
232
|
+
raise ValueError("Benchmark data is missing. Run bench2agent data install.")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def cmd_chat(args, root: Path) -> int:
|
|
236
|
+
interactive = sys.stdin.isatty() and not getattr(args, "question", None)
|
|
237
|
+
require_data(root, interactive=interactive)
|
|
238
|
+
session = load_session(args, root, interactive=interactive)
|
|
239
|
+
ensure_key(session.doc, interactive=interactive)
|
|
240
|
+
last = session.doc["turns"][-1] if session.doc["turns"] else None
|
|
241
|
+
|
|
242
|
+
def emit(event):
|
|
243
|
+
if event["t"] == "tool":
|
|
244
|
+
print(safe_text(" " + TOOL_LABELS.get(event["name"], event["name"])), file=sys.stderr, flush=True)
|
|
245
|
+
|
|
246
|
+
def answer(question):
|
|
247
|
+
nonlocal last
|
|
248
|
+
last = session.ask(question, emit)
|
|
249
|
+
if args.json:
|
|
250
|
+
print(json.dumps({"chat": session.doc["id"], **last}, ensure_ascii=False))
|
|
251
|
+
else:
|
|
252
|
+
print("\n" + render(last) + "\n", flush=True)
|
|
253
|
+
|
|
254
|
+
if getattr(args, "question", None):
|
|
255
|
+
answer(args.question)
|
|
256
|
+
return 0
|
|
257
|
+
if not args.json:
|
|
258
|
+
from bench2agent.core.mcp import B
|
|
259
|
+
snapshot = B.data()
|
|
260
|
+
editions = ", ".join(sorted(snapshot["editions"]))
|
|
261
|
+
print(f"Bench2Agent · {session.doc['provider']} / {session.doc['model']} · {session.doc['id']}")
|
|
262
|
+
print(safe_text("Data: " + editions))
|
|
263
|
+
print(HELP + "\n")
|
|
264
|
+
if last:
|
|
265
|
+
print(render(last) + "\n")
|
|
266
|
+
if interactive:
|
|
267
|
+
try:
|
|
268
|
+
import readline # optional on Windows; do not save questions outside our chat store
|
|
269
|
+
except ImportError:
|
|
270
|
+
pass
|
|
271
|
+
failed = False
|
|
272
|
+
while True:
|
|
273
|
+
try:
|
|
274
|
+
question = input("> " if interactive else "").strip()
|
|
275
|
+
except EOFError:
|
|
276
|
+
break
|
|
277
|
+
except KeyboardInterrupt:
|
|
278
|
+
if interactive:
|
|
279
|
+
print("\n" + HELP)
|
|
280
|
+
continue
|
|
281
|
+
return 130
|
|
282
|
+
if not question:
|
|
283
|
+
continue
|
|
284
|
+
if question in {"/exit", "/quit"}:
|
|
285
|
+
break
|
|
286
|
+
if question == "/help":
|
|
287
|
+
print(HELP)
|
|
288
|
+
continue
|
|
289
|
+
if question == "/chats":
|
|
290
|
+
print_chats(root)
|
|
291
|
+
continue
|
|
292
|
+
if question == "/status":
|
|
293
|
+
cmd_status(args, root)
|
|
294
|
+
continue
|
|
295
|
+
if question == "/sources":
|
|
296
|
+
print(render(last, sources=True) if last else "No answer yet.")
|
|
297
|
+
continue
|
|
298
|
+
if question == "/new":
|
|
299
|
+
session = Session(root, session.doc["provider"], session.doc["model"], language=session.doc.get("language", "auto"))
|
|
300
|
+
last = None
|
|
301
|
+
print("New conversation · " + session.doc["id"])
|
|
302
|
+
continue
|
|
303
|
+
try:
|
|
304
|
+
if question.startswith("/resume "):
|
|
305
|
+
cid = question.split(maxsplit=1)[1]
|
|
306
|
+
candidate = Session(root, session.doc["provider"], session.doc["model"], cid=cid)
|
|
307
|
+
ensure_key(candidate.doc, interactive=interactive)
|
|
308
|
+
session = candidate
|
|
309
|
+
last = session.doc["turns"][-1] if session.doc["turns"] else None
|
|
310
|
+
print("Resumed · " + session.doc["id"])
|
|
311
|
+
if last:
|
|
312
|
+
print(render(last))
|
|
313
|
+
elif question.startswith("/"):
|
|
314
|
+
print(HELP)
|
|
315
|
+
else:
|
|
316
|
+
answer(question)
|
|
317
|
+
except KeyboardInterrupt:
|
|
318
|
+
print("\nCancelled; the unfinished turn was not saved.", file=sys.stderr)
|
|
319
|
+
if not interactive:
|
|
320
|
+
return 130
|
|
321
|
+
except (ValueError, OSError, providers.ProviderError) as exc:
|
|
322
|
+
failed = True
|
|
323
|
+
print(safe_text(str(exc)), file=sys.stderr)
|
|
324
|
+
if not interactive:
|
|
325
|
+
break
|
|
326
|
+
if not args.json and session.doc["turns"]:
|
|
327
|
+
print("Continue: bench2agent --resume " + session.doc["id"])
|
|
328
|
+
return 1 if failed else 0
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def mcp_command(root: Path) -> list[str]:
|
|
332
|
+
# An absolute Python/module command survives venv installations and does
|
|
333
|
+
# not depend on bench2agent being discoverable in the MCP host's PATH.
|
|
334
|
+
return [sys.executable, "-m", "bench2agent", "--home", str(root), "mcp"]
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def client_command(client: str, root: Path, *, model: str | None = None) -> list[str]:
|
|
338
|
+
executable = shutil.which(client)
|
|
339
|
+
if not executable:
|
|
340
|
+
raise ValueError(f"{CLIENT_LABELS[client]} is not installed or is not on PATH. Install it first, then run bench2agent {client}.")
|
|
341
|
+
from bench2agent.core.paths import agent_environment
|
|
342
|
+
env = {**agent_environment(), "BENCH2AGENT_HOME": str(root)}
|
|
343
|
+
command = mcp_command(root)
|
|
344
|
+
if client == "claude":
|
|
345
|
+
config = {"mcpServers": {"bench2agent": {"command": command[0], "args": command[1:], "env": env}}}
|
|
346
|
+
native = [executable, "--mcp-config", json.dumps(config)]
|
|
347
|
+
if model:
|
|
348
|
+
native += ["--model", model]
|
|
349
|
+
return native
|
|
350
|
+
# Codex's -c values are TOML; JSON-quoted strings and arrays also fit TOML.
|
|
351
|
+
settings = {"command": json.dumps(command[0]), "args": json.dumps(command[1:]),
|
|
352
|
+
"env": "{" + ", ".join(json.dumps(k) + "=" + json.dumps(v) for k, v in env.items()) + "}"}
|
|
353
|
+
native = [executable]
|
|
354
|
+
for key, value in settings.items():
|
|
355
|
+
native += ["-c", f"mcp_servers.bench2agent.{key}={value}"]
|
|
356
|
+
native += ["--model", model or providers.DEFAULT_MODELS["openai"]]
|
|
357
|
+
return native
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def cmd_client(args, root: Path) -> int:
|
|
361
|
+
command = client_command(args.command, root, model=getattr(args, "model", None))
|
|
362
|
+
if args.dry_run:
|
|
363
|
+
print(shlex.join(command))
|
|
364
|
+
return 0
|
|
365
|
+
require_data(root, interactive=True)
|
|
366
|
+
print(f"Opening {CLIENT_LABELS[args.command]} with Bench2Agent…", file=sys.stderr, flush=True)
|
|
367
|
+
return subprocess.run(command, check=False).returncode
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def cmd_mcp(args, root: Path) -> int:
|
|
371
|
+
command = mcp_command(root)
|
|
372
|
+
if args.config:
|
|
373
|
+
from bench2agent.core.paths import agent_environment
|
|
374
|
+
print(json.dumps({"mcpServers": {"bench2agent": {"command": command[0], "args": command[1:],
|
|
375
|
+
"env": agent_environment()}}}, indent=2))
|
|
376
|
+
return 0
|
|
377
|
+
if args.connect:
|
|
378
|
+
client = shutil.which(args.connect)
|
|
379
|
+
if not client:
|
|
380
|
+
raise ValueError(f"{args.connect} is not installed or is not on PATH.")
|
|
381
|
+
from bench2agent.core.paths import agent_environment
|
|
382
|
+
env = agent_environment()
|
|
383
|
+
if args.connect == "codex":
|
|
384
|
+
native = [client, "mcp", "add", "bench2agent"]
|
|
385
|
+
for key, value in env.items():
|
|
386
|
+
native += ["--env", key + "=" + value]
|
|
387
|
+
else:
|
|
388
|
+
native = [client, "mcp", "add"]
|
|
389
|
+
for key, value in env.items():
|
|
390
|
+
native += ["--env", key + "=" + value]
|
|
391
|
+
# Claude's --env is variadic; another option must terminate it
|
|
392
|
+
# before the positional server name.
|
|
393
|
+
native += ["--transport", "stdio", "--scope", "user", "bench2agent"]
|
|
394
|
+
native += ["--", *command]
|
|
395
|
+
if args.dry_run:
|
|
396
|
+
print(shlex.join(native))
|
|
397
|
+
return 0
|
|
398
|
+
result = subprocess.run(native, check=False)
|
|
399
|
+
if not result.returncode:
|
|
400
|
+
print(f"Connected Bench2Agent to {args.connect}. Start a new {args.connect} session to use it.")
|
|
401
|
+
return result.returncode
|
|
402
|
+
if args.dry_run:
|
|
403
|
+
raise ValueError("--dry-run requires --connect codex or --connect claude.")
|
|
404
|
+
from bench2agent.core.mcp import _benchmark_tools, serve_stdio
|
|
405
|
+
return serve_stdio(tools=_benchmark_tools(), name="bench2agent")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def common_options(parser, *, child=False):
|
|
409
|
+
default = argparse.SUPPRESS if child else None
|
|
410
|
+
parser.add_argument("--home", default=default, help="runtime data directory (or BENCH2AGENT_HOME)")
|
|
411
|
+
parser.add_argument("--provider", choices=list(providers.KEY_NAMES), default=default)
|
|
412
|
+
parser.add_argument("--model", default=default, help="model ID supported by your account")
|
|
413
|
+
parser.add_argument("--language", choices=["auto", "ko", "en"], default=default)
|
|
414
|
+
parser.add_argument("--resume", nargs="?", const="latest", default=default, metavar="ID")
|
|
415
|
+
parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS if child else False)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def cmd_service(args, root: Path) -> int:
|
|
419
|
+
"""Use the same service implementation from the public terminal command."""
|
|
420
|
+
from .core import cli as service
|
|
421
|
+
handlers = {"serve": service.cmd_serve, "setup": service.cmd_setup,
|
|
422
|
+
"bundle": service.cmd_bundle, "fetch-data": service.cmd_fetch,
|
|
423
|
+
"datasets": service.cmd_datasets, "benchmarks": service.cmd_benchmarks}
|
|
424
|
+
return handlers[args.command](args)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def main(argv=None) -> int:
|
|
428
|
+
parser = argparse.ArgumentParser(prog="bench2agent", description=__doc__)
|
|
429
|
+
parser.add_argument("--version", action="version", version="Bench2Agent " + __version__)
|
|
430
|
+
common_options(parser)
|
|
431
|
+
commands = parser.add_subparsers(dest="command")
|
|
432
|
+
chat = commands.add_parser("chat", help="start a conversation (also the default)")
|
|
433
|
+
common_options(chat, child=True)
|
|
434
|
+
chat.set_defaults(fn=cmd_chat)
|
|
435
|
+
ask = commands.add_parser("ask", help="answer one question")
|
|
436
|
+
ask.add_argument("question")
|
|
437
|
+
common_options(ask, child=True)
|
|
438
|
+
ask.set_defaults(fn=cmd_chat)
|
|
439
|
+
init = commands.add_parser("init", help="choose model provider and model")
|
|
440
|
+
common_options(init, child=True)
|
|
441
|
+
init.set_defaults(fn=cmd_init)
|
|
442
|
+
status = commands.add_parser("status", help="data and model connection status")
|
|
443
|
+
common_options(status, child=True)
|
|
444
|
+
status.set_defaults(fn=cmd_status)
|
|
445
|
+
chats = commands.add_parser("chats", help="list saved conversations")
|
|
446
|
+
common_options(chats, child=True)
|
|
447
|
+
chats.set_defaults(fn=lambda args, root: print_chats(root) or 0)
|
|
448
|
+
migration = commands.add_parser("migrate", help="copy data and saved chats from a previous installation")
|
|
449
|
+
common_options(migration, child=True)
|
|
450
|
+
migration.add_argument("--from", dest="source", required=True, metavar="DIR", help="previous runtime data directory")
|
|
451
|
+
migration.set_defaults(fn=cmd_migrate)
|
|
452
|
+
data = commands.add_parser("data", help="install, inspect or bundle benchmark data")
|
|
453
|
+
common_options(data, child=True)
|
|
454
|
+
actions = data.add_subparsers(dest="action", required=True)
|
|
455
|
+
data_status = actions.add_parser("status")
|
|
456
|
+
common_options(data_status, child=True)
|
|
457
|
+
install = actions.add_parser("install")
|
|
458
|
+
common_options(install, child=True)
|
|
459
|
+
source = install.add_mutually_exclusive_group()
|
|
460
|
+
source.add_argument("--file")
|
|
461
|
+
source.add_argument("--url")
|
|
462
|
+
install.add_argument("--sha256", help="expected SHA-256 of the downloaded file")
|
|
463
|
+
bundle = actions.add_parser("bundle")
|
|
464
|
+
common_options(bundle, child=True)
|
|
465
|
+
bundle.add_argument("--out", required=True)
|
|
466
|
+
data.set_defaults(fn=cmd_data)
|
|
467
|
+
mcp = commands.add_parser("mcp", help="serve read-only tools or connect an existing AI client")
|
|
468
|
+
common_options(mcp, child=True)
|
|
469
|
+
mode = mcp.add_mutually_exclusive_group()
|
|
470
|
+
mode.add_argument("--config", action="store_true", help="print MCP configuration for other clients")
|
|
471
|
+
mode.add_argument("--connect", choices=["codex", "claude"], help="register via the client's own CLI")
|
|
472
|
+
mcp.add_argument("--dry-run", action="store_true", help="show registration command without changing settings")
|
|
473
|
+
mcp.set_defaults(fn=cmd_mcp)
|
|
474
|
+
for name in ("claude", "codex"):
|
|
475
|
+
client = commands.add_parser(name, help=f"open {CLIENT_LABELS[name]} with Bench2Agent tools")
|
|
476
|
+
common_options(client, child=True)
|
|
477
|
+
client.add_argument("--dry-run", action="store_true", help="show the launch command without downloading data or starting the client")
|
|
478
|
+
client.set_defaults(fn=cmd_client)
|
|
479
|
+
serve = commands.add_parser("serve", help="start the local web interface")
|
|
480
|
+
common_options(serve, child=True)
|
|
481
|
+
serve.add_argument("--port", type=int, default=8001)
|
|
482
|
+
serve.add_argument("--no-meili", action="store_true", help="do not start the optional search engine")
|
|
483
|
+
serve.set_defaults(fn=cmd_service)
|
|
484
|
+
for name, description in (("setup", "install the optional browser search engine"),
|
|
485
|
+
("bundle", "bundle the full browser data on the build machine")):
|
|
486
|
+
service = commands.add_parser(name, help=description)
|
|
487
|
+
common_options(service, child=True)
|
|
488
|
+
service.set_defaults(fn=cmd_service)
|
|
489
|
+
fetch = commands.add_parser("fetch-data", help="install a full browser data bundle")
|
|
490
|
+
common_options(fetch, child=True)
|
|
491
|
+
source = fetch.add_mutually_exclusive_group()
|
|
492
|
+
source.add_argument("--file")
|
|
493
|
+
source.add_argument("--url")
|
|
494
|
+
source.add_argument("--release", metavar="TAG")
|
|
495
|
+
fetch.set_defaults(fn=cmd_service)
|
|
496
|
+
datasets = commands.add_parser("datasets", help="export paper and dataset source inventories")
|
|
497
|
+
common_options(datasets, child=True)
|
|
498
|
+
datasets.add_argument("--out", default=".")
|
|
499
|
+
datasets.add_argument("--gid", type=int)
|
|
500
|
+
datasets.set_defaults(fn=cmd_service)
|
|
501
|
+
benchmarks = commands.add_parser("benchmarks", help="build the benchmark snapshot from reviewed extraction")
|
|
502
|
+
common_options(benchmarks, child=True)
|
|
503
|
+
benchmarks.add_argument("--out")
|
|
504
|
+
benchmarks.set_defaults(fn=cmd_service)
|
|
505
|
+
args = parser.parse_args(argv)
|
|
506
|
+
if args.home:
|
|
507
|
+
os.environ["BENCH2AGENT_HOME"] = args.home
|
|
508
|
+
from bench2agent.core.paths import ROOT
|
|
509
|
+
try:
|
|
510
|
+
return getattr(args, "fn", cmd_chat)(args, ROOT)
|
|
511
|
+
except KeyboardInterrupt:
|
|
512
|
+
return 130
|
|
513
|
+
except (ValueError, OSError, providers.ProviderError) as exc:
|
|
514
|
+
print(safe_text(str(exc)), file=sys.stderr)
|
|
515
|
+
return 1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared benchmark evidence, deterministic tools and the local web service."""
|