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.
Files changed (104) hide show
  1. taco/__init__.py +1 -0
  2. taco/agent_openapi/__init__.py +1 -0
  3. taco/agent_openapi/catalog.py +198 -0
  4. taco/agent_openapi/models.py +47 -0
  5. taco/agent_openapi/parameters.py +111 -0
  6. taco/agent_openapi/service.py +73 -0
  7. taco/aikb/__init__.py +4 -0
  8. taco/aikb/catalog.py +380 -0
  9. taco/aikb/executors.py +157 -0
  10. taco/aikb/file_input.py +55 -0
  11. taco/aikb/models.py +42 -0
  12. taco/aikb/parameters.py +135 -0
  13. taco/aikb/service.py +68 -0
  14. taco/assets/__init__.py +1 -0
  15. taco/assets/agent_openapi/catalog.json +14 -0
  16. taco/assets/agent_openapi/operations/conversation/list-conversations.json +67 -0
  17. taco/assets/agent_openapi/operations/message/list-conversation-messages.json +80 -0
  18. taco/assets/agent_openapi/operations/trace/list-message-traces.json +72 -0
  19. taco/assets/aikb/catalog.json +41 -0
  20. taco/assets/aikb/operations/conversation/chat-conversation-on-open.json +99 -0
  21. taco/assets/aikb/operations/directory/delete-directory.json +81 -0
  22. taco/assets/aikb/operations/directory/edit-directory.json +97 -0
  23. taco/assets/aikb/operations/directory/list-directory-tree.json +106 -0
  24. taco/assets/aikb/operations/directory/save-directory.json +109 -0
  25. taco/assets/aikb/operations/faq/create-faq.json +193 -0
  26. taco/assets/aikb/operations/faq/delete-faq.json +81 -0
  27. taco/assets/aikb/operations/faq/describe-faq.json +82 -0
  28. taco/assets/aikb/operations/faq/edit-faq.json +193 -0
  29. taco/assets/aikb/operations/faq/list-faqs.json +136 -0
  30. taco/assets/aikb/operations/file/create-file.json +145 -0
  31. taco/assets/aikb/operations/file/delete-files.json +110 -0
  32. taco/assets/aikb/operations/file/describe-file.json +82 -0
  33. taco/assets/aikb/operations/file/get-file-upload-url.json +154 -0
  34. taco/assets/aikb/operations/file/list-files.json +146 -0
  35. taco/assets/aikb/operations/media/describe-faq-media-url.json +98 -0
  36. taco/assets/aikb/operations/media/describe-file-media-url.json +90 -0
  37. taco/assets/aikb/operations/oss/signature-for-upload.json +85 -0
  38. taco/assets/aikb/operations/recycle-bin/list-recycled-items.json +151 -0
  39. taco/assets/aikb/operations/repository/describe-bot-repository.json +71 -0
  40. taco/assets/aikb/operations/repository/list-private-repositories.json +75 -0
  41. taco/assets/aikb/operations/repository/list-public-repositories.json +75 -0
  42. taco/assets/aikb/operations/search/search-knowledge-on-open.json +180 -0
  43. taco/assets/examples/README.md +3 -0
  44. taco/assets/examples/agent-basic.json +16 -0
  45. taco/assets/examples/agent-builtin-tool.json +28 -0
  46. taco/assets/examples/agent-workflow-tool.json +30 -0
  47. taco/assets/examples/chatflow-basic.json +24 -0
  48. taco/assets/examples/workflow-http.json +34 -0
  49. taco/assets/manifest.json +12 -0
  50. taco/assets/scenarios/README.md +3 -0
  51. taco/assets/scenarios/agent-basic.json +80 -0
  52. taco/assets/scenarios/agent-builtin-tool.json +116 -0
  53. taco/assets/scenarios/agent-workflow-tool.json +277 -0
  54. taco/assets/schemas/README.md +3 -0
  55. taco/assets/schemas/agent-tool.json +71 -0
  56. taco/assets/schemas/agent.json +199 -0
  57. taco/assets/schemas/chatflow.json +1048 -0
  58. taco/assets/schemas/workflow.http.json +1052 -0
  59. taco/assets/schemas/workflow.json +1052 -0
  60. taco/assets/skills/README.md +3 -0
  61. taco/assets/skills/taco/SKILL.md +68 -0
  62. taco/assets/skills/taco/manifest.json +10 -0
  63. taco/cli.py +127 -0
  64. taco/commands/__init__.py +1 -0
  65. taco/commands/agent.py +760 -0
  66. taco/commands/aikb.py +132 -0
  67. taco/commands/app.py +151 -0
  68. taco/commands/category.py +37 -0
  69. taco/commands/chatflow.py +183 -0
  70. taco/commands/credential.py +222 -0
  71. taco/commands/discovery.py +409 -0
  72. taco/commands/model.py +55 -0
  73. taco/commands/profile.py +124 -0
  74. taco/commands/tool.py +61 -0
  75. taco/commands/workflow.py +714 -0
  76. taco/core/__init__.py +1 -0
  77. taco/core/access_token_manager.py +98 -0
  78. taco/core/api_client.py +263 -0
  79. taco/core/assets.py +81 -0
  80. taco/core/config_store.py +209 -0
  81. taco/core/context.py +19 -0
  82. taco/core/developer_center_client.py +165 -0
  83. taco/core/errors.py +19 -0
  84. taco/core/file_lock.py +80 -0
  85. taco/core/http_headers.py +21 -0
  86. taco/core/openapi_signer.py +221 -0
  87. taco/core/output.py +72 -0
  88. taco/core/profile_manager.py +217 -0
  89. taco/core/profile_resolver.py +151 -0
  90. taco/core/secure_file.py +76 -0
  91. taco/release.py +363 -0
  92. taco/services/__init__.py +1 -0
  93. taco/services/agent_service.py +314 -0
  94. taco/services/app_service.py +153 -0
  95. taco/services/category_service.py +57 -0
  96. taco/services/model_service.py +117 -0
  97. taco/services/tool_service.py +482 -0
  98. taco/services/workflow_compiler.py +1665 -0
  99. taco/services/workflow_service.py +652 -0
  100. taco/services/workflow_tool_service.py +439 -0
  101. tinet_agent_cli-0.1.0.dev36.dist-info/METADATA +158 -0
  102. tinet_agent_cli-0.1.0.dev36.dist-info/RECORD +104 -0
  103. tinet_agent_cli-0.1.0.dev36.dist-info/WHEEL +4 -0
  104. 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
+ )