tinet-agent-cli 0.1.0.dev36__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.
- taco/__init__.py +1 -0
- taco/agent_openapi/__init__.py +1 -0
- taco/agent_openapi/catalog.py +198 -0
- taco/agent_openapi/models.py +47 -0
- taco/agent_openapi/parameters.py +111 -0
- taco/agent_openapi/service.py +73 -0
- taco/aikb/__init__.py +4 -0
- taco/aikb/catalog.py +380 -0
- taco/aikb/executors.py +157 -0
- taco/aikb/file_input.py +55 -0
- taco/aikb/models.py +42 -0
- taco/aikb/parameters.py +135 -0
- taco/aikb/service.py +68 -0
- taco/assets/__init__.py +1 -0
- taco/assets/agent_openapi/catalog.json +14 -0
- taco/assets/agent_openapi/operations/conversation/list-conversations.json +67 -0
- taco/assets/agent_openapi/operations/message/list-conversation-messages.json +80 -0
- taco/assets/agent_openapi/operations/trace/list-message-traces.json +72 -0
- taco/assets/aikb/catalog.json +41 -0
- taco/assets/aikb/operations/conversation/chat-conversation-on-open.json +99 -0
- taco/assets/aikb/operations/directory/delete-directory.json +81 -0
- taco/assets/aikb/operations/directory/edit-directory.json +97 -0
- taco/assets/aikb/operations/directory/list-directory-tree.json +106 -0
- taco/assets/aikb/operations/directory/save-directory.json +109 -0
- taco/assets/aikb/operations/faq/create-faq.json +193 -0
- taco/assets/aikb/operations/faq/delete-faq.json +81 -0
- taco/assets/aikb/operations/faq/describe-faq.json +82 -0
- taco/assets/aikb/operations/faq/edit-faq.json +193 -0
- taco/assets/aikb/operations/faq/list-faqs.json +136 -0
- taco/assets/aikb/operations/file/create-file.json +145 -0
- taco/assets/aikb/operations/file/delete-files.json +110 -0
- taco/assets/aikb/operations/file/describe-file.json +82 -0
- taco/assets/aikb/operations/file/get-file-upload-url.json +154 -0
- taco/assets/aikb/operations/file/list-files.json +146 -0
- taco/assets/aikb/operations/media/describe-faq-media-url.json +98 -0
- taco/assets/aikb/operations/media/describe-file-media-url.json +90 -0
- taco/assets/aikb/operations/oss/signature-for-upload.json +85 -0
- taco/assets/aikb/operations/recycle-bin/list-recycled-items.json +151 -0
- taco/assets/aikb/operations/repository/describe-bot-repository.json +71 -0
- taco/assets/aikb/operations/repository/list-private-repositories.json +75 -0
- taco/assets/aikb/operations/repository/list-public-repositories.json +75 -0
- taco/assets/aikb/operations/search/search-knowledge-on-open.json +180 -0
- taco/assets/examples/README.md +3 -0
- taco/assets/examples/agent-basic.json +16 -0
- taco/assets/examples/agent-builtin-tool.json +28 -0
- taco/assets/examples/agent-workflow-tool.json +30 -0
- taco/assets/examples/chatflow-basic.json +24 -0
- taco/assets/examples/workflow-http.json +34 -0
- taco/assets/manifest.json +12 -0
- taco/assets/scenarios/README.md +3 -0
- taco/assets/scenarios/agent-basic.json +80 -0
- taco/assets/scenarios/agent-builtin-tool.json +116 -0
- taco/assets/scenarios/agent-workflow-tool.json +277 -0
- taco/assets/schemas/README.md +3 -0
- taco/assets/schemas/agent-tool.json +71 -0
- taco/assets/schemas/agent.json +199 -0
- taco/assets/schemas/chatflow.json +1048 -0
- taco/assets/schemas/workflow.http.json +1052 -0
- taco/assets/schemas/workflow.json +1052 -0
- taco/assets/skills/README.md +3 -0
- taco/assets/skills/taco/SKILL.md +68 -0
- taco/assets/skills/taco/manifest.json +10 -0
- taco/cli.py +127 -0
- taco/commands/__init__.py +1 -0
- taco/commands/agent.py +760 -0
- taco/commands/aikb.py +132 -0
- taco/commands/app.py +151 -0
- taco/commands/category.py +37 -0
- taco/commands/chatflow.py +183 -0
- taco/commands/credential.py +222 -0
- taco/commands/discovery.py +409 -0
- taco/commands/model.py +55 -0
- taco/commands/profile.py +124 -0
- taco/commands/tool.py +61 -0
- taco/commands/workflow.py +714 -0
- taco/core/__init__.py +1 -0
- taco/core/access_token_manager.py +98 -0
- taco/core/api_client.py +263 -0
- taco/core/assets.py +81 -0
- taco/core/config_store.py +209 -0
- taco/core/context.py +19 -0
- taco/core/developer_center_client.py +165 -0
- taco/core/errors.py +19 -0
- taco/core/file_lock.py +80 -0
- taco/core/http_headers.py +21 -0
- taco/core/openapi_signer.py +221 -0
- taco/core/output.py +72 -0
- taco/core/profile_manager.py +217 -0
- taco/core/profile_resolver.py +151 -0
- taco/core/secure_file.py +76 -0
- taco/release.py +363 -0
- taco/services/__init__.py +1 -0
- taco/services/agent_service.py +314 -0
- taco/services/app_service.py +153 -0
- taco/services/category_service.py +57 -0
- taco/services/model_service.py +117 -0
- taco/services/tool_service.py +482 -0
- taco/services/workflow_compiler.py +1665 -0
- taco/services/workflow_service.py +652 -0
- taco/services/workflow_tool_service.py +439 -0
- tinet_agent_cli-0.1.0.dev36.dist-info/METADATA +158 -0
- tinet_agent_cli-0.1.0.dev36.dist-info/RECORD +104 -0
- tinet_agent_cli-0.1.0.dev36.dist-info/WHEEL +4 -0
- tinet_agent_cli-0.1.0.dev36.dist-info/entry_points.txt +2 -0
taco/release.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import ast
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
import zipfile
|
|
13
|
+
from email import policy
|
|
14
|
+
from email.parser import BytesParser
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Iterable
|
|
17
|
+
|
|
18
|
+
from taco.core.errors import TacoError
|
|
19
|
+
from taco.core.output import failure, success, to_json_line
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
RESOURCE_GROUPS = (
|
|
23
|
+
"skills",
|
|
24
|
+
"schemas",
|
|
25
|
+
"scenarios",
|
|
26
|
+
"examples",
|
|
27
|
+
"aikb",
|
|
28
|
+
"agent_openapi",
|
|
29
|
+
)
|
|
30
|
+
REQUIRED_ASSETS = (
|
|
31
|
+
"taco/assets/skills/taco/SKILL.md",
|
|
32
|
+
"taco/assets/skills/taco/manifest.json",
|
|
33
|
+
"taco/assets/schemas/agent.json",
|
|
34
|
+
"taco/assets/schemas/agent-tool.json",
|
|
35
|
+
"taco/assets/schemas/workflow.json",
|
|
36
|
+
"taco/assets/schemas/workflow.http.json",
|
|
37
|
+
"taco/assets/scenarios/agent-basic.json",
|
|
38
|
+
"taco/assets/scenarios/agent-builtin-tool.json",
|
|
39
|
+
"taco/assets/scenarios/agent-workflow-tool.json",
|
|
40
|
+
"taco/assets/examples/agent-basic.json",
|
|
41
|
+
"taco/assets/examples/agent-builtin-tool.json",
|
|
42
|
+
"taco/assets/examples/agent-workflow-tool.json",
|
|
43
|
+
"taco/assets/examples/workflow-http.json",
|
|
44
|
+
"taco/assets/agent_openapi/catalog.json",
|
|
45
|
+
"taco/assets/agent_openapi/operations/conversation/list-conversations.json",
|
|
46
|
+
"taco/assets/agent_openapi/operations/message/list-conversation-messages.json",
|
|
47
|
+
"taco/assets/agent_openapi/operations/trace/list-message-traces.json",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _invalid_runtime_version_error() -> TacoError:
|
|
52
|
+
return TacoError(
|
|
53
|
+
code="RELEASE_ASSET_INVALID",
|
|
54
|
+
message="wheel 内的 runtime __version__ 必须是唯一字符串字面量。",
|
|
55
|
+
exit_code=7,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _runtime_version_from_source(source: bytes) -> str:
|
|
60
|
+
try:
|
|
61
|
+
module = ast.parse(source.decode("utf-8"), filename="<wheel-runtime>")
|
|
62
|
+
except (SyntaxError, UnicodeDecodeError):
|
|
63
|
+
raise _invalid_runtime_version_error() from None
|
|
64
|
+
|
|
65
|
+
assignments: list[ast.expr | None] = []
|
|
66
|
+
for statement in module.body:
|
|
67
|
+
if isinstance(statement, ast.Assign):
|
|
68
|
+
for target in statement.targets:
|
|
69
|
+
if isinstance(target, ast.Name) and target.id == "__version__":
|
|
70
|
+
assignments.append(statement.value)
|
|
71
|
+
elif (
|
|
72
|
+
isinstance(statement, ast.AnnAssign)
|
|
73
|
+
and isinstance(statement.target, ast.Name)
|
|
74
|
+
and statement.target.id == "__version__"
|
|
75
|
+
):
|
|
76
|
+
assignments.append(statement.value)
|
|
77
|
+
elif (
|
|
78
|
+
isinstance(statement, ast.AugAssign)
|
|
79
|
+
and isinstance(statement.target, ast.Name)
|
|
80
|
+
and statement.target.id == "__version__"
|
|
81
|
+
):
|
|
82
|
+
assignments.append(None)
|
|
83
|
+
|
|
84
|
+
if len(assignments) != 1:
|
|
85
|
+
raise _invalid_runtime_version_error()
|
|
86
|
+
value = assignments[0]
|
|
87
|
+
if not isinstance(value, ast.Constant) or not isinstance(value.value, str):
|
|
88
|
+
raise _invalid_runtime_version_error()
|
|
89
|
+
return value.value
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def sha256_file(path: Path) -> str:
|
|
93
|
+
digest = hashlib.sha256()
|
|
94
|
+
with path.open("rb") as file:
|
|
95
|
+
for chunk in iter(lambda: file.read(1024 * 1024), b""):
|
|
96
|
+
digest.update(chunk)
|
|
97
|
+
return digest.hexdigest()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def verify_wheel_resources(wheel: Path) -> tuple[str, ...]:
|
|
101
|
+
try:
|
|
102
|
+
with zipfile.ZipFile(wheel) as archive:
|
|
103
|
+
archive_names = archive.namelist()
|
|
104
|
+
names = set(archive_names)
|
|
105
|
+
metadata_paths = [
|
|
106
|
+
name
|
|
107
|
+
for name in archive_names
|
|
108
|
+
if len(name.split("/")) == 2
|
|
109
|
+
and name.split("/")[0].endswith(".dist-info")
|
|
110
|
+
and name.split("/")[1] == "METADATA"
|
|
111
|
+
]
|
|
112
|
+
runtime_paths = [
|
|
113
|
+
name for name in archive_names if name == "taco/__init__.py"
|
|
114
|
+
]
|
|
115
|
+
manifest_bytes = (
|
|
116
|
+
archive.read("taco/assets/manifest.json")
|
|
117
|
+
if "taco/assets/manifest.json" in names
|
|
118
|
+
else None
|
|
119
|
+
)
|
|
120
|
+
skill_manifest_bytes = (
|
|
121
|
+
archive.read("taco/assets/skills/taco/manifest.json")
|
|
122
|
+
if "taco/assets/skills/taco/manifest.json" in names
|
|
123
|
+
else None
|
|
124
|
+
)
|
|
125
|
+
metadata_bytes = (
|
|
126
|
+
archive.read(metadata_paths[0])
|
|
127
|
+
if len(metadata_paths) == 1
|
|
128
|
+
else None
|
|
129
|
+
)
|
|
130
|
+
runtime_bytes = (
|
|
131
|
+
archive.read(runtime_paths[0])
|
|
132
|
+
if len(runtime_paths) == 1
|
|
133
|
+
else None
|
|
134
|
+
)
|
|
135
|
+
except (OSError, zipfile.BadZipFile) as error:
|
|
136
|
+
raise TacoError(
|
|
137
|
+
code="RELEASE_ASSET_INVALID",
|
|
138
|
+
message="wheel 不是可读取的 ZIP 归档。",
|
|
139
|
+
exit_code=7,
|
|
140
|
+
) from error
|
|
141
|
+
|
|
142
|
+
if len(runtime_paths) != 1 or runtime_bytes is None:
|
|
143
|
+
raise _invalid_runtime_version_error()
|
|
144
|
+
runtime_version = _runtime_version_from_source(runtime_bytes)
|
|
145
|
+
|
|
146
|
+
missing: list[str] = []
|
|
147
|
+
if manifest_bytes is None:
|
|
148
|
+
missing.append("manifest.json")
|
|
149
|
+
for group in RESOURCE_GROUPS:
|
|
150
|
+
prefix = f"taco/assets/{group}/"
|
|
151
|
+
if not any(name.startswith(prefix) and name != prefix for name in names):
|
|
152
|
+
missing.append(group)
|
|
153
|
+
missing.extend(path for path in REQUIRED_ASSETS if path not in names)
|
|
154
|
+
if missing:
|
|
155
|
+
raise TacoError(
|
|
156
|
+
code="RELEASE_ASSET_MISSING",
|
|
157
|
+
message=f"wheel 缺少资源:{', '.join(missing)}",
|
|
158
|
+
exit_code=7,
|
|
159
|
+
hint="请检查 pyproject.toml 的 package data 配置后重新构建。",
|
|
160
|
+
)
|
|
161
|
+
if len(metadata_paths) != 1 or metadata_bytes is None:
|
|
162
|
+
raise TacoError(
|
|
163
|
+
code="RELEASE_ASSET_INVALID",
|
|
164
|
+
message="wheel 必须且只能包含一个 *.dist-info/METADATA。",
|
|
165
|
+
exit_code=7,
|
|
166
|
+
)
|
|
167
|
+
try:
|
|
168
|
+
manifest = json.loads(manifest_bytes)
|
|
169
|
+
skill_manifest = json.loads(skill_manifest_bytes)
|
|
170
|
+
except (UnicodeDecodeError, json.JSONDecodeError, TypeError) as error:
|
|
171
|
+
raise TacoError(
|
|
172
|
+
code="RELEASE_ASSET_INVALID",
|
|
173
|
+
message="wheel 内的资源 manifest 不是合法 JSON。",
|
|
174
|
+
exit_code=7,
|
|
175
|
+
) from error
|
|
176
|
+
resource_groups = (
|
|
177
|
+
manifest.get("resource_groups") if isinstance(manifest, dict) else None
|
|
178
|
+
)
|
|
179
|
+
if (
|
|
180
|
+
not isinstance(manifest, dict)
|
|
181
|
+
or manifest.get("format_version") != 1
|
|
182
|
+
or not isinstance(resource_groups, list)
|
|
183
|
+
or not all(isinstance(group, str) for group in resource_groups)
|
|
184
|
+
or set(resource_groups) != set(RESOURCE_GROUPS)
|
|
185
|
+
):
|
|
186
|
+
raise TacoError(
|
|
187
|
+
code="RELEASE_ASSET_INVALID",
|
|
188
|
+
message="wheel 内的资源 manifest 结构不合法。",
|
|
189
|
+
exit_code=7,
|
|
190
|
+
)
|
|
191
|
+
metadata = BytesParser(policy=policy.default).parsebytes(metadata_bytes)
|
|
192
|
+
metadata_versions = metadata.get_all("Version", [])
|
|
193
|
+
versions = [
|
|
194
|
+
runtime_version,
|
|
195
|
+
metadata_versions[0] if len(metadata_versions) == 1 else None,
|
|
196
|
+
manifest.get("cli_version"),
|
|
197
|
+
skill_manifest.get("cli_version")
|
|
198
|
+
if isinstance(skill_manifest, dict)
|
|
199
|
+
else None,
|
|
200
|
+
]
|
|
201
|
+
if (
|
|
202
|
+
not all(isinstance(version, str) and version.strip() for version in versions)
|
|
203
|
+
or len(set(versions)) != 1
|
|
204
|
+
):
|
|
205
|
+
raise TacoError(
|
|
206
|
+
code="RELEASE_ASSET_INVALID",
|
|
207
|
+
message="wheel runtime、METADATA 与资源 manifest 的版本必须非空且完全一致。",
|
|
208
|
+
exit_code=7,
|
|
209
|
+
)
|
|
210
|
+
return tuple(sorted(RESOURCE_GROUPS))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def write_checksum_file(artifacts: Iterable[Path], output: Path) -> Path:
|
|
214
|
+
lines = [
|
|
215
|
+
f"{sha256_file(path)} {path.name}"
|
|
216
|
+
for path in sorted(artifacts, key=lambda item: item.name)
|
|
217
|
+
]
|
|
218
|
+
output.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
219
|
+
return output
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def build_release(dist_dir: Path, *, upload: bool = False) -> dict[str, object]:
|
|
223
|
+
try:
|
|
224
|
+
return _build_release(dist_dir, upload=upload)
|
|
225
|
+
except OSError as error:
|
|
226
|
+
raise TacoError(
|
|
227
|
+
code="RELEASE_IO_FAILED",
|
|
228
|
+
message="无法读写发布目录或产物。",
|
|
229
|
+
exit_code=7,
|
|
230
|
+
hint=f"请检查目录权限和磁盘空间:{dist_dir}",
|
|
231
|
+
) from error
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _build_release(dist_dir: Path, *, upload: bool = False) -> dict[str, object]:
|
|
235
|
+
dist_dir.mkdir(parents=True, exist_ok=True)
|
|
236
|
+
safe_env = _environment_without_twine_credentials()
|
|
237
|
+
upload_env = _upload_environment() if upload else None
|
|
238
|
+
repository_url = os.getenv("TACO_PYPI_REPOSITORY_URL") if upload else None
|
|
239
|
+
with tempfile.TemporaryDirectory(prefix=".taco-build-", dir=dist_dir) as staging:
|
|
240
|
+
staging_dir = Path(staging)
|
|
241
|
+
_run(
|
|
242
|
+
[sys.executable, "-m", "build", "--outdir", str(staging_dir)],
|
|
243
|
+
env=safe_env,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
staged_artifacts = sorted(
|
|
247
|
+
[
|
|
248
|
+
*staging_dir.glob("*.whl"),
|
|
249
|
+
*staging_dir.glob("*.tar.gz"),
|
|
250
|
+
],
|
|
251
|
+
key=lambda path: path.name,
|
|
252
|
+
)
|
|
253
|
+
wheels = [path for path in staged_artifacts if path.suffix == ".whl"]
|
|
254
|
+
sdists = [path for path in staged_artifacts if path.name.endswith(".tar.gz")]
|
|
255
|
+
if not wheels or not sdists:
|
|
256
|
+
raise TacoError(
|
|
257
|
+
code="RELEASE_ARTIFACT_MISSING",
|
|
258
|
+
message="构建必须同时生成 wheel 和 sdist 产物。",
|
|
259
|
+
exit_code=7,
|
|
260
|
+
)
|
|
261
|
+
for wheel in wheels:
|
|
262
|
+
verify_wheel_resources(wheel)
|
|
263
|
+
|
|
264
|
+
_run(
|
|
265
|
+
[sys.executable, "-m", "twine", "check", *map(str, staged_artifacts)],
|
|
266
|
+
env=safe_env,
|
|
267
|
+
)
|
|
268
|
+
artifacts: list[Path] = []
|
|
269
|
+
for artifact in staged_artifacts:
|
|
270
|
+
destination = dist_dir / artifact.name
|
|
271
|
+
shutil.copy2(artifact, destination)
|
|
272
|
+
artifacts.append(destination)
|
|
273
|
+
|
|
274
|
+
checksum_file = write_checksum_file(artifacts, dist_dir / "SHA256SUMS")
|
|
275
|
+
|
|
276
|
+
if upload:
|
|
277
|
+
_run(
|
|
278
|
+
[
|
|
279
|
+
sys.executable,
|
|
280
|
+
"-m",
|
|
281
|
+
"twine",
|
|
282
|
+
"upload",
|
|
283
|
+
"--non-interactive",
|
|
284
|
+
"--repository-url",
|
|
285
|
+
str(repository_url),
|
|
286
|
+
*map(str, artifacts),
|
|
287
|
+
],
|
|
288
|
+
env=upload_env,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
"artifacts": [str(path) for path in artifacts],
|
|
293
|
+
"checksum_file": str(checksum_file),
|
|
294
|
+
"uploaded": upload,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _environment_without_twine_credentials() -> dict[str, str]:
|
|
299
|
+
env = dict(os.environ)
|
|
300
|
+
env.pop("TWINE_USERNAME", None)
|
|
301
|
+
env.pop("TWINE_PASSWORD", None)
|
|
302
|
+
return env
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _upload_environment() -> dict[str, str]:
|
|
306
|
+
repository_url = os.getenv("TACO_PYPI_REPOSITORY_URL")
|
|
307
|
+
username = os.getenv("TWINE_USERNAME")
|
|
308
|
+
password = os.getenv("TWINE_PASSWORD")
|
|
309
|
+
if not repository_url:
|
|
310
|
+
raise TacoError(
|
|
311
|
+
code="RELEASE_REPOSITORY_REQUIRED",
|
|
312
|
+
message="上传前必须设置 TACO_PYPI_REPOSITORY_URL。",
|
|
313
|
+
exit_code=2,
|
|
314
|
+
)
|
|
315
|
+
if not username or not password:
|
|
316
|
+
raise TacoError(
|
|
317
|
+
code="RELEASE_CREDENTIALS_REQUIRED",
|
|
318
|
+
message="非交互上传必须设置 TWINE_USERNAME 和 TWINE_PASSWORD。",
|
|
319
|
+
exit_code=2,
|
|
320
|
+
)
|
|
321
|
+
env = _environment_without_twine_credentials()
|
|
322
|
+
env.update(
|
|
323
|
+
{
|
|
324
|
+
"TWINE_USERNAME": username,
|
|
325
|
+
"TWINE_PASSWORD": password,
|
|
326
|
+
"TWINE_NON_INTERACTIVE": "1",
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
return env
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _run(command: list[str], *, env: dict[str, str] | None = None) -> None:
|
|
333
|
+
completed = subprocess.run(
|
|
334
|
+
command,
|
|
335
|
+
check=False,
|
|
336
|
+
capture_output=True,
|
|
337
|
+
text=True,
|
|
338
|
+
env=env,
|
|
339
|
+
)
|
|
340
|
+
if completed.returncode != 0:
|
|
341
|
+
raise TacoError(
|
|
342
|
+
code="RELEASE_COMMAND_FAILED",
|
|
343
|
+
message=f"发布命令执行失败,退出码 {completed.returncode}。",
|
|
344
|
+
exit_code=7,
|
|
345
|
+
hint="请在本地直接运行对应的 build/twine 命令查看详细日志。",
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def main() -> None:
|
|
350
|
+
parser = argparse.ArgumentParser(description="构建并校验 TACO 内部发布产物")
|
|
351
|
+
parser.add_argument("--dist-dir", type=Path, default=Path("dist"))
|
|
352
|
+
parser.add_argument("--upload", action="store_true")
|
|
353
|
+
args = parser.parse_args()
|
|
354
|
+
try:
|
|
355
|
+
result = build_release(args.dist_dir, upload=args.upload)
|
|
356
|
+
except TacoError as error:
|
|
357
|
+
print(to_json_line(failure(error)), file=sys.stderr)
|
|
358
|
+
raise SystemExit(error.exit_code) from error
|
|
359
|
+
print(to_json_line(success(result)))
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
if __name__ == "__main__":
|
|
363
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Business services for TACO."""
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from jsonschema import Draft202012Validator
|
|
6
|
+
|
|
7
|
+
from taco.core.api_client import ApiClient
|
|
8
|
+
from taco.core.assets import load_json_asset
|
|
9
|
+
from taco.core.errors import TacoError
|
|
10
|
+
from taco.services.category_service import CategoryService
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentService:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
api_client: ApiClient | None = None,
|
|
17
|
+
*,
|
|
18
|
+
category_service: CategoryService | None = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
self.api_client = api_client or ApiClient()
|
|
21
|
+
self.category_service = category_service or CategoryService(
|
|
22
|
+
api_client=self.api_client
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def create_agent(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
name: str,
|
|
29
|
+
category: str,
|
|
30
|
+
description: str = "",
|
|
31
|
+
profile: str | None = None,
|
|
32
|
+
) -> dict[str, Any]:
|
|
33
|
+
self.category_service.get_category(category, profile=profile)
|
|
34
|
+
app = self.api_client.post(
|
|
35
|
+
"/apps",
|
|
36
|
+
json={
|
|
37
|
+
"name": name,
|
|
38
|
+
"description": description,
|
|
39
|
+
"mode": "agent-chat",
|
|
40
|
+
"t_category_code": category,
|
|
41
|
+
"icon_type": "emoji",
|
|
42
|
+
"icon": "🤖",
|
|
43
|
+
"icon_background": "#FFEAD5",
|
|
44
|
+
},
|
|
45
|
+
profile=profile,
|
|
46
|
+
)
|
|
47
|
+
if not isinstance(app, dict):
|
|
48
|
+
raise _missing_model_config_error()
|
|
49
|
+
if not isinstance(app.get("model_config"), dict):
|
|
50
|
+
app = self.api_client.get(f"/apps/{app.get('id', '')}", profile=profile)
|
|
51
|
+
if not isinstance(app, dict) or not isinstance(
|
|
52
|
+
app.get("model_config"),
|
|
53
|
+
dict,
|
|
54
|
+
):
|
|
55
|
+
raise _missing_model_config_error()
|
|
56
|
+
return app
|
|
57
|
+
|
|
58
|
+
def pull_agent_config(self, app_id: str, *, profile: str | None = None) -> Any:
|
|
59
|
+
app_detail = self.api_client.get(f"/apps/{app_id}", profile=profile)
|
|
60
|
+
if not isinstance(app_detail, dict):
|
|
61
|
+
raise _missing_model_config_error()
|
|
62
|
+
|
|
63
|
+
model_config = app_detail.get("model_config")
|
|
64
|
+
if not isinstance(model_config, dict):
|
|
65
|
+
raise _missing_model_config_error()
|
|
66
|
+
|
|
67
|
+
return model_config
|
|
68
|
+
|
|
69
|
+
def run_agent(
|
|
70
|
+
self,
|
|
71
|
+
app_id: str,
|
|
72
|
+
*,
|
|
73
|
+
model_config: dict[str, Any],
|
|
74
|
+
query: str,
|
|
75
|
+
inputs: dict[str, Any] | None = None,
|
|
76
|
+
conversation_id: str | None = None,
|
|
77
|
+
parent_message_id: str | None = None,
|
|
78
|
+
files: list[dict[str, Any]] | None = None,
|
|
79
|
+
on_event: Callable[[dict[str, Any]], None] | None = None,
|
|
80
|
+
profile: str | None = None,
|
|
81
|
+
) -> Any:
|
|
82
|
+
events: list[dict[str, Any]] = []
|
|
83
|
+
result: dict[str, Any] = {"answer": ""}
|
|
84
|
+
completed = False
|
|
85
|
+
request_payload: dict[str, Any] = {
|
|
86
|
+
"inputs": inputs or {},
|
|
87
|
+
"query": query,
|
|
88
|
+
"model_config": model_config,
|
|
89
|
+
"response_mode": "streaming",
|
|
90
|
+
"retriever_from": "dev",
|
|
91
|
+
}
|
|
92
|
+
if conversation_id is not None:
|
|
93
|
+
request_payload["conversation_id"] = conversation_id
|
|
94
|
+
if parent_message_id is not None:
|
|
95
|
+
request_payload["parent_message_id"] = parent_message_id
|
|
96
|
+
if files is not None:
|
|
97
|
+
request_payload["files"] = files
|
|
98
|
+
|
|
99
|
+
stream = self.api_client.stream(
|
|
100
|
+
"POST",
|
|
101
|
+
f"/apps/{app_id}/chat-messages",
|
|
102
|
+
json=request_payload,
|
|
103
|
+
profile=profile,
|
|
104
|
+
)
|
|
105
|
+
try:
|
|
106
|
+
for sse_event in stream:
|
|
107
|
+
if isinstance(sse_event.data, dict):
|
|
108
|
+
event = dict(sse_event.data)
|
|
109
|
+
else:
|
|
110
|
+
event = {"data": sse_event.data}
|
|
111
|
+
if sse_event.event and "event" not in event:
|
|
112
|
+
event["event"] = sse_event.event
|
|
113
|
+
if sse_event.id is not None:
|
|
114
|
+
event["sse_id"] = sse_event.id
|
|
115
|
+
events.append(event)
|
|
116
|
+
|
|
117
|
+
if event.get("event") == "error":
|
|
118
|
+
raise TacoError(
|
|
119
|
+
code="AGENT_RUN_FAILED",
|
|
120
|
+
message=str(
|
|
121
|
+
event.get("message") or "Agent 调试流返回错误。"
|
|
122
|
+
),
|
|
123
|
+
exit_code=5,
|
|
124
|
+
hint="请检查本地 DSL、模型配置和输入参数。",
|
|
125
|
+
)
|
|
126
|
+
if on_event is not None:
|
|
127
|
+
on_event(event)
|
|
128
|
+
answer = event.get("answer")
|
|
129
|
+
if isinstance(answer, str):
|
|
130
|
+
if event.get("event") == "message_replace":
|
|
131
|
+
result["answer"] = answer
|
|
132
|
+
else:
|
|
133
|
+
result["answer"] += answer
|
|
134
|
+
for key in ("conversation_id", "message_id", "task_id"):
|
|
135
|
+
if event.get(key):
|
|
136
|
+
result[key] = event[key]
|
|
137
|
+
if isinstance(event.get("metadata"), dict):
|
|
138
|
+
result["metadata"] = event["metadata"]
|
|
139
|
+
if event.get("event") == "message_end":
|
|
140
|
+
completed = True
|
|
141
|
+
break
|
|
142
|
+
finally:
|
|
143
|
+
close = getattr(stream, "close", None)
|
|
144
|
+
if callable(close):
|
|
145
|
+
close()
|
|
146
|
+
|
|
147
|
+
if not completed:
|
|
148
|
+
raise TacoError(
|
|
149
|
+
code="AGENT_RUN_INCOMPLETE",
|
|
150
|
+
message="Agent 调试流在 message_end 前结束。",
|
|
151
|
+
exit_code=5,
|
|
152
|
+
hint="请检查网络和服务端日志后重试。",
|
|
153
|
+
)
|
|
154
|
+
result["events"] = events
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
def stop_agent(
|
|
158
|
+
self,
|
|
159
|
+
app_id: str,
|
|
160
|
+
*,
|
|
161
|
+
task_id: str,
|
|
162
|
+
profile: str | None = None,
|
|
163
|
+
) -> Any:
|
|
164
|
+
return self.api_client.post(
|
|
165
|
+
f"/apps/{app_id}/chat-messages/{task_id}/stop",
|
|
166
|
+
profile=profile,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def publish_agent(
|
|
170
|
+
self,
|
|
171
|
+
app_id: str,
|
|
172
|
+
*,
|
|
173
|
+
model_config: dict[str, Any],
|
|
174
|
+
profile: str | None = None,
|
|
175
|
+
) -> Any:
|
|
176
|
+
return self.api_client.post(
|
|
177
|
+
f"/apps/{app_id}/model-config",
|
|
178
|
+
json=model_config,
|
|
179
|
+
profile=profile,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _missing_model_config_error() -> TacoError:
|
|
184
|
+
return TacoError(
|
|
185
|
+
code="AGENT_MODEL_CONFIG_MISSING",
|
|
186
|
+
message="应用详情中缺少 model_config。",
|
|
187
|
+
exit_code=6,
|
|
188
|
+
hint="请确认应用是支持 model_config 的 Agent 类型智能体。",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def validate_agent_config(model_config: dict[str, Any]) -> dict[str, Any]:
|
|
193
|
+
validator = Draft202012Validator(load_json_asset("schemas", "agent"))
|
|
194
|
+
errors: list[str] = []
|
|
195
|
+
for error in sorted(
|
|
196
|
+
validator.iter_errors(model_config),
|
|
197
|
+
key=lambda item: tuple(str(part) for part in item.path),
|
|
198
|
+
):
|
|
199
|
+
path = _json_path(list(error.absolute_path))
|
|
200
|
+
if error.validator == "required" and isinstance(error.instance, dict):
|
|
201
|
+
missing = [
|
|
202
|
+
field
|
|
203
|
+
for field in error.validator_value
|
|
204
|
+
if field not in error.instance
|
|
205
|
+
]
|
|
206
|
+
errors.extend(
|
|
207
|
+
f"{path + '.' if path else ''}{field} 为必填项"
|
|
208
|
+
for field in missing
|
|
209
|
+
)
|
|
210
|
+
elif error.validator == "enum":
|
|
211
|
+
allowed = "、".join(
|
|
212
|
+
str(value) for value in error.validator_value if value is not None
|
|
213
|
+
)
|
|
214
|
+
errors.append(f"{path or '$'} 必须是以下值之一:{allowed}")
|
|
215
|
+
elif error.validator == "const":
|
|
216
|
+
errors.append(f"{path or '$'} 必须等于 {error.validator_value}")
|
|
217
|
+
else:
|
|
218
|
+
errors.append(f"{path or '$'}:{error.message}")
|
|
219
|
+
if errors:
|
|
220
|
+
raise _validation_error(errors)
|
|
221
|
+
return {"valid": True, "errors": []}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def validate_completion_params(
|
|
225
|
+
completion_params: dict[str, Any],
|
|
226
|
+
parameter_rules: list[dict[str, Any]],
|
|
227
|
+
) -> None:
|
|
228
|
+
errors: list[str] = []
|
|
229
|
+
if "stop" in completion_params:
|
|
230
|
+
stop = completion_params["stop"]
|
|
231
|
+
if (
|
|
232
|
+
not isinstance(stop, list)
|
|
233
|
+
or not all(isinstance(item, str) for item in stop)
|
|
234
|
+
or len(stop) > 4
|
|
235
|
+
):
|
|
236
|
+
errors.append(
|
|
237
|
+
"model.completion_params.stop 必须是最多包含 4 个字符串的数组"
|
|
238
|
+
)
|
|
239
|
+
provider_params = {
|
|
240
|
+
name: value
|
|
241
|
+
for name, value in completion_params.items()
|
|
242
|
+
if name != "stop"
|
|
243
|
+
}
|
|
244
|
+
if not parameter_rules:
|
|
245
|
+
if errors:
|
|
246
|
+
raise _validation_error(errors)
|
|
247
|
+
return
|
|
248
|
+
rules = {
|
|
249
|
+
rule.get("name"): rule
|
|
250
|
+
for rule in parameter_rules
|
|
251
|
+
if isinstance(rule, dict) and isinstance(rule.get("name"), str)
|
|
252
|
+
}
|
|
253
|
+
for name, value in provider_params.items():
|
|
254
|
+
rule = rules.get(name)
|
|
255
|
+
path = f"model.completion_params.{name}"
|
|
256
|
+
if rule is None:
|
|
257
|
+
errors.append(f"{path} 不是该模型支持的参数")
|
|
258
|
+
continue
|
|
259
|
+
expected = rule.get("type")
|
|
260
|
+
if not _matches_parameter_type(value, expected):
|
|
261
|
+
errors.append(f"{path} 类型应为 {expected}")
|
|
262
|
+
continue
|
|
263
|
+
minimum = rule.get("min")
|
|
264
|
+
maximum = rule.get("max")
|
|
265
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
266
|
+
if isinstance(minimum, (int, float)) and value < minimum:
|
|
267
|
+
errors.append(f"{path} 不能小于 {minimum}")
|
|
268
|
+
if isinstance(maximum, (int, float)) and value > maximum:
|
|
269
|
+
errors.append(f"{path} 不能大于 {maximum}")
|
|
270
|
+
options = rule.get("options")
|
|
271
|
+
if isinstance(options, list) and options:
|
|
272
|
+
allowed = [
|
|
273
|
+
option.get("value") if isinstance(option, dict) else option
|
|
274
|
+
for option in options
|
|
275
|
+
]
|
|
276
|
+
if value not in allowed:
|
|
277
|
+
errors.append(f"{path} 不在允许选项中")
|
|
278
|
+
if errors:
|
|
279
|
+
raise _validation_error(errors)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _matches_parameter_type(value: Any, expected: Any) -> bool:
|
|
283
|
+
if expected in {"string", "text"}:
|
|
284
|
+
return isinstance(value, str)
|
|
285
|
+
if expected in {"int", "integer"}:
|
|
286
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
287
|
+
if expected in {"float", "number"}:
|
|
288
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
289
|
+
if expected in {"bool", "boolean"}:
|
|
290
|
+
return isinstance(value, bool)
|
|
291
|
+
if expected in {"array", "list"}:
|
|
292
|
+
return isinstance(value, list)
|
|
293
|
+
if expected in {"object", "dict"}:
|
|
294
|
+
return isinstance(value, dict)
|
|
295
|
+
return True
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _json_path(parts: list[Any]) -> str:
|
|
299
|
+
path = ""
|
|
300
|
+
for part in parts:
|
|
301
|
+
if isinstance(part, int):
|
|
302
|
+
path += f"[{part}]"
|
|
303
|
+
else:
|
|
304
|
+
path += f".{part}" if path else str(part)
|
|
305
|
+
return path
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _validation_error(errors: list[str]) -> TacoError:
|
|
309
|
+
return TacoError(
|
|
310
|
+
code="AGENT_DSL_VALIDATION_FAILED",
|
|
311
|
+
message="Agent DSL 校验失败:" + ";".join(errors),
|
|
312
|
+
exit_code=6,
|
|
313
|
+
hint="请执行 taco schema agent -o json 后修正本地 DSL。",
|
|
314
|
+
)
|