tars-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.
- tars_cli/__init__.py +1 -0
- tars_cli/__main__.py +4 -0
- tars_cli/app.py +164 -0
- tars_cli/auth.py +58 -0
- tars_cli/client.py +117 -0
- tars_cli/commands/__init__.py +0 -0
- tars_cli/commands/auth_cmd.py +71 -0
- tars_cli/commands/dataset_cmd.py +258 -0
- tars_cli/commands/eval_cmd.py +126 -0
- tars_cli/commands/plugin_cmd.py +103 -0
- tars_cli/commands/template_cmd.py +59 -0
- tars_cli/config.py +33 -0
- tars_cli/hints.py +59 -0
- tars_cli/output.py +64 -0
- tars_cli/utils/__init__.py +0 -0
- tars_cli/utils/polling.py +67 -0
- tars_cli/utils/progress.py +44 -0
- tars_cli-0.1.0.dist-info/METADATA +18 -0
- tars_cli-0.1.0.dist-info/RECORD +22 -0
- tars_cli-0.1.0.dist-info/WHEEL +5 -0
- tars_cli-0.1.0.dist-info/entry_points.txt +2 -0
- tars_cli-0.1.0.dist-info/top_level.txt +1 -0
tars_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
tars_cli/__main__.py
ADDED
tars_cli/app.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from tars_cli import __version__
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="tars",
|
|
14
|
+
help=(
|
|
15
|
+
"tars CLI - 评估平台命令行工具\n\n"
|
|
16
|
+
"🚀 快速上手:\n"
|
|
17
|
+
" 路线 A(智能生成): tars auth login → tars plugin import → tars dataset generate → tars eval run\n"
|
|
18
|
+
" 路线 B(手动上传): tars auth login → tars plugin import → tars template download → tars dataset upload → tars eval run\n\n"
|
|
19
|
+
"运行 tars quickstart 查看完整示例"
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
auth_app = typer.Typer(
|
|
24
|
+
name="auth",
|
|
25
|
+
help="认证管理\n\n典型流程: tars auth login --server <url>",
|
|
26
|
+
no_args_is_help=True,
|
|
27
|
+
)
|
|
28
|
+
plugin_app = typer.Typer(
|
|
29
|
+
name="plugin",
|
|
30
|
+
help="Plugin 管理\n\n典型流程: tars plugin import <git_url> → tars plugin export-report <plugin_id>",
|
|
31
|
+
no_args_is_help=True,
|
|
32
|
+
)
|
|
33
|
+
dataset_app = typer.Typer(
|
|
34
|
+
name="dataset",
|
|
35
|
+
help="数据集管理\n\n典型流程: tars dataset generate <plugin_id> → tars dataset publish <version_id>",
|
|
36
|
+
no_args_is_help=True,
|
|
37
|
+
)
|
|
38
|
+
eval_app = typer.Typer(
|
|
39
|
+
name="eval",
|
|
40
|
+
help="评估管理\n\n典型流程: tars eval run <plugin_id> → tars eval status <task_id>",
|
|
41
|
+
no_args_is_help=True,
|
|
42
|
+
)
|
|
43
|
+
template_app = typer.Typer(
|
|
44
|
+
name="template",
|
|
45
|
+
help="模板管理\n\n典型流程: tars template download plan → tars template download testcase-single",
|
|
46
|
+
no_args_is_help=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
app.add_typer(auth_app, name="auth")
|
|
50
|
+
app.add_typer(plugin_app, name="plugin")
|
|
51
|
+
app.add_typer(dataset_app, name="dataset")
|
|
52
|
+
app.add_typer(eval_app, name="eval")
|
|
53
|
+
app.add_typer(template_app, name="template")
|
|
54
|
+
|
|
55
|
+
# Global options stored in context
|
|
56
|
+
_output_format: str = "table"
|
|
57
|
+
_quiet: bool = False
|
|
58
|
+
_verbose: bool = False
|
|
59
|
+
_server: Optional[str] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.callback(invoke_without_command=True)
|
|
63
|
+
def main(
|
|
64
|
+
ctx: typer.Context,
|
|
65
|
+
output: str = typer.Option("table", "--output", "-o", help="输出格式: table, json, yaml"),
|
|
66
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="静默模式,仅输出核心结果"),
|
|
67
|
+
verbose: bool = typer.Option(False, "--verbose", help="详细模式,输出 HTTP 请求日志"),
|
|
68
|
+
server: Optional[str] = typer.Option(None, "--server", help="服务端地址"),
|
|
69
|
+
version: bool = typer.Option(False, "--version", "-V", help="显示版本号"),
|
|
70
|
+
) -> None:
|
|
71
|
+
global _output_format, _quiet, _verbose, _server
|
|
72
|
+
if version:
|
|
73
|
+
typer.echo(f"tars-cli {__version__}")
|
|
74
|
+
raise typer.Exit()
|
|
75
|
+
_output_format = output
|
|
76
|
+
_quiet = quiet
|
|
77
|
+
_verbose = verbose
|
|
78
|
+
_server = server
|
|
79
|
+
if ctx.invoked_subcommand is None:
|
|
80
|
+
typer.echo(ctx.get_help())
|
|
81
|
+
raise typer.Exit()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_global_options() -> tuple[str, bool, bool, Optional[str]]:
|
|
85
|
+
return _output_format, _quiet, _verbose, _server
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
QUICKSTART_TEXT = """\
|
|
89
|
+
🚀 tars CLI 快速上手
|
|
90
|
+
|
|
91
|
+
━━━ 路线 A: 智能生成 ━━━
|
|
92
|
+
|
|
93
|
+
# 1. 登录
|
|
94
|
+
tars auth login --server https://tars.example.com
|
|
95
|
+
|
|
96
|
+
# 2. 导入 Plugin
|
|
97
|
+
tars plugin import https://git.example.com/my-plugin.git
|
|
98
|
+
|
|
99
|
+
# 3. 智能生成数据集(Agent / Skill)
|
|
100
|
+
tars dataset generate <plugin_id> --agent-name <name> --publish
|
|
101
|
+
tars dataset generate <plugin_id> --skill-name <name> --publish
|
|
102
|
+
|
|
103
|
+
# 多轮场景
|
|
104
|
+
tars dataset generate <plugin_id> --agent-name <name> -t multi_turn --publish
|
|
105
|
+
|
|
106
|
+
# 4. 运行评估
|
|
107
|
+
tars eval run <plugin_id> --agent-name <name> --dataset-version <version_id>
|
|
108
|
+
tars eval run <plugin_id> --skill-name <name> --dataset-version <version_id>
|
|
109
|
+
|
|
110
|
+
# 5. 导出报告(可选)
|
|
111
|
+
tars plugin export-report <plugin_id>
|
|
112
|
+
|
|
113
|
+
━━━ 路线 B: 手动上传 ━━━
|
|
114
|
+
|
|
115
|
+
# 1. 登录
|
|
116
|
+
tars auth login --server https://tars.example.com
|
|
117
|
+
|
|
118
|
+
# 2. 导入 Plugin
|
|
119
|
+
tars plugin import https://git.example.com/my-plugin.git
|
|
120
|
+
|
|
121
|
+
# 3. 下载模板
|
|
122
|
+
tars template download plan
|
|
123
|
+
tars template download testcase-single
|
|
124
|
+
tars template download testcase-multi
|
|
125
|
+
|
|
126
|
+
# 4. 填写模板后上传(Agent / Skill)
|
|
127
|
+
tars dataset upload <plugin_id> --agent-name <name> --plan plan.yaml --cases cases.yaml
|
|
128
|
+
tars dataset upload <plugin_id> --skill-name <name> --plan plan.yaml --cases cases.yaml
|
|
129
|
+
|
|
130
|
+
# 5. 运行评估
|
|
131
|
+
tars eval run <plugin_id> --agent-name <name> --dataset-version <version_id>
|
|
132
|
+
|
|
133
|
+
━━━ 其他常用命令 ━━━
|
|
134
|
+
|
|
135
|
+
# 查看数据集版本列表
|
|
136
|
+
tars dataset list <plugin_id> --agent-name <name>
|
|
137
|
+
tars dataset list <plugin_id> --skill-name <name> -t multi_turn
|
|
138
|
+
|
|
139
|
+
# 发布草稿数据集
|
|
140
|
+
tars dataset publish <version_id>
|
|
141
|
+
|
|
142
|
+
# 查看评估任务状态
|
|
143
|
+
tars eval status <task_id>
|
|
144
|
+
|
|
145
|
+
━━━ CI/CD 集成 ━━━
|
|
146
|
+
|
|
147
|
+
export TARS_ACCESS_TOKEN=eyJhbGc...
|
|
148
|
+
export TARS_SERVER_URL=https://tars.example.com
|
|
149
|
+
tars eval run <plugin_id> --agent-name <name> --dataset-version <id> -o json --threshold 0.8
|
|
150
|
+
# 退出码: 0=通过, 1=未通过, 2=错误
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.command()
|
|
155
|
+
def quickstart() -> None:
|
|
156
|
+
"""输出完整的端到端使用示例"""
|
|
157
|
+
typer.echo(QUICKSTART_TEXT)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
import tars_cli.commands.auth_cmd # noqa: E402, F401
|
|
161
|
+
import tars_cli.commands.plugin_cmd # noqa: E402, F401
|
|
162
|
+
import tars_cli.commands.dataset_cmd # noqa: E402, F401
|
|
163
|
+
import tars_cli.commands.eval_cmd # noqa: E402, F401
|
|
164
|
+
import tars_cli.commands.template_cmd # noqa: E402, F401
|
tars_cli/auth.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import stat
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from tars_cli.config import get_credentials_path, get_env_token, resolve_server_url
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Credentials:
|
|
13
|
+
server_url: str
|
|
14
|
+
access_token: str
|
|
15
|
+
refresh_token: str
|
|
16
|
+
expires_at: str = ""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def save_credentials(creds: Credentials) -> None:
|
|
20
|
+
path = get_credentials_path()
|
|
21
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
path.write_text(json.dumps({
|
|
23
|
+
"server_url": creds.server_url,
|
|
24
|
+
"access_token": creds.access_token,
|
|
25
|
+
"refresh_token": creds.refresh_token,
|
|
26
|
+
"expires_at": creds.expires_at,
|
|
27
|
+
}, indent=2))
|
|
28
|
+
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_credentials(cli_server: str | None = None) -> Credentials | None:
|
|
32
|
+
env_token = get_env_token()
|
|
33
|
+
if env_token:
|
|
34
|
+
return Credentials(
|
|
35
|
+
server_url=resolve_server_url(cli_server),
|
|
36
|
+
access_token=env_token,
|
|
37
|
+
refresh_token="",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
path = get_credentials_path()
|
|
41
|
+
if not path.exists():
|
|
42
|
+
return None
|
|
43
|
+
try:
|
|
44
|
+
data = json.loads(path.read_text())
|
|
45
|
+
return Credentials(
|
|
46
|
+
server_url=resolve_server_url(cli_server) if cli_server else data.get("server_url", ""),
|
|
47
|
+
access_token=data["access_token"],
|
|
48
|
+
refresh_token=data.get("refresh_token", ""),
|
|
49
|
+
expires_at=data.get("expires_at", ""),
|
|
50
|
+
)
|
|
51
|
+
except (json.JSONDecodeError, KeyError):
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def clear_credentials() -> None:
|
|
56
|
+
path = get_credentials_path()
|
|
57
|
+
if path.exists():
|
|
58
|
+
path.unlink()
|
tars_cli/client.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from httpx import HTTPTransport
|
|
9
|
+
|
|
10
|
+
from tars_cli.auth import Credentials, load_credentials, save_credentials
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthExpiredError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TarsAPIClient:
|
|
18
|
+
def __init__(self, server_url: str, credentials: Credentials, verbose: bool = False) -> None:
|
|
19
|
+
self._server_url = server_url.rstrip("/")
|
|
20
|
+
self._credentials = credentials
|
|
21
|
+
self._verbose = verbose
|
|
22
|
+
self._client = httpx.Client(base_url=f"{self._server_url}/api/v1", timeout=30.0, transport=HTTPTransport(proxy=None))
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_context(cls, cli_server: str | None = None, verbose: bool = False) -> TarsAPIClient:
|
|
26
|
+
creds = load_credentials(cli_server)
|
|
27
|
+
if not creds:
|
|
28
|
+
typer.echo("未登录,请先执行 tars auth login", err=True)
|
|
29
|
+
raise typer.Exit(code=2)
|
|
30
|
+
server_url = creds.server_url
|
|
31
|
+
return cls(server_url=server_url, credentials=creds, verbose=verbose)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def server_url(self) -> str:
|
|
35
|
+
return self._server_url
|
|
36
|
+
|
|
37
|
+
def _headers(self) -> dict[str, str]:
|
|
38
|
+
return {"Authorization": f"Bearer {self._credentials.access_token}"}
|
|
39
|
+
|
|
40
|
+
def _log_request(self, method: str, url: str, **kwargs: Any) -> None:
|
|
41
|
+
if self._verbose:
|
|
42
|
+
typer.echo(f" → {method} {url}", err=True)
|
|
43
|
+
if "json" in kwargs:
|
|
44
|
+
typer.echo(f" Body: {kwargs['json']}", err=True)
|
|
45
|
+
|
|
46
|
+
def _log_response(self, resp: httpx.Response) -> None:
|
|
47
|
+
if self._verbose:
|
|
48
|
+
typer.echo(f" ← {resp.status_code} ({len(resp.content)} bytes)", err=True)
|
|
49
|
+
|
|
50
|
+
def _refresh_token(self) -> bool:
|
|
51
|
+
if not self._credentials.refresh_token:
|
|
52
|
+
return False
|
|
53
|
+
try:
|
|
54
|
+
resp = self._client.post("/auth/refresh", json={"refresh_token": self._credentials.refresh_token})
|
|
55
|
+
if resp.status_code == 200:
|
|
56
|
+
data = resp.json()
|
|
57
|
+
self._credentials.access_token = data["access_token"]
|
|
58
|
+
self._credentials.refresh_token = data.get("refresh_token", self._credentials.refresh_token)
|
|
59
|
+
self._credentials.expires_at = data.get("expires_at", "")
|
|
60
|
+
save_credentials(self._credentials)
|
|
61
|
+
return True
|
|
62
|
+
except httpx.HTTPError:
|
|
63
|
+
pass
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
|
|
67
|
+
url = path
|
|
68
|
+
headers = kwargs.pop("headers", None) or {}
|
|
69
|
+
headers.update(self._headers())
|
|
70
|
+
kwargs["headers"] = headers
|
|
71
|
+
self._log_request(method, url, **kwargs)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
resp = self._client.request(method, url, **kwargs)
|
|
75
|
+
except httpx.ConnectError:
|
|
76
|
+
typer.echo(f"无法连接到服务端: {self._server_url}", err=True)
|
|
77
|
+
raise typer.Exit(code=2)
|
|
78
|
+
except httpx.TimeoutException:
|
|
79
|
+
typer.echo(f"请求超时: {method} {url}", err=True)
|
|
80
|
+
raise typer.Exit(code=2)
|
|
81
|
+
|
|
82
|
+
self._log_response(resp)
|
|
83
|
+
|
|
84
|
+
if resp.status_code == 401:
|
|
85
|
+
if self._refresh_token():
|
|
86
|
+
kwargs["headers"].update(self._headers())
|
|
87
|
+
self._log_request(method, url, **kwargs)
|
|
88
|
+
resp = self._client.request(method, url, **kwargs)
|
|
89
|
+
self._log_response(resp)
|
|
90
|
+
else:
|
|
91
|
+
raise AuthExpiredError("认证已过期,请重新执行 tars auth login")
|
|
92
|
+
|
|
93
|
+
return resp
|
|
94
|
+
|
|
95
|
+
def get(self, path: str, **kwargs: Any) -> httpx.Response:
|
|
96
|
+
try:
|
|
97
|
+
return self._request("GET", path, **kwargs)
|
|
98
|
+
except AuthExpiredError as e:
|
|
99
|
+
typer.echo(str(e), err=True)
|
|
100
|
+
raise typer.Exit(code=2)
|
|
101
|
+
|
|
102
|
+
def post(self, path: str, **kwargs: Any) -> httpx.Response:
|
|
103
|
+
try:
|
|
104
|
+
return self._request("POST", path, **kwargs)
|
|
105
|
+
except AuthExpiredError as e:
|
|
106
|
+
typer.echo(str(e), err=True)
|
|
107
|
+
raise typer.Exit(code=2)
|
|
108
|
+
|
|
109
|
+
def put(self, path: str, **kwargs: Any) -> httpx.Response:
|
|
110
|
+
try:
|
|
111
|
+
return self._request("PUT", path, **kwargs)
|
|
112
|
+
except AuthExpiredError as e:
|
|
113
|
+
typer.echo(str(e), err=True)
|
|
114
|
+
raise typer.Exit(code=2)
|
|
115
|
+
|
|
116
|
+
def close(self) -> None:
|
|
117
|
+
self._client.close()
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from httpx import HTTPTransport
|
|
7
|
+
|
|
8
|
+
from tars_cli.app import auth_app, get_global_options
|
|
9
|
+
from tars_cli.auth import Credentials, clear_credentials, load_credentials, save_credentials
|
|
10
|
+
from tars_cli.client import TarsAPIClient
|
|
11
|
+
from tars_cli.config import resolve_server_url
|
|
12
|
+
from tars_cli.hints import print_hints
|
|
13
|
+
from tars_cli.output import render
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@auth_app.command()
|
|
17
|
+
def login(
|
|
18
|
+
server: str = typer.Option("", "--server", "-s", help="服务端地址"),
|
|
19
|
+
) -> None:
|
|
20
|
+
"""登录 tars 平台"""
|
|
21
|
+
output_fmt, quiet, verbose, global_server = get_global_options()
|
|
22
|
+
server_url = resolve_server_url(server or global_server)
|
|
23
|
+
|
|
24
|
+
username = typer.prompt("用户名")
|
|
25
|
+
password = typer.prompt("密码", hide_input=True)
|
|
26
|
+
|
|
27
|
+
client = httpx.Client(base_url=f"{server_url}/api/v1", timeout=30.0, transport=HTTPTransport(proxy=None))
|
|
28
|
+
try:
|
|
29
|
+
resp = client.post("/auth/login", json={"username": username, "password": password})
|
|
30
|
+
if resp.status_code != 200:
|
|
31
|
+
typer.echo(f"登录失败: {resp.json().get('detail', resp.text)}", err=True)
|
|
32
|
+
raise typer.Exit(code=2)
|
|
33
|
+
|
|
34
|
+
data = resp.json()
|
|
35
|
+
creds = Credentials(
|
|
36
|
+
server_url=server_url,
|
|
37
|
+
access_token=data["access_token"],
|
|
38
|
+
refresh_token=data.get("refresh_token", ""),
|
|
39
|
+
expires_at=data.get("expires_at", ""),
|
|
40
|
+
)
|
|
41
|
+
save_credentials(creds)
|
|
42
|
+
|
|
43
|
+
me_resp = client.get("/auth/me", headers={"Authorization": f"Bearer {creds.access_token}"})
|
|
44
|
+
user_info = me_resp.json() if me_resp.status_code == 200 else {}
|
|
45
|
+
|
|
46
|
+
result = {
|
|
47
|
+
"message": "登录成功",
|
|
48
|
+
"username": user_info.get("username", username),
|
|
49
|
+
"server": server_url,
|
|
50
|
+
}
|
|
51
|
+
web_url, next_step = print_hints(server_url, "auth_login", quiet=quiet)
|
|
52
|
+
render(result, output_format=output_fmt, quiet=quiet, web_url=web_url, next_step=next_step)
|
|
53
|
+
finally:
|
|
54
|
+
client.close()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@auth_app.command()
|
|
58
|
+
def logout() -> None:
|
|
59
|
+
"""登出并清除本地凭证"""
|
|
60
|
+
output_fmt, quiet, verbose, global_server = get_global_options()
|
|
61
|
+
creds = load_credentials(global_server)
|
|
62
|
+
if creds and creds.refresh_token:
|
|
63
|
+
try:
|
|
64
|
+
client = TarsAPIClient(server_url=creds.server_url, credentials=creds, verbose=verbose)
|
|
65
|
+
client.post("/auth/logout", json={"refresh_token": creds.refresh_token})
|
|
66
|
+
client.close()
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
clear_credentials()
|
|
71
|
+
typer.echo("已登出")
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from tars_cli.app import dataset_app, get_global_options
|
|
9
|
+
from tars_cli.auth import load_credentials
|
|
10
|
+
from tars_cli.client import TarsAPIClient
|
|
11
|
+
from tars_cli.hints import print_hints
|
|
12
|
+
from tars_cli.output import render, render_table
|
|
13
|
+
from tars_cli.utils.polling import poll_task
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataset_app.command()
|
|
17
|
+
def generate(
|
|
18
|
+
plugin_id: str = typer.Argument(help="Plugin ID"),
|
|
19
|
+
agent_name: Optional[str] = typer.Option(None, "--agent-name", help="Agent 名称"),
|
|
20
|
+
skill_name: Optional[str] = typer.Option(None, "--skill-name", help="Skill 名称"),
|
|
21
|
+
conversation_type: str = typer.Option("single_turn", "--conversation-type", "-t", help="会话类型: single_turn, multi_turn"),
|
|
22
|
+
publish: bool = typer.Option(False, "--publish", help="生成后自动发布"),
|
|
23
|
+
) -> None:
|
|
24
|
+
"""智能生成数据集(路线 A)"""
|
|
25
|
+
if agent_name and skill_name:
|
|
26
|
+
typer.echo("--agent-name 和 --skill-name 不能同时使用", err=True)
|
|
27
|
+
raise typer.Exit(code=2)
|
|
28
|
+
if not agent_name and not skill_name:
|
|
29
|
+
typer.echo("请指定 --agent-name 或 --skill-name", err=True)
|
|
30
|
+
raise typer.Exit(code=2)
|
|
31
|
+
|
|
32
|
+
output_fmt, quiet, verbose, server = get_global_options()
|
|
33
|
+
creds = load_credentials(server)
|
|
34
|
+
if not creds:
|
|
35
|
+
typer.echo("未登录,请先执行 tars auth login", err=True)
|
|
36
|
+
raise typer.Exit(code=2)
|
|
37
|
+
|
|
38
|
+
client = TarsAPIClient(server_url=creds.server_url, credentials=creds, verbose=verbose)
|
|
39
|
+
try:
|
|
40
|
+
target_name = agent_name or skill_name
|
|
41
|
+
body: dict = {"plugin_id": plugin_id, "conversation_type": conversation_type, "agent_name": target_name}
|
|
42
|
+
if skill_name:
|
|
43
|
+
body["evaluation_target_type"] = "skill"
|
|
44
|
+
body["skill_name"] = skill_name
|
|
45
|
+
|
|
46
|
+
resp = client.post("/data-set-versions", json=body)
|
|
47
|
+
if not resp.is_success:
|
|
48
|
+
typer.echo(f"创建版本失败: {resp.text}", err=True)
|
|
49
|
+
raise typer.Exit(code=2)
|
|
50
|
+
version_data = resp.json()
|
|
51
|
+
version_id = version_data["id"]
|
|
52
|
+
|
|
53
|
+
plan_body: dict = {"version_id": version_id, "plugin_id": plugin_id, "agent_name": target_name}
|
|
54
|
+
resp = client.post("/datasets/generate/generate-plan", json=plan_body)
|
|
55
|
+
if not resp.is_success:
|
|
56
|
+
typer.echo(f"生成计划失败: {resp.text}", err=True)
|
|
57
|
+
raise typer.Exit(code=2)
|
|
58
|
+
|
|
59
|
+
plan_data = poll_task(
|
|
60
|
+
request_fn=lambda: client.get("/datasets/generate/generate-plan/status", params={"version_id": version_id}),
|
|
61
|
+
label="生成评估计划",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
strategy = plan_data.get("test_strategy")
|
|
65
|
+
strategy_readable = plan_data.get("test_strategy_readable", "")
|
|
66
|
+
if strategy:
|
|
67
|
+
save_resp = client.put(
|
|
68
|
+
f"/data-set-versions/{version_id}/test-strategy",
|
|
69
|
+
json={"test_strategy": strategy, "test_strategy_readable": strategy_readable},
|
|
70
|
+
)
|
|
71
|
+
if not save_resp.is_success:
|
|
72
|
+
typer.echo(f"保存评估计划失败: {save_resp.text}", err=True)
|
|
73
|
+
raise typer.Exit(code=2)
|
|
74
|
+
|
|
75
|
+
resp = client.post(f"/data-set-versions/{version_id}/generate-cases")
|
|
76
|
+
if not resp.is_success:
|
|
77
|
+
typer.echo(f"生成用例失败: {resp.text}", err=True)
|
|
78
|
+
raise typer.Exit(code=2)
|
|
79
|
+
|
|
80
|
+
result_data = poll_task(
|
|
81
|
+
request_fn=lambda: client.get(f"/data-set-versions/{version_id}/generate-cases/status"),
|
|
82
|
+
label="生成评估用例",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if publish:
|
|
86
|
+
resp = client.post(f"/data-set-versions/{version_id}/publish")
|
|
87
|
+
if not resp.is_success:
|
|
88
|
+
typer.echo(f"发布失败: {resp.text}", err=True)
|
|
89
|
+
raise typer.Exit(code=2)
|
|
90
|
+
|
|
91
|
+
result = {
|
|
92
|
+
"version_id": version_id,
|
|
93
|
+
"status": "published" if publish else "draft",
|
|
94
|
+
"case_count": result_data.get("case_count", ""),
|
|
95
|
+
"message": "数据集生成完成",
|
|
96
|
+
}
|
|
97
|
+
web_url, next_step = print_hints(
|
|
98
|
+
creds.server_url, "dataset_generate", quiet=quiet,
|
|
99
|
+
plugin_id=plugin_id, version_id=str(version_id),
|
|
100
|
+
)
|
|
101
|
+
render(result, output_format=output_fmt, quiet=quiet, web_url=web_url, next_step=next_step)
|
|
102
|
+
finally:
|
|
103
|
+
client.close()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataset_app.command()
|
|
107
|
+
def upload(
|
|
108
|
+
plugin_id: str = typer.Argument(help="Plugin ID"),
|
|
109
|
+
agent_name: Optional[str] = typer.Option(None, "--agent-name", help="Agent 名称"),
|
|
110
|
+
skill_name: Optional[str] = typer.Option(None, "--skill-name", help="Skill 名称"),
|
|
111
|
+
plan: str = typer.Option(..., "--plan", help="评估计划文件路径"),
|
|
112
|
+
cases: str = typer.Option(..., "--cases", help="用例文件路径"),
|
|
113
|
+
conversation_type: str = typer.Option("single_turn", "--conversation-type", "-t", help="会话类型"),
|
|
114
|
+
) -> None:
|
|
115
|
+
"""手动上传数据集(路线 B)"""
|
|
116
|
+
if agent_name and skill_name:
|
|
117
|
+
typer.echo("--agent-name 和 --skill-name 不能同时使用", err=True)
|
|
118
|
+
raise typer.Exit(code=2)
|
|
119
|
+
if not agent_name and not skill_name:
|
|
120
|
+
typer.echo("请指定 --agent-name 或 --skill-name", err=True)
|
|
121
|
+
raise typer.Exit(code=2)
|
|
122
|
+
|
|
123
|
+
for f in [plan, cases]:
|
|
124
|
+
if not Path(f).exists():
|
|
125
|
+
typer.echo(f"文件不存在: {f}", err=True)
|
|
126
|
+
raise typer.Exit(code=2)
|
|
127
|
+
|
|
128
|
+
output_fmt, quiet, verbose, server = get_global_options()
|
|
129
|
+
creds = load_credentials(server)
|
|
130
|
+
if not creds:
|
|
131
|
+
typer.echo("未登录,请先执行 tars auth login", err=True)
|
|
132
|
+
raise typer.Exit(code=2)
|
|
133
|
+
|
|
134
|
+
client = TarsAPIClient(server_url=creds.server_url, credentials=creds, verbose=verbose)
|
|
135
|
+
try:
|
|
136
|
+
target_name = agent_name or skill_name
|
|
137
|
+
body: dict = {"plugin_id": plugin_id, "conversation_type": conversation_type, "agent_name": target_name}
|
|
138
|
+
if skill_name:
|
|
139
|
+
body["evaluation_target_type"] = "skill"
|
|
140
|
+
body["skill_name"] = skill_name
|
|
141
|
+
|
|
142
|
+
resp = client.post("/data-set-versions", json=body)
|
|
143
|
+
if not resp.is_success:
|
|
144
|
+
typer.echo(f"创建版本失败: {resp.text}", err=True)
|
|
145
|
+
raise typer.Exit(code=2)
|
|
146
|
+
version_id = resp.json()["id"]
|
|
147
|
+
|
|
148
|
+
with open(plan, "rb") as plan_f:
|
|
149
|
+
plan_resp = client.post(f"/data-set-versions/{version_id}/evaluation-plan/upload", files={"file": (Path(plan).name, plan_f)})
|
|
150
|
+
if not plan_resp.is_success:
|
|
151
|
+
typer.echo(f"上传计划失败: {plan_resp.text}", err=True)
|
|
152
|
+
raise typer.Exit(code=2)
|
|
153
|
+
|
|
154
|
+
with open(cases, "rb") as cases_f:
|
|
155
|
+
cases_resp = client.post(
|
|
156
|
+
f"/data-set-versions/{version_id}/import-cases",
|
|
157
|
+
files={"file": (Path(cases).name, cases_f)},
|
|
158
|
+
)
|
|
159
|
+
if not cases_resp.is_success:
|
|
160
|
+
typer.echo(f"上传用例失败: {cases_resp.text}", err=True)
|
|
161
|
+
raise typer.Exit(code=2)
|
|
162
|
+
cases_data = cases_resp.json()
|
|
163
|
+
imported_count = cases_data.get("imported_count", 0)
|
|
164
|
+
typer.echo(f"成功导入 {imported_count} 条用例", err=True)
|
|
165
|
+
|
|
166
|
+
resp = client.post(f"/data-set-versions/{version_id}/publish")
|
|
167
|
+
if not resp.is_success:
|
|
168
|
+
typer.echo(f"发布失败: {resp.text}", err=True)
|
|
169
|
+
raise typer.Exit(code=2)
|
|
170
|
+
|
|
171
|
+
result = {
|
|
172
|
+
"version_id": version_id,
|
|
173
|
+
"status": "published",
|
|
174
|
+
"message": "数据集上传并发布成功",
|
|
175
|
+
}
|
|
176
|
+
web_url, next_step = print_hints(
|
|
177
|
+
creds.server_url, "dataset_upload", quiet=quiet,
|
|
178
|
+
plugin_id=plugin_id, version_id=str(version_id),
|
|
179
|
+
)
|
|
180
|
+
render(result, output_format=output_fmt, quiet=quiet, web_url=web_url, next_step=next_step)
|
|
181
|
+
finally:
|
|
182
|
+
client.close()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataset_app.command("list")
|
|
186
|
+
def list_versions(
|
|
187
|
+
plugin_id: str = typer.Argument(help="Plugin ID"),
|
|
188
|
+
agent_name: Optional[str] = typer.Option(None, "--agent-name", help="Agent 名称"),
|
|
189
|
+
skill_name: Optional[str] = typer.Option(None, "--skill-name", help="Skill 名称"),
|
|
190
|
+
conversation_type: str = typer.Option("single_turn", "--conversation-type", "-t", help="会话类型"),
|
|
191
|
+
status: Optional[str] = typer.Option(None, "--status", help="筛选状态: draft, published"),
|
|
192
|
+
) -> None:
|
|
193
|
+
"""列出数据集版本"""
|
|
194
|
+
output_fmt, quiet, verbose, server = get_global_options()
|
|
195
|
+
creds = load_credentials(server)
|
|
196
|
+
if not creds:
|
|
197
|
+
typer.echo("未登录,请先执行 tars auth login", err=True)
|
|
198
|
+
raise typer.Exit(code=2)
|
|
199
|
+
|
|
200
|
+
client = TarsAPIClient(server_url=creds.server_url, credentials=creds, verbose=verbose)
|
|
201
|
+
try:
|
|
202
|
+
target_name = agent_name or skill_name
|
|
203
|
+
if not target_name:
|
|
204
|
+
typer.echo("请指定 --agent-name 或 --skill-name", err=True)
|
|
205
|
+
raise typer.Exit(code=2)
|
|
206
|
+
|
|
207
|
+
params: dict = {"plugin_id": plugin_id, "conversation_type": conversation_type, "agent_name": target_name}
|
|
208
|
+
if skill_name:
|
|
209
|
+
params["evaluation_target_type"] = "skill"
|
|
210
|
+
if status:
|
|
211
|
+
params["status"] = status
|
|
212
|
+
|
|
213
|
+
resp = client.get("/data-set-versions", params=params)
|
|
214
|
+
if not resp.is_success:
|
|
215
|
+
typer.echo(f"查询失败: {resp.text}", err=True)
|
|
216
|
+
raise typer.Exit(code=2)
|
|
217
|
+
|
|
218
|
+
data = resp.json()
|
|
219
|
+
items = data.get("items", data) if isinstance(data, dict) else data
|
|
220
|
+
|
|
221
|
+
if output_fmt == "table":
|
|
222
|
+
columns = ["版本 ID", "状态", "用例数", "创建时间"]
|
|
223
|
+
rows = [
|
|
224
|
+
[str(item.get("id", "")), item.get("status", ""), str(item.get("test_case_count", "")), str(item.get("created_at", ""))]
|
|
225
|
+
for item in items
|
|
226
|
+
]
|
|
227
|
+
render_table("数据集版本列表", columns, rows)
|
|
228
|
+
else:
|
|
229
|
+
render({"items": items, "total": len(items)}, output_format=output_fmt, quiet=quiet)
|
|
230
|
+
finally:
|
|
231
|
+
client.close()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataset_app.command()
|
|
235
|
+
def publish(
|
|
236
|
+
version_id: str = typer.Argument(help="数据集版本 ID"),
|
|
237
|
+
) -> None:
|
|
238
|
+
"""发布草稿数据集版本"""
|
|
239
|
+
output_fmt, quiet, verbose, server = get_global_options()
|
|
240
|
+
creds = load_credentials(server)
|
|
241
|
+
if not creds:
|
|
242
|
+
typer.echo("未登录,请先执行 tars auth login", err=True)
|
|
243
|
+
raise typer.Exit(code=2)
|
|
244
|
+
|
|
245
|
+
client = TarsAPIClient(server_url=creds.server_url, credentials=creds, verbose=verbose)
|
|
246
|
+
try:
|
|
247
|
+
resp = client.post(f"/data-set-versions/{version_id}/publish")
|
|
248
|
+
if not resp.is_success:
|
|
249
|
+
typer.echo(f"发布失败: {resp.text}", err=True)
|
|
250
|
+
raise typer.Exit(code=2)
|
|
251
|
+
|
|
252
|
+
result = {"version_id": version_id, "status": "published", "message": "发布成功"}
|
|
253
|
+
web_url, next_step = print_hints(
|
|
254
|
+
creds.server_url, "dataset_publish", quiet=quiet, version_id=version_id,
|
|
255
|
+
)
|
|
256
|
+
render(result, output_format=output_fmt, quiet=quiet, web_url=web_url, next_step=next_step)
|
|
257
|
+
finally:
|
|
258
|
+
client.close()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from tars_cli.app import eval_app, get_global_options
|
|
8
|
+
from tars_cli.auth import load_credentials
|
|
9
|
+
from tars_cli.client import TarsAPIClient
|
|
10
|
+
from tars_cli.hints import print_hints
|
|
11
|
+
from tars_cli.output import render
|
|
12
|
+
from tars_cli.utils.polling import poll_task
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@eval_app.command()
|
|
16
|
+
def run(
|
|
17
|
+
plugin_id: str = typer.Argument(help="Plugin ID"),
|
|
18
|
+
agent_name: Optional[str] = typer.Option(None, "--agent-name", help="Agent 名称"),
|
|
19
|
+
skill_name: Optional[str] = typer.Option(None, "--skill-name", help="Skill 名称"),
|
|
20
|
+
dataset_version: str = typer.Option(..., "--dataset-version", help="数据集版本 ID"),
|
|
21
|
+
threshold: Optional[float] = typer.Option(None, "--threshold", help="CI/CD 卡点阈值 (0-1)"),
|
|
22
|
+
timeout: int = typer.Option(1800, "--timeout", help="轮询超时时间(秒),默认 1800"),
|
|
23
|
+
) -> None:
|
|
24
|
+
"""运行评估任务"""
|
|
25
|
+
if agent_name and skill_name:
|
|
26
|
+
typer.echo("--agent-name 和 --skill-name 不能同时使用", err=True)
|
|
27
|
+
raise typer.Exit(code=2)
|
|
28
|
+
|
|
29
|
+
if threshold is not None and (threshold < 0 or threshold > 1):
|
|
30
|
+
typer.echo("--threshold 必须在 0-1 范围内", err=True)
|
|
31
|
+
raise typer.Exit(code=2)
|
|
32
|
+
|
|
33
|
+
output_fmt, quiet, verbose, server = get_global_options()
|
|
34
|
+
creds = load_credentials(server)
|
|
35
|
+
if not creds:
|
|
36
|
+
typer.echo("未登录,请先执行 tars auth login", err=True)
|
|
37
|
+
raise typer.Exit(code=2)
|
|
38
|
+
|
|
39
|
+
client = TarsAPIClient(server_url=creds.server_url, credentials=creds, verbose=verbose)
|
|
40
|
+
try:
|
|
41
|
+
body: dict = {"dataset_version_id": dataset_version}
|
|
42
|
+
target_name = agent_name or skill_name
|
|
43
|
+
if target_name:
|
|
44
|
+
body["agent_name"] = target_name
|
|
45
|
+
if skill_name and not agent_name:
|
|
46
|
+
body["evaluation_target_type"] = "skill"
|
|
47
|
+
body["skill_name"] = skill_name
|
|
48
|
+
|
|
49
|
+
resp = client.post(f"/plugins/{plugin_id}/evaluations", json=body)
|
|
50
|
+
if not resp.is_success:
|
|
51
|
+
typer.echo(f"创建评估任务失败: {resp.text}", err=True)
|
|
52
|
+
raise typer.Exit(code=2)
|
|
53
|
+
|
|
54
|
+
task_data = resp.json()
|
|
55
|
+
task_id = task_data["id"]
|
|
56
|
+
typer.echo(f"任务已创建: {task_id}", err=True)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
final_data = poll_task(
|
|
60
|
+
request_fn=lambda: client.get(f"/plugins/{plugin_id}/evaluations/{task_id}/progress"),
|
|
61
|
+
label="评估执行中",
|
|
62
|
+
timeout=timeout,
|
|
63
|
+
)
|
|
64
|
+
except typer.Exit:
|
|
65
|
+
typer.echo(f"\n轮询超时,任务仍在后台运行。可用以下命令查看结果:\n tars eval status {task_id}", err=True)
|
|
66
|
+
raise
|
|
67
|
+
|
|
68
|
+
detail_resp = client.get(f"/plugins/{plugin_id}/evaluations/{task_id}")
|
|
69
|
+
if detail_resp.status_code == 200:
|
|
70
|
+
final_data.update(detail_resp.json())
|
|
71
|
+
|
|
72
|
+
score = final_data.get("score", 0)
|
|
73
|
+
pass_rate = final_data.get("pass_rate", 0)
|
|
74
|
+
status = final_data.get("status", "")
|
|
75
|
+
|
|
76
|
+
result = {
|
|
77
|
+
"task_id": task_id,
|
|
78
|
+
"status": status,
|
|
79
|
+
"score": score,
|
|
80
|
+
"pass_rate": pass_rate,
|
|
81
|
+
"message": "评估完成",
|
|
82
|
+
}
|
|
83
|
+
web_url, next_step = print_hints(
|
|
84
|
+
creds.server_url, "eval_run", quiet=quiet, plugin_id=plugin_id, task_id=str(task_id),
|
|
85
|
+
)
|
|
86
|
+
render(result, output_format=output_fmt, quiet=quiet, web_url=web_url, next_step=next_step)
|
|
87
|
+
|
|
88
|
+
if threshold is not None:
|
|
89
|
+
normalized_score = score / 100.0 if score > 1 else score
|
|
90
|
+
if normalized_score < threshold:
|
|
91
|
+
raise typer.Exit(code=1)
|
|
92
|
+
finally:
|
|
93
|
+
client.close()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@eval_app.command()
|
|
97
|
+
def status(
|
|
98
|
+
task_id: str = typer.Argument(help="评估任务 ID"),
|
|
99
|
+
) -> None:
|
|
100
|
+
"""查看评估任务状态"""
|
|
101
|
+
output_fmt, quiet, verbose, server = get_global_options()
|
|
102
|
+
creds = load_credentials(server)
|
|
103
|
+
if not creds:
|
|
104
|
+
typer.echo("未登录,请先执行 tars auth login", err=True)
|
|
105
|
+
raise typer.Exit(code=2)
|
|
106
|
+
|
|
107
|
+
client = TarsAPIClient(server_url=creds.server_url, credentials=creds, verbose=verbose)
|
|
108
|
+
try:
|
|
109
|
+
resp = client.get(f"/evaluation-report/{task_id}")
|
|
110
|
+
if not resp.is_success:
|
|
111
|
+
typer.echo(f"查询失败: {resp.text}", err=True)
|
|
112
|
+
raise typer.Exit(code=2)
|
|
113
|
+
|
|
114
|
+
data = resp.json()
|
|
115
|
+
stats = data.get("test_case_stats", {})
|
|
116
|
+
result = {
|
|
117
|
+
"task_id": task_id,
|
|
118
|
+
"status": data.get("status", ""),
|
|
119
|
+
"overall_score": data.get("overall_score", 0),
|
|
120
|
+
"total_cases": stats.get("total_cases", 0),
|
|
121
|
+
"passed_cases": stats.get("passed_cases", 0),
|
|
122
|
+
"failed_cases": stats.get("failed_cases", 0),
|
|
123
|
+
}
|
|
124
|
+
render(result, output_format=output_fmt, quiet=quiet)
|
|
125
|
+
finally:
|
|
126
|
+
client.close()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from tars_cli.app import get_global_options, plugin_app
|
|
9
|
+
from tars_cli.auth import load_credentials
|
|
10
|
+
from tars_cli.client import TarsAPIClient
|
|
11
|
+
from tars_cli.hints import print_hints
|
|
12
|
+
from tars_cli.output import render
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _is_git_url(source: str) -> bool:
|
|
16
|
+
return "://" in source or source.endswith(".git")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@plugin_app.command("import")
|
|
20
|
+
def import_plugin(
|
|
21
|
+
source: str = typer.Argument(help="Git URL 或本地文件路径"),
|
|
22
|
+
) -> None:
|
|
23
|
+
"""导入 Plugin(自动识别 Git URL 或文件)"""
|
|
24
|
+
output_fmt, quiet, verbose, server = get_global_options()
|
|
25
|
+
creds = load_credentials(server)
|
|
26
|
+
if not creds:
|
|
27
|
+
typer.echo("未登录,请先执行 tars auth login", err=True)
|
|
28
|
+
raise typer.Exit(code=2)
|
|
29
|
+
|
|
30
|
+
client = TarsAPIClient(server_url=creds.server_url, credentials=creds, verbose=verbose)
|
|
31
|
+
try:
|
|
32
|
+
if _is_git_url(source):
|
|
33
|
+
resp = client.post("/plugins/import-git/validate", json={"git_url": source})
|
|
34
|
+
if not resp.is_success:
|
|
35
|
+
typer.echo(f"Git URL 验证失败: {resp.text}", err=True)
|
|
36
|
+
raise typer.Exit(code=2)
|
|
37
|
+
|
|
38
|
+
resp = client.post("/plugins/import-git", json={"git_url": source})
|
|
39
|
+
else:
|
|
40
|
+
file_path = Path(source)
|
|
41
|
+
if not file_path.exists():
|
|
42
|
+
typer.echo(f"文件不存在: {source}", err=True)
|
|
43
|
+
raise typer.Exit(code=2)
|
|
44
|
+
|
|
45
|
+
with open(file_path, "rb") as f:
|
|
46
|
+
resp = client.post("/plugins/upload/validate", files={"file": (file_path.name, f)})
|
|
47
|
+
if not resp.is_success:
|
|
48
|
+
typer.echo(f"文件验证失败: {resp.text}", err=True)
|
|
49
|
+
raise typer.Exit(code=2)
|
|
50
|
+
|
|
51
|
+
with open(file_path, "rb") as f:
|
|
52
|
+
resp = client.post("/plugins/upload", files={"file": (file_path.name, f)})
|
|
53
|
+
|
|
54
|
+
if not resp.is_success:
|
|
55
|
+
typer.echo(f"导入失败: {resp.text}", err=True)
|
|
56
|
+
raise typer.Exit(code=2)
|
|
57
|
+
|
|
58
|
+
data = resp.json()
|
|
59
|
+
plugin_id = data.get("id", "")
|
|
60
|
+
result = {
|
|
61
|
+
"plugin_id": plugin_id,
|
|
62
|
+
"name": data.get("name", ""),
|
|
63
|
+
"message": "Plugin 导入成功",
|
|
64
|
+
}
|
|
65
|
+
web_url, next_step = print_hints(
|
|
66
|
+
creds.server_url, "plugin_import", quiet=quiet, plugin_id=str(plugin_id)
|
|
67
|
+
)
|
|
68
|
+
render(result, output_format=output_fmt, quiet=quiet, web_url=web_url, next_step=next_step)
|
|
69
|
+
finally:
|
|
70
|
+
client.close()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@plugin_app.command("export-report")
|
|
74
|
+
def export_report(
|
|
75
|
+
plugin_id: str = typer.Argument(help="Plugin ID"),
|
|
76
|
+
output_path: str | None = typer.Option(None, "-o", "--output-path", help="输出文件路径"),
|
|
77
|
+
) -> None:
|
|
78
|
+
"""导出 Plugin 评估报告(HTML)"""
|
|
79
|
+
if not output_path:
|
|
80
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
81
|
+
output_path = f"report_{plugin_id[:8]}_{timestamp}.html"
|
|
82
|
+
output_fmt, quiet, verbose, server = get_global_options()
|
|
83
|
+
creds = load_credentials(server)
|
|
84
|
+
if not creds:
|
|
85
|
+
typer.echo("未登录,请先执行 tars auth login", err=True)
|
|
86
|
+
raise typer.Exit(code=2)
|
|
87
|
+
|
|
88
|
+
client = TarsAPIClient(server_url=creds.server_url, credentials=creds, verbose=verbose)
|
|
89
|
+
try:
|
|
90
|
+
resp = client.get(f"/plugins/{plugin_id}/export-report/html")
|
|
91
|
+
if not resp.is_success:
|
|
92
|
+
typer.echo(f"导出失败: {resp.text}", err=True)
|
|
93
|
+
raise typer.Exit(code=2)
|
|
94
|
+
|
|
95
|
+
Path(output_path).write_bytes(resp.content)
|
|
96
|
+
abs_path = str(Path(output_path).resolve())
|
|
97
|
+
result = {"message": "报告导出成功", "path": abs_path}
|
|
98
|
+
web_url, next_step = print_hints(
|
|
99
|
+
creds.server_url, "plugin_report", quiet=quiet, plugin_id=plugin_id
|
|
100
|
+
)
|
|
101
|
+
render(result, output_format=output_fmt, quiet=quiet, web_url=web_url, next_step=next_step)
|
|
102
|
+
finally:
|
|
103
|
+
client.close()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from tars_cli.app import get_global_options, template_app
|
|
8
|
+
from tars_cli.auth import load_credentials
|
|
9
|
+
from tars_cli.client import TarsAPIClient
|
|
10
|
+
from tars_cli.hints import print_hints
|
|
11
|
+
from tars_cli.output import render
|
|
12
|
+
|
|
13
|
+
TEMPLATE_MAP = {
|
|
14
|
+
"plan": ("evaluation-plan", "yaml"),
|
|
15
|
+
"testcase-single": ("dataset", None),
|
|
16
|
+
"testcase-multi": ("dataset/multi-turn-dataset", None),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@template_app.command()
|
|
21
|
+
def download(
|
|
22
|
+
template_type: str = typer.Argument(help="模板类型: plan, testcase-single, testcase-multi"),
|
|
23
|
+
format: str = typer.Option("yaml", "--format", "-f", help="文件格式: yaml, json"),
|
|
24
|
+
) -> None:
|
|
25
|
+
"""下载模板文件"""
|
|
26
|
+
if template_type not in TEMPLATE_MAP:
|
|
27
|
+
typer.echo(f"未知模板类型: {template_type},可选: {', '.join(TEMPLATE_MAP.keys())}", err=True)
|
|
28
|
+
raise typer.Exit(code=2)
|
|
29
|
+
|
|
30
|
+
output_fmt, quiet, verbose, server = get_global_options()
|
|
31
|
+
creds = load_credentials(server)
|
|
32
|
+
if not creds:
|
|
33
|
+
typer.echo("未登录,请先执行 tars auth login", err=True)
|
|
34
|
+
raise typer.Exit(code=2)
|
|
35
|
+
|
|
36
|
+
client = TarsAPIClient(server_url=creds.server_url, credentials=creds, verbose=verbose)
|
|
37
|
+
try:
|
|
38
|
+
path_prefix, default_format = TEMPLATE_MAP[template_type]
|
|
39
|
+
if template_type == "plan":
|
|
40
|
+
url = f"/templates/{path_prefix}/{format}"
|
|
41
|
+
elif template_type == "testcase-single":
|
|
42
|
+
url = f"/templates/{path_prefix}/{format}"
|
|
43
|
+
else:
|
|
44
|
+
url = f"/templates/{path_prefix}?format={format}"
|
|
45
|
+
|
|
46
|
+
resp = client.get(url)
|
|
47
|
+
if not resp.is_success:
|
|
48
|
+
typer.echo(f"下载失败: {resp.text}", err=True)
|
|
49
|
+
raise typer.Exit(code=2)
|
|
50
|
+
|
|
51
|
+
filename = f"{template_type}-template.{format}"
|
|
52
|
+
Path(filename).write_bytes(resp.content)
|
|
53
|
+
abs_path = str(Path(filename).resolve())
|
|
54
|
+
|
|
55
|
+
result = {"message": "模板下载成功", "file": abs_path}
|
|
56
|
+
web_url, next_step = print_hints(creds.server_url, "template_download", quiet=quiet)
|
|
57
|
+
render(result, output_format=output_fmt, quiet=quiet, web_url=web_url, next_step=next_step)
|
|
58
|
+
finally:
|
|
59
|
+
client.close()
|
tars_cli/config.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from platformdirs import user_config_dir
|
|
7
|
+
|
|
8
|
+
APP_NAME = "tars"
|
|
9
|
+
DEFAULT_SERVER_URL = "http://localhost:8001"
|
|
10
|
+
|
|
11
|
+
ENV_ACCESS_TOKEN = "TARS_ACCESS_TOKEN"
|
|
12
|
+
ENV_SERVER_URL = "TARS_SERVER_URL"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_config_dir() -> Path:
|
|
16
|
+
return Path(user_config_dir(APP_NAME))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_credentials_path() -> Path:
|
|
20
|
+
return get_config_dir() / "credentials.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_server_url(cli_server: str | None = None) -> str:
|
|
24
|
+
if cli_server:
|
|
25
|
+
return cli_server.rstrip("/")
|
|
26
|
+
env_url = os.environ.get(ENV_SERVER_URL)
|
|
27
|
+
if env_url:
|
|
28
|
+
return env_url.rstrip("/")
|
|
29
|
+
return DEFAULT_SERVER_URL
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_env_token() -> str | None:
|
|
33
|
+
return os.environ.get(ENV_ACCESS_TOKEN)
|
tars_cli/hints.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
ROUTE_MAP: dict[str, str] = {
|
|
6
|
+
"plugin_import": "/plugins/{plugin_id}",
|
|
7
|
+
"plugin_report": "/plugins/{plugin_id}/report",
|
|
8
|
+
"dataset_version": "/plugins/{plugin_id}/datasets/{version_id}",
|
|
9
|
+
"dataset_generate": "/plugins/{plugin_id}/datasets/{version_id}",
|
|
10
|
+
"dataset_upload": "/plugins/{plugin_id}/datasets/{version_id}",
|
|
11
|
+
"dataset_publish": "/plugins/{plugin_id}/datasets/{version_id}",
|
|
12
|
+
"eval_task": "/plugins/{plugin_id}/evaluations/{task_id}",
|
|
13
|
+
"eval_run": "/plugins/{plugin_id}/evaluations/{task_id}",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
NEXT_STEP_MAP: dict[str, str] = {
|
|
17
|
+
"auth_login": "tars plugin import <git_url_or_file_path>",
|
|
18
|
+
"plugin_import": "tars dataset generate {plugin_id} --agent-name <name> --conversation-type single_turn",
|
|
19
|
+
"dataset_generate": "tars eval run {plugin_id} --agent-name <name> --dataset-version {version_id}",
|
|
20
|
+
"dataset_upload": "tars eval run {plugin_id} --agent-name <name> --dataset-version {version_id}",
|
|
21
|
+
"dataset_publish": "tars eval run {plugin_id} --agent-name <name> --dataset-version {version_id}",
|
|
22
|
+
"eval_run": "tars plugin export-report {plugin_id}",
|
|
23
|
+
"template_download": "tars dataset upload {plugin_id} --agent-name <name> --plan plan.yaml --cases cases.yaml",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_web_url(server_url: str, command: str, **kwargs: str) -> str:
|
|
28
|
+
route = ROUTE_MAP.get(command, "")
|
|
29
|
+
if not route:
|
|
30
|
+
return ""
|
|
31
|
+
try:
|
|
32
|
+
path = route.format(**kwargs)
|
|
33
|
+
except KeyError:
|
|
34
|
+
return ""
|
|
35
|
+
web_base = server_url.rstrip("/").rsplit(":", 1)[0] + ":8000"
|
|
36
|
+
return f"{web_base}{path}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_next_step(command: str, **kwargs: str) -> str:
|
|
40
|
+
template = NEXT_STEP_MAP.get(command, "")
|
|
41
|
+
if not template:
|
|
42
|
+
return ""
|
|
43
|
+
try:
|
|
44
|
+
return template.format(**kwargs)
|
|
45
|
+
except KeyError:
|
|
46
|
+
return template
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def print_hints(server_url: str, command: str, quiet: bool = False, **kwargs: str) -> tuple[str, str]:
|
|
50
|
+
web_url = build_web_url(server_url, command, **kwargs)
|
|
51
|
+
next_step = build_next_step(command, **kwargs)
|
|
52
|
+
|
|
53
|
+
if not quiet:
|
|
54
|
+
if web_url:
|
|
55
|
+
typer.echo(f"\n📎 在 Web 端查看: {web_url}")
|
|
56
|
+
if next_step:
|
|
57
|
+
typer.echo(f"👉 下一步: {next_step}")
|
|
58
|
+
|
|
59
|
+
return web_url, next_step
|
tars_cli/output.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
import yaml
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
OutputFormat = str # "table" | "json" | "yaml"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def render_table(title: str, columns: list[str], rows: list[list[str]]) -> None:
|
|
17
|
+
table = Table(title=title, show_lines=True)
|
|
18
|
+
for col in columns:
|
|
19
|
+
table.add_column(col)
|
|
20
|
+
for row in rows:
|
|
21
|
+
table.add_row(*row)
|
|
22
|
+
console.print(table)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def render_json(data: dict[str, Any]) -> None:
|
|
26
|
+
typer.echo(json.dumps(data, indent=2, ensure_ascii=False, default=str))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def render_yaml(data: dict[str, Any]) -> None:
|
|
30
|
+
typer.echo(yaml.dump(data, allow_unicode=True, default_flow_style=False, sort_keys=False).rstrip())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def render(
|
|
34
|
+
data: dict[str, Any],
|
|
35
|
+
output_format: str = "table",
|
|
36
|
+
quiet: bool = False,
|
|
37
|
+
table_title: str = "",
|
|
38
|
+
table_columns: list[str] | None = None,
|
|
39
|
+
table_rows: list[list[str]] | None = None,
|
|
40
|
+
web_url: str = "",
|
|
41
|
+
next_step: str = "",
|
|
42
|
+
) -> None:
|
|
43
|
+
if output_format == "json":
|
|
44
|
+
payload = dict(data)
|
|
45
|
+
if not quiet:
|
|
46
|
+
if web_url:
|
|
47
|
+
payload["web_url"] = web_url
|
|
48
|
+
if next_step:
|
|
49
|
+
payload["next_step"] = next_step
|
|
50
|
+
render_json(payload)
|
|
51
|
+
elif output_format == "yaml":
|
|
52
|
+
payload = dict(data)
|
|
53
|
+
if not quiet:
|
|
54
|
+
if web_url:
|
|
55
|
+
payload["web_url"] = web_url
|
|
56
|
+
if next_step:
|
|
57
|
+
payload["next_step"] = next_step
|
|
58
|
+
render_yaml(payload)
|
|
59
|
+
else:
|
|
60
|
+
if table_columns and table_rows:
|
|
61
|
+
render_table(table_title, table_columns, table_rows)
|
|
62
|
+
else:
|
|
63
|
+
for k, v in data.items():
|
|
64
|
+
typer.echo(f"{k}: {v}")
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from tars_cli.utils.progress import ProgressDisplay
|
|
10
|
+
|
|
11
|
+
TERMINAL_STATES = {"completed", "failed", "partial", "timeout", "cancelled"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def poll_task(
|
|
15
|
+
request_fn: Callable[[], httpx.Response],
|
|
16
|
+
status_extractor: Callable[[dict[str, Any]], str] | None = None,
|
|
17
|
+
progress_extractor: Callable[[dict[str, Any]], int] | None = None,
|
|
18
|
+
stage_extractor: Callable[[dict[str, Any]], str] | None = None,
|
|
19
|
+
terminal_states: set[str] | None = None,
|
|
20
|
+
timeout: int = 0,
|
|
21
|
+
label: str = "处理中",
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
terminal = terminal_states or TERMINAL_STATES
|
|
24
|
+
_status_fn = status_extractor or (lambda d: d.get("status", ""))
|
|
25
|
+
_stage_fn = stage_extractor or (lambda d: d.get("current_stage", ""))
|
|
26
|
+
|
|
27
|
+
def _extract_progress(data: dict[str, Any]) -> int:
|
|
28
|
+
if progress_extractor:
|
|
29
|
+
return progress_extractor(data)
|
|
30
|
+
pct = data.get("overall_progress")
|
|
31
|
+
if pct is not None:
|
|
32
|
+
return int(pct)
|
|
33
|
+
progress_obj = data.get("progress")
|
|
34
|
+
if isinstance(progress_obj, dict):
|
|
35
|
+
pct = progress_obj.get("percentage")
|
|
36
|
+
if pct is not None:
|
|
37
|
+
return int(pct)
|
|
38
|
+
status = _status_fn(data)
|
|
39
|
+
stage_map = {"idle": 0, "pending": 5, "generating": 30, "processing": 50, "publishing": 80, "completed": 100, "failed": 100, "partial": 100}
|
|
40
|
+
return stage_map.get(status, 10)
|
|
41
|
+
|
|
42
|
+
start = time.monotonic()
|
|
43
|
+
|
|
44
|
+
with ProgressDisplay(label) as progress:
|
|
45
|
+
while True:
|
|
46
|
+
elapsed = time.monotonic() - start
|
|
47
|
+
if timeout > 0 and elapsed >= timeout:
|
|
48
|
+
typer.echo(f"任务超时({timeout}秒)", err=True)
|
|
49
|
+
raise typer.Exit(code=2)
|
|
50
|
+
|
|
51
|
+
resp = request_fn()
|
|
52
|
+
if not resp.is_success:
|
|
53
|
+
typer.echo(f"轮询失败: HTTP {resp.status_code}", err=True)
|
|
54
|
+
raise typer.Exit(code=2)
|
|
55
|
+
|
|
56
|
+
data = resp.json()
|
|
57
|
+
status = _status_fn(data)
|
|
58
|
+
pct = _extract_progress(data)
|
|
59
|
+
stage = _stage_fn(data)
|
|
60
|
+
|
|
61
|
+
progress.update(pct, stage or status)
|
|
62
|
+
|
|
63
|
+
if status in terminal:
|
|
64
|
+
return data
|
|
65
|
+
|
|
66
|
+
interval = 2.0 if elapsed < 30 else 5.0
|
|
67
|
+
time.sleep(interval)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskID, TextColumn, TimeElapsedColumn
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProgressDisplay:
|
|
13
|
+
def __init__(self, label: str = "处理中") -> None:
|
|
14
|
+
self._label = label
|
|
15
|
+
self._progress = Progress(
|
|
16
|
+
SpinnerColumn(),
|
|
17
|
+
TextColumn("[bold blue]{task.description}"),
|
|
18
|
+
BarColumn(),
|
|
19
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
20
|
+
TimeElapsedColumn(),
|
|
21
|
+
console=console,
|
|
22
|
+
)
|
|
23
|
+
self._task_id: TaskID | None = None
|
|
24
|
+
|
|
25
|
+
def __enter__(self) -> ProgressDisplay:
|
|
26
|
+
self._progress.__enter__()
|
|
27
|
+
self._task_id = self._progress.add_task(self._label, total=100)
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
def __exit__(
|
|
31
|
+
self,
|
|
32
|
+
exc_type: type[BaseException] | None,
|
|
33
|
+
exc_val: BaseException | None,
|
|
34
|
+
exc_tb: TracebackType | None,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._progress.__exit__(exc_type, exc_val, exc_tb)
|
|
37
|
+
|
|
38
|
+
def update(self, percentage: int, description: str = "") -> None:
|
|
39
|
+
if self._task_id is not None:
|
|
40
|
+
self._progress.update(
|
|
41
|
+
self._task_id,
|
|
42
|
+
completed=percentage,
|
|
43
|
+
description=description or self._label,
|
|
44
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tars-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: tars 平台命令行工具
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: typer>=0.12
|
|
7
|
+
Requires-Dist: httpx>=0.27
|
|
8
|
+
Requires-Dist: rich>=13.0
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Requires-Dist: platformdirs>=4.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-mock>=3.12; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
15
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
16
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
17
|
+
Requires-Dist: ruff>=0.3; extra == "dev"
|
|
18
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
tars_cli/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
tars_cli/__main__.py,sha256=Rsczv9C7Joo10Rh7j2bbHOlT32N3eh6vDZTvJv0rk2I,67
|
|
3
|
+
tars_cli/app.py,sha256=NU5f5I1suovHydso62FlwzILMP4BNLhyDFkJroc5-Jk,5289
|
|
4
|
+
tars_cli/auth.py,sha256=hfrTps-ESdR4y4FO_oQ1h-tBFgZuHKbV-goO3_CH6wg,1609
|
|
5
|
+
tars_cli/client.py,sha256=8FV7TAy2ShEXe12Tb8afVbJL43kqpHvStiqI8tUY4B8,4294
|
|
6
|
+
tars_cli/config.py,sha256=y7mtl9J5A2LiSMGep6Txsd00ZQiXPKkyJz4xMMs3rGY,742
|
|
7
|
+
tars_cli/hints.py,sha256=1o1Sf0f12Lcj5dywxPLYmy0enUiqx2g6SPZcJDVao0E,2268
|
|
8
|
+
tars_cli/output.py,sha256=5-xP0APs9bePTNfKvYB0Qp4ButKfhlzg1f_Q4HRxegk,1764
|
|
9
|
+
tars_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
tars_cli/commands/auth_cmd.py,sha256=stncfZVKl0qxRDMuVwuso65-D2cSXolfqVGKW3i8nTc,2557
|
|
11
|
+
tars_cli/commands/dataset_cmd.py,sha256=-jyDbiNvBc8C04NK-1RDYvxyesrkSjOrju9YBGPisYk,10939
|
|
12
|
+
tars_cli/commands/eval_cmd.py,sha256=51vI3TC1ZMXuGvSEJmA7xUvwWCVwwghQkFUu5s02GNA,4828
|
|
13
|
+
tars_cli/commands/plugin_cmd.py,sha256=uJ9jDjB64hZktsgZW_WlrgeRAcJBVywcI4OQ2PXN0d4,3935
|
|
14
|
+
tars_cli/commands/template_cmd.py,sha256=sye4f5xRS3x2ahGFOTeIgwTg-tFVY5H7gj4_5JBxKYU,2196
|
|
15
|
+
tars_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
tars_cli/utils/polling.py,sha256=L0X56mWJa5BbWvcMb0TLbi-gtW7upVPfnLUcRvdgX18,2321
|
|
17
|
+
tars_cli/utils/progress.py,sha256=8p1gZ0K_dNQpYz87Lo3l7n1Q69w7hbqBhIZNajBe2VI,1377
|
|
18
|
+
tars_cli-0.1.0.dist-info/METADATA,sha256=W_rqC0qPSQNX03xUIw1nL8vqpgvFYZ-ZzrmG1aRq1sY,578
|
|
19
|
+
tars_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
20
|
+
tars_cli-0.1.0.dist-info/entry_points.txt,sha256=tuhRjiF7IqnCY1nCxapF0i6_ZjQIIOzE6qX1V4_N8O8,42
|
|
21
|
+
tars_cli-0.1.0.dist-info/top_level.txt,sha256=adbikxYObQ-84gvQ5Fe_XM01Q5Oufhy5NPrZD1nbCVQ,9
|
|
22
|
+
tars_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tars_cli
|