aidong-aeval 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.
- aeval_client/__init__.py +1 -0
- aeval_client/agent.py +57 -0
- aeval_client/cli.py +222 -0
- aeval_client/config.py +72 -0
- aidong_aeval-0.1.0.dist-info/METADATA +56 -0
- aidong_aeval-0.1.0.dist-info/RECORD +8 -0
- aidong_aeval-0.1.0.dist-info/WHEEL +4 -0
- aidong_aeval-0.1.0.dist-info/entry_points.txt +2 -0
aeval_client/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""开发者评测客户端,不依赖数据库或 Langfuse SDK。"""
|
aeval_client/agent.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""main/v1 的 HTTP 文本执行;过程观测交给 Agent 内置 Langfuse。"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def text_content(message):
|
|
8
|
+
value = (message.get("kwargs") or message).get("content")
|
|
9
|
+
if isinstance(value, str):
|
|
10
|
+
return value
|
|
11
|
+
if isinstance(value, list):
|
|
12
|
+
return "\n".join(b["text"] for b in value if isinstance(b, dict) and b.get("type") == "text")
|
|
13
|
+
raise ValueError("最终回答不是文本")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Agent:
|
|
17
|
+
def __init__(self, target, protocol, user_id, effort=None, token=None):
|
|
18
|
+
self.target, self.protocol, self.effort = target, protocol, effort
|
|
19
|
+
headers = {"X-User-Id": user_id}
|
|
20
|
+
if token:
|
|
21
|
+
headers["Authorization"] = "Bearer " + token
|
|
22
|
+
self.client = httpx.Client(headers=headers, timeout=httpx.Timeout(900, connect=10), follow_redirects=False)
|
|
23
|
+
|
|
24
|
+
def close(self):
|
|
25
|
+
self.client.close()
|
|
26
|
+
|
|
27
|
+
def validate(self):
|
|
28
|
+
if self.effort is None:
|
|
29
|
+
return
|
|
30
|
+
if self.protocol != "v1":
|
|
31
|
+
raise ValueError("main 不支持请求级 effort")
|
|
32
|
+
response = self.client.get(self.target + "/api/agent/model")
|
|
33
|
+
response.raise_for_status()
|
|
34
|
+
levels = response.json()["thinking"]["levels"]
|
|
35
|
+
if self.effort not in levels:
|
|
36
|
+
raise ValueError(f"不支持思考档位 {self.effort},可选:{', '.join(levels)}")
|
|
37
|
+
|
|
38
|
+
def ask(self, question, thread_id, history=None):
|
|
39
|
+
body = {"question": question, "configurable": {"thread_id": thread_id}}
|
|
40
|
+
if history:
|
|
41
|
+
body["chat_history"] = history
|
|
42
|
+
if self.effort:
|
|
43
|
+
body["effort"] = self.effort
|
|
44
|
+
answer = None
|
|
45
|
+
with self.client.stream("POST", self.target + "/api/agent/langgraph", json=body) as response:
|
|
46
|
+
response.raise_for_status()
|
|
47
|
+
for line in response.iter_lines():
|
|
48
|
+
if not line.startswith("data: "):
|
|
49
|
+
continue
|
|
50
|
+
event = json.loads(line[6:])
|
|
51
|
+
if event.get("status") == "error":
|
|
52
|
+
raise ValueError("Agent 返回错误事件,请检查 Agent 日志")
|
|
53
|
+
if event.get("event") == "on_chain_end" and event.get("name") == "LangGraph":
|
|
54
|
+
answer = text_content(event["data"]["output"]["messages"][-1])
|
|
55
|
+
if answer is None:
|
|
56
|
+
raise ValueError("响应缺少最终回答,调用是否完成需要核对 trace")
|
|
57
|
+
return answer
|
aeval_client/cli.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""开发机评测入口,恢复时以中心服务的固定清单为准。"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from aeval_client import config
|
|
12
|
+
from aeval_client.agent import Agent
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ApiError(ValueError):
|
|
16
|
+
def __init__(self, status, detail):
|
|
17
|
+
self.status = status
|
|
18
|
+
super().__init__(detail)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Service:
|
|
22
|
+
def __init__(self, cfg):
|
|
23
|
+
self.base = cfg["server"]
|
|
24
|
+
self.client = httpx.Client(headers={"X-Eval-Token": cfg["api_key"]},
|
|
25
|
+
timeout=180, follow_redirects=False)
|
|
26
|
+
|
|
27
|
+
def call(self, method, path, **kwargs):
|
|
28
|
+
response = self.client.request(method, self.base + "/api/client" + path, **kwargs)
|
|
29
|
+
if not response.is_success:
|
|
30
|
+
try:
|
|
31
|
+
detail = response.json().get("detail")
|
|
32
|
+
except ValueError:
|
|
33
|
+
detail = None
|
|
34
|
+
raise ApiError(response.status_code, detail if isinstance(detail, str) else f"评测服务返回 HTTP {response.status_code}")
|
|
35
|
+
return response.json()
|
|
36
|
+
|
|
37
|
+
def close(self):
|
|
38
|
+
self.client.close()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def save(path, value):
|
|
42
|
+
config.private_write(path, json.dumps(value, ensure_ascii=False))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parser():
|
|
46
|
+
p = argparse.ArgumentParser(description="在开发机执行 OmniAgent 评测")
|
|
47
|
+
p.add_argument("command", nargs="?", choices=["configure", "datasets", "status", "results", "resume"])
|
|
48
|
+
p.add_argument("run_id", nargs="?")
|
|
49
|
+
p.add_argument("--target")
|
|
50
|
+
p.add_argument("--dataset")
|
|
51
|
+
p.add_argument("--customer")
|
|
52
|
+
p.add_argument("--protocol", choices=["main", "v1"])
|
|
53
|
+
p.add_argument("--revision")
|
|
54
|
+
p.add_argument("--model")
|
|
55
|
+
p.add_argument("--effort")
|
|
56
|
+
p.add_argument("--environment")
|
|
57
|
+
p.add_argument("--k", type=int, default=1)
|
|
58
|
+
p.add_argument("--retry-uncertain", action="store_true", help="恢复失败后显式重问未知任务,可能产生重复调用")
|
|
59
|
+
return p
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def summary(service, run_id):
|
|
63
|
+
result = service.call("GET", f"/runs/{run_id}/report")
|
|
64
|
+
print(json.dumps({"运行": run_id, "状态": result["run"]["status"],
|
|
65
|
+
"结果": result["report"]["total"], "指标": result["metrics"]}, ensure_ascii=False, indent=2))
|
|
66
|
+
print(f"报告:{service.base}/runs/{run_id}")
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def execute(service, cfg, run_id, retry=False):
|
|
71
|
+
prefix = f"/runs/{run_id}"
|
|
72
|
+
state = service.call("GET", prefix)
|
|
73
|
+
spec = state["spec"]
|
|
74
|
+
cache = config.HOME / "runs" / run_id
|
|
75
|
+
agent = Agent(spec["target"], spec["protocol"], state["user_id"], spec["effort"], cfg.get("agent_token"))
|
|
76
|
+
stopped = threading.Event()
|
|
77
|
+
|
|
78
|
+
def heartbeats():
|
|
79
|
+
while not stopped.wait(15):
|
|
80
|
+
try:
|
|
81
|
+
service.call("POST", prefix + "/heartbeat")
|
|
82
|
+
except (httpx.HTTPError, ValueError):
|
|
83
|
+
pass # 主请求报告连接错误;心跳失败不能伪造成功。
|
|
84
|
+
|
|
85
|
+
pulse = threading.Thread(target=heartbeats, daemon=True)
|
|
86
|
+
pulse.start()
|
|
87
|
+
try:
|
|
88
|
+
service.call("POST", prefix + "/heartbeat")
|
|
89
|
+
# 上次回答已写本地但上传未确认时,先原样重传。
|
|
90
|
+
for pending in sorted(cache.glob("*.json")):
|
|
91
|
+
service.call("PUT", prefix + f"/tasks/{pending.stem}", json=json.loads(pending.read_text()))
|
|
92
|
+
pending.unlink()
|
|
93
|
+
state = service.call("GET", prefix)
|
|
94
|
+
for task in state["tasks"]:
|
|
95
|
+
if task["state"] != "issued":
|
|
96
|
+
continue
|
|
97
|
+
try:
|
|
98
|
+
service.call("POST", prefix + f"/tasks/{task['id']}/recover")
|
|
99
|
+
except ValueError:
|
|
100
|
+
if not retry:
|
|
101
|
+
raise ValueError("存在未知调用,已尝试从 trace 恢复。稍后 resume;确认可重问时加 --retry-uncertain")
|
|
102
|
+
service.call("POST", prefix + f"/tasks/{task['id']}/retry")
|
|
103
|
+
state = service.call("GET", prefix)
|
|
104
|
+
if not state["approved"] or any(task["state"] == "pending" for task in state["tasks"]):
|
|
105
|
+
agent.validate()
|
|
106
|
+
if not state["approved"]:
|
|
107
|
+
marker = cache / "probe"
|
|
108
|
+
# 探针也可能在响应丢失前执行完成;同一会话不自动再问。
|
|
109
|
+
if not marker.exists() or marker.read_text() != state["probe_thread"]:
|
|
110
|
+
config.private_write(marker, state["probe_thread"])
|
|
111
|
+
agent.ask("你好,请只回复“收到”两个字", state["probe_thread"])
|
|
112
|
+
result = service.call("POST", prefix + "/preflight")
|
|
113
|
+
print(f"安全检查通过,观测环境:{result['environment']}", flush=True)
|
|
114
|
+
tasks = service.call("GET", prefix)["tasks"]
|
|
115
|
+
for index, task in enumerate(tasks):
|
|
116
|
+
if task["state"] != "pending":
|
|
117
|
+
continue
|
|
118
|
+
q = service.call("POST", prefix + f"/tasks/{task['id']}/claim")
|
|
119
|
+
start = time.monotonic()
|
|
120
|
+
# 断流或超时留下 issued;不把未知调用误报为可安全重试的失败。
|
|
121
|
+
answer = agent.ask(q["question"], q["thread_id"], q.get("chat_history"))
|
|
122
|
+
payload = {"thread_id": q["thread_id"], "answer": answer, "elapsed_s": time.monotonic() - start}
|
|
123
|
+
path = cache / f"{task['id']}.json"
|
|
124
|
+
save(path, payload)
|
|
125
|
+
service.call("PUT", prefix + f"/tasks/{task['id']}", json=payload)
|
|
126
|
+
path.unlink()
|
|
127
|
+
print(f"回答进度:{index + 1}/{len(tasks)}", flush=True)
|
|
128
|
+
service.call("POST", prefix + "/finish")
|
|
129
|
+
last = None
|
|
130
|
+
while True:
|
|
131
|
+
state = service.call("GET", prefix)
|
|
132
|
+
done = sum(t["state"] in ("done", "failed") for t in state["tasks"])
|
|
133
|
+
progress = (done, state["status"])
|
|
134
|
+
if progress != last:
|
|
135
|
+
print(f"判定进度:{done}/{len(state['tasks'])} · {state['status']}", flush=True)
|
|
136
|
+
last = progress
|
|
137
|
+
if state["status"] not in ("running", "judging"):
|
|
138
|
+
break
|
|
139
|
+
time.sleep(3)
|
|
140
|
+
summary(service, run_id)
|
|
141
|
+
(cache / "probe").unlink(missing_ok=True)
|
|
142
|
+
if cache.exists() and not any(cache.iterdir()):
|
|
143
|
+
cache.rmdir()
|
|
144
|
+
return 0 if state["status"] == "done" else 2
|
|
145
|
+
finally:
|
|
146
|
+
stopped.set()
|
|
147
|
+
pulse.join(timeout=1)
|
|
148
|
+
agent.close()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def main(argv=None):
|
|
152
|
+
args = parser().parse_args(argv)
|
|
153
|
+
service = None
|
|
154
|
+
run_id = args.run_id
|
|
155
|
+
try:
|
|
156
|
+
if args.command == "configure":
|
|
157
|
+
config.configure()
|
|
158
|
+
return 0
|
|
159
|
+
cfg = config.read()
|
|
160
|
+
service = Service(cfg)
|
|
161
|
+
if args.command == "datasets":
|
|
162
|
+
result = service.call("GET", "/datasets", params={"customer": args.customer or cfg["customer"], "refresh": "true"})
|
|
163
|
+
for item in result["items"]:
|
|
164
|
+
print(f"{item['id']}\t{item['name']}\t{item['count']} 题")
|
|
165
|
+
return 0
|
|
166
|
+
if args.command in ("status", "results", "resume"):
|
|
167
|
+
if not run_id:
|
|
168
|
+
raise ValueError("请指定运行编号")
|
|
169
|
+
if args.command == "status":
|
|
170
|
+
state = service.call("GET", f"/runs/{run_id}")
|
|
171
|
+
print(json.dumps({k: state[k] for k in ("run_id", "status", "environment", "disconnected", "tasks")},
|
|
172
|
+
ensure_ascii=False, indent=2))
|
|
173
|
+
return 0
|
|
174
|
+
if args.command == "results":
|
|
175
|
+
summary(service, run_id)
|
|
176
|
+
return 0
|
|
177
|
+
if any(getattr(args, k) is not None for k in ("target", "dataset", "customer", "protocol", "revision", "model", "effort", "environment")):
|
|
178
|
+
raise ValueError("恢复不能更换原运行参数,请新建评测")
|
|
179
|
+
service.call("POST", f"/runs/{run_id}/resume")
|
|
180
|
+
return execute(service, cfg, run_id, args.retry_uncertain)
|
|
181
|
+
if not args.target:
|
|
182
|
+
raise ValueError("请指定 --target")
|
|
183
|
+
dataset = args.dataset or cfg.get("dataset")
|
|
184
|
+
if not dataset:
|
|
185
|
+
raise ValueError("请先运行 aeval datasets,再用 --dataset 选择 Langfuse 数据集")
|
|
186
|
+
spec = {"customer": args.customer or cfg["customer"], "dataset": dataset,
|
|
187
|
+
"target": config.url(args.target), "protocol": args.protocol or cfg["protocol"],
|
|
188
|
+
"revision": args.revision, "effort": args.effort,
|
|
189
|
+
"model": args.model, "environment": args.environment or cfg.get("environment"), "k": args.k}
|
|
190
|
+
# 响应丢失后的同参数重试复用请求 UUID;成功取得 run_id 后即清除。
|
|
191
|
+
pending = config.HOME / "pending-create.json"
|
|
192
|
+
if pending.exists():
|
|
193
|
+
previous = json.loads(pending.read_text())
|
|
194
|
+
if previous["server"] != cfg["server"] or previous["spec"] != spec:
|
|
195
|
+
raise ValueError("存在未确认的创建请求,请先用原参数重试,确认运行编号后再发起新评测")
|
|
196
|
+
request_id = previous["request_id"]
|
|
197
|
+
else:
|
|
198
|
+
request_id = str(uuid4())
|
|
199
|
+
save(pending, {"server": cfg["server"], "spec": spec, "request_id": request_id})
|
|
200
|
+
try:
|
|
201
|
+
result = service.call("POST", "/runs", json=spec | {"request_id": request_id})
|
|
202
|
+
except ApiError as error:
|
|
203
|
+
if 400 <= error.status < 500:
|
|
204
|
+
pending.unlink()
|
|
205
|
+
raise
|
|
206
|
+
run_id = result["run_id"]
|
|
207
|
+
print(f"运行编号:{run_id};中断后使用 aeval resume {run_id}", flush=True)
|
|
208
|
+
pending.unlink()
|
|
209
|
+
return execute(service, cfg, run_id)
|
|
210
|
+
except (ValueError, httpx.HTTPError, OSError, KeyboardInterrupt) as error:
|
|
211
|
+
# 不打印 HTTP 异常中的请求 URL、请求头或响应正文,避免泄漏凭据。
|
|
212
|
+
print(f"评测未完成:{error if isinstance(error, ValueError) else type(error).__name__}")
|
|
213
|
+
if run_id:
|
|
214
|
+
print(f"恢复命令:aeval resume {run_id}")
|
|
215
|
+
return 2
|
|
216
|
+
finally:
|
|
217
|
+
if service:
|
|
218
|
+
service.close()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if __name__ == "__main__":
|
|
222
|
+
raise SystemExit(main())
|
aeval_client/config.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""交互和手写配置共用 TOML;凭据与恢复文件仅当前用户可读。"""
|
|
2
|
+
|
|
3
|
+
import getpass
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import tempfile
|
|
8
|
+
import tomllib
|
|
9
|
+
from urllib.parse import urlsplit
|
|
10
|
+
|
|
11
|
+
HOME = Path.home() / ".aeval"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def private_write(path, content):
|
|
15
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
16
|
+
fd, temporary = tempfile.mkstemp(dir=path.parent)
|
|
17
|
+
try:
|
|
18
|
+
with os.fdopen(fd, "w") as stream:
|
|
19
|
+
stream.write(content)
|
|
20
|
+
stream.flush()
|
|
21
|
+
os.fsync(stream.fileno())
|
|
22
|
+
os.replace(temporary, path)
|
|
23
|
+
finally:
|
|
24
|
+
if os.path.exists(temporary):
|
|
25
|
+
os.unlink(temporary)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def url(value):
|
|
29
|
+
value = value if "://" in value else "http://" + value
|
|
30
|
+
parsed = urlsplit(value)
|
|
31
|
+
if parsed.scheme not in ("http", "https") or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment:
|
|
32
|
+
raise ValueError("地址必须是 http(s) URL,不能包含凭据、查询参数或片段")
|
|
33
|
+
return value.rstrip("/")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read(path=None):
|
|
37
|
+
path = path or HOME / "config"
|
|
38
|
+
if not path.exists():
|
|
39
|
+
raise ValueError("请先执行 aeval configure 或创建 ~/.aeval/config")
|
|
40
|
+
if path.stat().st_mode & 0o077:
|
|
41
|
+
raise ValueError(f"配置包含凭据,请先执行 chmod 600 {path}")
|
|
42
|
+
cfg = tomllib.loads(path.read_text())
|
|
43
|
+
for key in ("server", "api_key", "customer", "protocol"):
|
|
44
|
+
if not isinstance(cfg.get(key), str) or not cfg[key].strip():
|
|
45
|
+
raise ValueError(f"配置缺少 {key}")
|
|
46
|
+
cfg["server"] = url(cfg["server"])
|
|
47
|
+
if cfg["protocol"] not in ("main", "v1"):
|
|
48
|
+
raise ValueError("protocol 必须是 main 或 v1")
|
|
49
|
+
for key in ("dataset", "environment", "agent_token"):
|
|
50
|
+
if key in cfg and (not isinstance(cfg[key], str) or not cfg[key].strip()):
|
|
51
|
+
raise ValueError(f"{key} 必须是非空字符串")
|
|
52
|
+
return cfg
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def configure():
|
|
56
|
+
values = {}
|
|
57
|
+
for key, label in (("server", "Agent-Eval 地址"), ("api_key", "团队 API Key"),
|
|
58
|
+
("customer", "客户"), ("protocol", "Agent 协议 main/v1"),
|
|
59
|
+
("dataset", "默认 Langfuse 数据集 ID(可留空)"),
|
|
60
|
+
("agent_token", "Agent bearer token(可留空)")):
|
|
61
|
+
values[key] = (getpass.getpass(label + ": ") if "key" in key or "token" in key else input(label + ": ")).strip()
|
|
62
|
+
values = {k: v for k, v in values.items() if v}
|
|
63
|
+
content = "".join(f"{k} = {json.dumps(v, ensure_ascii=False)}\n" for k, v in values.items())
|
|
64
|
+
# 先验证临时配置,避免无效交互覆盖已有配置。
|
|
65
|
+
temporary = HOME / "config.new"
|
|
66
|
+
private_write(temporary, content)
|
|
67
|
+
try:
|
|
68
|
+
read(temporary)
|
|
69
|
+
temporary.replace(HOME / "config")
|
|
70
|
+
finally:
|
|
71
|
+
temporary.unlink(missing_ok=True)
|
|
72
|
+
print(f"配置已保存:{HOME / 'config'}")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aidong-aeval
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OmniAgent 开发者评测客户端
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: httpx<1,>=0.27
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# Agent-Eval 开发者 CLI
|
|
10
|
+
|
|
11
|
+
在开发机调用 OmniAgent,中心服务统一出题、判定和保存报告。
|
|
12
|
+
Agent 自行配置 Langfuse 和写工具禁用;客户端不需要数据库或 Langfuse 凭据。
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv tool install aidong-aeval
|
|
16
|
+
aeval configure
|
|
17
|
+
aeval datasets
|
|
18
|
+
aeval --target 127.0.0.1:8787 --dataset <数据集ID> --revision <版本>
|
|
19
|
+
aeval resume <运行编号>
|
|
20
|
+
aeval results <运行编号>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
PyPI 包名为 `aidong-aeval`,命令名为 `aeval`。
|
|
24
|
+
开发安装:`uv tool install ./client`。旧运维包也提供 aeval 命令,请使用不同工具环境。
|
|
25
|
+
|
|
26
|
+
也可手写 `~/.aeval/config`(TOML),并执行 `chmod 600 ~/.aeval/config`:
|
|
27
|
+
|
|
28
|
+
```toml
|
|
29
|
+
server = "https://评测服务域名"
|
|
30
|
+
api_key = "团队口令"
|
|
31
|
+
customer = "客户标识"
|
|
32
|
+
protocol = "v1"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
可选 `dataset`、`environment`、`agent_token`。环境无默认值;Agent bearer token
|
|
36
|
+
仅发给 Agent,不发给评测服务。`--environment` 只核对、不设置环境;
|
|
37
|
+
`--model` 只核对实际模型,不切换模型。v1 可传 `--effort high`,
|
|
38
|
+
CLI 先验证模型支持的档位;main 不支持该参数。
|
|
39
|
+
|
|
40
|
+
调用断线后先执行 resume,从 trace 恢复已有回答;没有完成证据时不自动重问。
|
|
41
|
+
确认允许重复调用时执行 `aeval resume <编号> --retry-uncertain`,重新预检后使用新会话。
|
|
42
|
+
回答缓存保存在 `~/.aeval/runs/`,确认上传后删除。
|
|
43
|
+
|
|
44
|
+
## 发布准备
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv build --project client --out-dir dist/client
|
|
48
|
+
uvx twine check dist/client/*
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
检查 wheel/sdist 只包含客户端源码和公开说明;在隔离环境安装 wheel 并运行
|
|
52
|
+
`aeval --help`,完成连接真实测试服务的验证。凭据、客户数据与服务端源码不打包。
|
|
53
|
+
人工确认 PyPI 项目名称、维护者权限与发布凭据后才可执行上传;
|
|
54
|
+
本仓不配置 tag 自动发布。
|
|
55
|
+
首次上传创建 PyPI 项目;后续发布使用仅限 `aidong-aeval` 项目的 Token,
|
|
56
|
+
不长期保留首次创建项目使用的账户级 Token。
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
aeval_client/__init__.py,sha256=imxMk1yQiH3iHPyY4goErSC1H0HoHDBjQNLw7ZIVe-E,71
|
|
2
|
+
aeval_client/agent.py,sha256=C05cEnzlj67-ndHkeSqlXD8Ozmrh6X5bE47AJEsmnkU,2414
|
|
3
|
+
aeval_client/cli.py,sha256=MPLDpB3QmB81aO0kSYJx73uY96lc0k-YUx9h1atTJ5U,9944
|
|
4
|
+
aeval_client/config.py,sha256=IWQmg_1V1Qtj4Ct_G7Vb7g6Jk_OhW4BwNJkonX-NZZY,2885
|
|
5
|
+
aidong_aeval-0.1.0.dist-info/METADATA,sha256=ZI8R2S7GzsGbiSKXoc__TjVuG9WjMsMupij-djyMcJU,2187
|
|
6
|
+
aidong_aeval-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
7
|
+
aidong_aeval-0.1.0.dist-info/entry_points.txt,sha256=4mrxz4u-qMm7FdgIovKnB5yR4c9lEKGLYgV5bhNbKE0,48
|
|
8
|
+
aidong_aeval-0.1.0.dist-info/RECORD,,
|