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/commands/agent.py
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from json import JSONDecodeError
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
from uuid import UUID, uuid4
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from taco.commands.app import app_app
|
|
14
|
+
from taco.commands.category import category_app
|
|
15
|
+
from taco.commands.chatflow import chatflow_app
|
|
16
|
+
from taco.commands.credential import credential_app
|
|
17
|
+
from taco.commands.discovery import example_app, scenario_app, schema_app, skills_app
|
|
18
|
+
from taco.commands.model import model_app
|
|
19
|
+
from taco.commands.tool import tool_app
|
|
20
|
+
from taco.commands.workflow import workflow_app
|
|
21
|
+
from taco.agent_openapi.catalog import load_catalog, operation_not_found
|
|
22
|
+
from taco.agent_openapi.models import thaw
|
|
23
|
+
from taco.agent_openapi.parameters import parse_parameters
|
|
24
|
+
from taco.agent_openapi.service import AgentOpenApiService
|
|
25
|
+
from taco.core.context import get_cli_context
|
|
26
|
+
from taco.core.errors import TacoError
|
|
27
|
+
from taco.core.output import emit_success, exit_with_error, to_json_line
|
|
28
|
+
from taco.services.agent_service import (
|
|
29
|
+
AgentService,
|
|
30
|
+
validate_agent_config,
|
|
31
|
+
validate_completion_params,
|
|
32
|
+
)
|
|
33
|
+
from taco.services.model_service import ModelService
|
|
34
|
+
from taco.services.tool_service import ToolService
|
|
35
|
+
from taco.services.workflow_tool_service import WorkflowToolService
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
agent_app = typer.Typer(help="Agent 智能体管理")
|
|
39
|
+
agent_tool_app = typer.Typer(help="本地 Agent DSL 工具挂载")
|
|
40
|
+
agent_operation_app = typer.Typer(help="发现 Agent 数据 OpenAPI operation")
|
|
41
|
+
agent_app.add_typer(agent_tool_app, name="tool")
|
|
42
|
+
agent_app.add_typer(agent_operation_app, name="operation")
|
|
43
|
+
agent_app.add_typer(app_app, name="app")
|
|
44
|
+
agent_app.add_typer(category_app, name="category")
|
|
45
|
+
agent_app.add_typer(model_app, name="model")
|
|
46
|
+
agent_app.add_typer(workflow_app, name="workflow")
|
|
47
|
+
agent_app.add_typer(chatflow_app, name="chatflow")
|
|
48
|
+
agent_app.add_typer(example_app, name="example")
|
|
49
|
+
agent_app.add_typer(scenario_app, name="scenario")
|
|
50
|
+
agent_app.add_typer(skills_app, name="skills")
|
|
51
|
+
agent_app.add_typer(schema_app, name="schema")
|
|
52
|
+
agent_tool_app.add_typer(tool_app, name="catalog")
|
|
53
|
+
agent_tool_app.add_typer(credential_app, name="credential")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@agent_operation_app.command("list", help="列出 Agent 数据 OpenAPI operation。")
|
|
57
|
+
def list_agent_operations(
|
|
58
|
+
ctx: typer.Context,
|
|
59
|
+
group: str | None = typer.Option(None, "--group", help="按分组筛选。"),
|
|
60
|
+
) -> None:
|
|
61
|
+
operations = [
|
|
62
|
+
_agent_operation_summary(item)
|
|
63
|
+
for item in load_catalog().operations
|
|
64
|
+
if group is None or item.group == group
|
|
65
|
+
]
|
|
66
|
+
emit_success(ctx, operations, json_default=True)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@agent_operation_app.command("search", help="按关键词搜索 Agent 数据 OpenAPI operation。")
|
|
70
|
+
def search_agent_operations(ctx: typer.Context, query: str = typer.Argument(...)) -> None:
|
|
71
|
+
needle = query.casefold()
|
|
72
|
+
matches = []
|
|
73
|
+
for operation in load_catalog().operations:
|
|
74
|
+
haystack = " ".join(
|
|
75
|
+
[operation.id, operation.title, operation.description, *operation.keywords]
|
|
76
|
+
).casefold()
|
|
77
|
+
if needle in haystack:
|
|
78
|
+
matches.append(_agent_operation_summary(operation))
|
|
79
|
+
emit_success(ctx, matches[:10], json_default=True)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@agent_operation_app.command("describe", help="查看 Agent 数据 OpenAPI operation 完整契约。")
|
|
83
|
+
def describe_agent_operation(
|
|
84
|
+
ctx: typer.Context,
|
|
85
|
+
operation_id: str = typer.Argument(...),
|
|
86
|
+
) -> None:
|
|
87
|
+
operation = load_catalog().by_id.get(operation_id)
|
|
88
|
+
if operation is None:
|
|
89
|
+
exit_with_error(ctx, operation_not_found(operation_id), json_default=True)
|
|
90
|
+
emit_success(
|
|
91
|
+
ctx,
|
|
92
|
+
{
|
|
93
|
+
**_agent_operation_summary(operation),
|
|
94
|
+
"description": operation.description,
|
|
95
|
+
"keywords": list(operation.keywords),
|
|
96
|
+
"http": thaw(operation.http),
|
|
97
|
+
"response": thaw(operation.response),
|
|
98
|
+
"risk": thaw(operation.risk),
|
|
99
|
+
"execution": thaw(operation.execution),
|
|
100
|
+
"examples": thaw(operation.examples),
|
|
101
|
+
},
|
|
102
|
+
json_default=True,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@agent_app.command("call", help="校验参数并执行 Agent 数据 OpenAPI operation。")
|
|
107
|
+
def call_agent_operation(
|
|
108
|
+
ctx: typer.Context,
|
|
109
|
+
operation_id: str = typer.Argument(...),
|
|
110
|
+
param: list[str] | None = typer.Option(
|
|
111
|
+
None,
|
|
112
|
+
"--param",
|
|
113
|
+
help="语义参数 key=value,可重复。",
|
|
114
|
+
),
|
|
115
|
+
params_file: Path | None = typer.Option(
|
|
116
|
+
None,
|
|
117
|
+
"--params-file",
|
|
118
|
+
help="语义参数 JSON 文件。",
|
|
119
|
+
),
|
|
120
|
+
body_file: Path | None = typer.Option(
|
|
121
|
+
None,
|
|
122
|
+
"--body-file",
|
|
123
|
+
help="JSON body 文件。",
|
|
124
|
+
),
|
|
125
|
+
) -> None:
|
|
126
|
+
operation = load_catalog().by_id.get(operation_id)
|
|
127
|
+
if operation is None:
|
|
128
|
+
exit_with_error(ctx, operation_not_found(operation_id), json_default=True)
|
|
129
|
+
try:
|
|
130
|
+
query, body = parse_parameters(
|
|
131
|
+
operation,
|
|
132
|
+
params=param,
|
|
133
|
+
params_file=params_file,
|
|
134
|
+
body_file=body_file,
|
|
135
|
+
)
|
|
136
|
+
result = AgentOpenApiService().call(
|
|
137
|
+
operation_id,
|
|
138
|
+
query=query,
|
|
139
|
+
body=body,
|
|
140
|
+
profile=get_cli_context(ctx).profile,
|
|
141
|
+
)
|
|
142
|
+
except TacoError as error:
|
|
143
|
+
exit_with_error(ctx, error, json_default=True)
|
|
144
|
+
emit_success(ctx, result, json_default=True)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _agent_operation_summary(operation) -> dict[str, Any]:
|
|
148
|
+
return {
|
|
149
|
+
"id": operation.id,
|
|
150
|
+
"title": operation.title,
|
|
151
|
+
"group": operation.group,
|
|
152
|
+
"method": operation.http["method"],
|
|
153
|
+
"path": operation.http["path"],
|
|
154
|
+
"risk": operation.risk["level"],
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@agent_app.command("create", help="创建 Agent 并把服务端默认配置写入本地 DSL。")
|
|
159
|
+
def create_agent(
|
|
160
|
+
ctx: typer.Context,
|
|
161
|
+
name: str = typer.Option(..., "--name", help="智能体名称。"),
|
|
162
|
+
category: str = typer.Option(..., "--category", help="服务端分类 code。"),
|
|
163
|
+
description: str = typer.Option("", "--description", help="智能体描述。"),
|
|
164
|
+
output: Path | None = typer.Option(
|
|
165
|
+
None,
|
|
166
|
+
"--output",
|
|
167
|
+
help="本地 Agent DSL JSON 输出路径,默认生成唯一文件名。",
|
|
168
|
+
),
|
|
169
|
+
yes: bool = typer.Option(False, "--yes", help="确认创建远端 Agent。"),
|
|
170
|
+
overwrite: bool = typer.Option(
|
|
171
|
+
False,
|
|
172
|
+
"--overwrite",
|
|
173
|
+
help="允许覆盖已存在的本地 DSL,仍需 --yes。",
|
|
174
|
+
),
|
|
175
|
+
) -> None:
|
|
176
|
+
cli_context = get_cli_context(ctx)
|
|
177
|
+
if not yes:
|
|
178
|
+
exit_with_error(
|
|
179
|
+
ctx,
|
|
180
|
+
TacoError(
|
|
181
|
+
code="AGENT_CREATE_CONFIRM_REQUIRED",
|
|
182
|
+
message="创建远端 Agent 前必须显式传入 --yes。",
|
|
183
|
+
exit_code=2,
|
|
184
|
+
),
|
|
185
|
+
json_default=True,
|
|
186
|
+
)
|
|
187
|
+
try:
|
|
188
|
+
target = output or Path(f"taco-agent-{uuid4()}.json")
|
|
189
|
+
_prepare_agent_output(target, overwrite=overwrite)
|
|
190
|
+
app = AgentService().create_agent(
|
|
191
|
+
name=name,
|
|
192
|
+
category=category,
|
|
193
|
+
description=description,
|
|
194
|
+
profile=cli_context.profile,
|
|
195
|
+
)
|
|
196
|
+
app_id = app.get("id")
|
|
197
|
+
model_config = app.get("model_config")
|
|
198
|
+
if not isinstance(app_id, str) or not isinstance(model_config, dict):
|
|
199
|
+
raise TacoError(
|
|
200
|
+
code="AGENT_CREATE_RESPONSE_INVALID",
|
|
201
|
+
message="创建响应缺少应用 ID 或 model_config。",
|
|
202
|
+
exit_code=4,
|
|
203
|
+
)
|
|
204
|
+
_write_json_file(target, model_config)
|
|
205
|
+
except TacoError as error:
|
|
206
|
+
exit_with_error(ctx, error, json_default=True)
|
|
207
|
+
emit_success(
|
|
208
|
+
ctx,
|
|
209
|
+
{
|
|
210
|
+
"app_id": app_id,
|
|
211
|
+
"name": app.get("name"),
|
|
212
|
+
"category": category,
|
|
213
|
+
"path": str(target),
|
|
214
|
+
},
|
|
215
|
+
json_default=True,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@agent_app.command("pull", help="从服务端拉取 Agent model_config 到本地 DSL。")
|
|
220
|
+
def pull_agent(
|
|
221
|
+
ctx: typer.Context,
|
|
222
|
+
app_id: str = typer.Argument(..., help="应用 ID。"),
|
|
223
|
+
output: Path = typer.Option(..., "--output", help="本地 Agent DSL JSON 输出路径。"),
|
|
224
|
+
overwrite: bool = typer.Option(
|
|
225
|
+
False,
|
|
226
|
+
"--overwrite",
|
|
227
|
+
help="允许覆盖已存在的本地 DSL。",
|
|
228
|
+
),
|
|
229
|
+
) -> None:
|
|
230
|
+
cli_context = get_cli_context(ctx)
|
|
231
|
+
try:
|
|
232
|
+
_prepare_agent_output(output, overwrite=overwrite)
|
|
233
|
+
model_config = AgentService().pull_agent_config(
|
|
234
|
+
app_id,
|
|
235
|
+
profile=cli_context.profile,
|
|
236
|
+
)
|
|
237
|
+
_write_json_file(output, model_config)
|
|
238
|
+
except TacoError as error:
|
|
239
|
+
exit_with_error(ctx, error, json_default=True)
|
|
240
|
+
|
|
241
|
+
emit_success(ctx, {"path": str(output)}, json_default=True)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@agent_app.command("validate", help="校验本地 Agent DSL 及可选分类模型契约。")
|
|
245
|
+
def validate_agent(
|
|
246
|
+
ctx: typer.Context,
|
|
247
|
+
dsl: Path = typer.Option(..., "--dsl", help="本地 Agent DSL JSON 文件。"),
|
|
248
|
+
category: str | None = typer.Option(
|
|
249
|
+
None,
|
|
250
|
+
"--category",
|
|
251
|
+
help="通过服务端确认模型支持该分类。",
|
|
252
|
+
),
|
|
253
|
+
) -> None:
|
|
254
|
+
cli_context = get_cli_context(ctx)
|
|
255
|
+
try:
|
|
256
|
+
model_config = _read_json_object(dsl)
|
|
257
|
+
result = validate_agent_config(model_config)
|
|
258
|
+
parameter_rules: list[dict[str, Any]] = []
|
|
259
|
+
if category is not None:
|
|
260
|
+
model = model_config["model"]
|
|
261
|
+
model_description = ModelService().describe_model(
|
|
262
|
+
provider=model["provider"],
|
|
263
|
+
model=model["name"],
|
|
264
|
+
category=category,
|
|
265
|
+
profile=cli_context.profile,
|
|
266
|
+
)
|
|
267
|
+
parameter_rules = model_description["parameter_rules"]
|
|
268
|
+
result["model_verified"] = True
|
|
269
|
+
validate_completion_params(
|
|
270
|
+
model_config["model"]["completion_params"],
|
|
271
|
+
parameter_rules,
|
|
272
|
+
)
|
|
273
|
+
except TacoError as error:
|
|
274
|
+
exit_with_error(ctx, error, json_default=True)
|
|
275
|
+
emit_success(ctx, result, json_default=True)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@agent_app.command("run", help="使用本地 DSL 通过 SSE 调试 Agent。")
|
|
279
|
+
def run_agent(
|
|
280
|
+
ctx: typer.Context,
|
|
281
|
+
app_id: str = typer.Argument(..., help="应用 ID。"),
|
|
282
|
+
dsl: Path = typer.Option(..., "--dsl", help="本地 Agent DSL JSON 文件。"),
|
|
283
|
+
query: str = typer.Option(..., "--query", help="调试问题。"),
|
|
284
|
+
inputs: Path | None = typer.Option(None, "--inputs", help="调试 inputs JSON 文件。"),
|
|
285
|
+
conversation_id: str | None = typer.Option(
|
|
286
|
+
None,
|
|
287
|
+
"--conversation-id",
|
|
288
|
+
help="续聊会话 ID。",
|
|
289
|
+
),
|
|
290
|
+
parent_message_id: str | None = typer.Option(
|
|
291
|
+
None,
|
|
292
|
+
"--parent-message-id",
|
|
293
|
+
help="父消息 ID。",
|
|
294
|
+
),
|
|
295
|
+
files: Path | None = typer.Option(
|
|
296
|
+
None,
|
|
297
|
+
"--files",
|
|
298
|
+
help="Dify 消息文件数组 JSON。",
|
|
299
|
+
),
|
|
300
|
+
stream_events: bool = typer.Option(
|
|
301
|
+
False,
|
|
302
|
+
"--stream-events",
|
|
303
|
+
help="实时输出 NDJSON 事件,便于获取 task_id 后调用 stop。",
|
|
304
|
+
),
|
|
305
|
+
) -> None:
|
|
306
|
+
cli_context = get_cli_context(ctx)
|
|
307
|
+
try:
|
|
308
|
+
model_config = _read_json_object(dsl)
|
|
309
|
+
input_payload = _read_json_object(inputs) if inputs else {}
|
|
310
|
+
run_options: dict[str, Any] = {
|
|
311
|
+
"model_config": model_config,
|
|
312
|
+
"query": query,
|
|
313
|
+
"inputs": input_payload,
|
|
314
|
+
"profile": cli_context.profile,
|
|
315
|
+
}
|
|
316
|
+
if conversation_id is not None:
|
|
317
|
+
run_options["conversation_id"] = conversation_id
|
|
318
|
+
if parent_message_id is not None:
|
|
319
|
+
run_options["parent_message_id"] = parent_message_id
|
|
320
|
+
if files is not None:
|
|
321
|
+
run_options["files"] = _read_json_array(files)
|
|
322
|
+
if stream_events:
|
|
323
|
+
run_options["on_event"] = lambda event: typer.echo(
|
|
324
|
+
to_json_line({"type": "event", "event": event})
|
|
325
|
+
)
|
|
326
|
+
result = AgentService().run_agent(app_id, **run_options)
|
|
327
|
+
except TacoError as error:
|
|
328
|
+
exit_with_error(ctx, error, json_default=True)
|
|
329
|
+
|
|
330
|
+
emit_success(ctx, result, json_default=True)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@agent_app.command("stop", help="使用 task_id 停止正在运行的 Agent 调试任务。")
|
|
334
|
+
def stop_agent(
|
|
335
|
+
ctx: typer.Context,
|
|
336
|
+
app_id: str = typer.Argument(..., help="应用 ID。"),
|
|
337
|
+
task_id: str = typer.Option(..., "--task-id", help="SSE 返回的任务 ID。"),
|
|
338
|
+
) -> None:
|
|
339
|
+
cli_context = get_cli_context(ctx)
|
|
340
|
+
try:
|
|
341
|
+
result = AgentService().stop_agent(
|
|
342
|
+
app_id,
|
|
343
|
+
task_id=task_id,
|
|
344
|
+
profile=cli_context.profile,
|
|
345
|
+
)
|
|
346
|
+
except TacoError as error:
|
|
347
|
+
exit_with_error(ctx, error, json_default=True)
|
|
348
|
+
emit_success(ctx, result, json_default=True)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@agent_tool_app.command("list", help="列出本地 Agent DSL 已挂载工具。")
|
|
352
|
+
def list_agent_tools(
|
|
353
|
+
ctx: typer.Context,
|
|
354
|
+
dsl: Path = typer.Option(..., "--dsl", help="本地 Agent DSL JSON 文件。"),
|
|
355
|
+
) -> None:
|
|
356
|
+
try:
|
|
357
|
+
model_config = _read_json_object(dsl)
|
|
358
|
+
tools = _agent_tools(model_config)
|
|
359
|
+
except TacoError as error:
|
|
360
|
+
exit_with_error(ctx, error, json_default=True)
|
|
361
|
+
emit_success(ctx, {"path": str(dsl), "items": tools}, json_default=True)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@agent_tool_app.command("add", help="发现工具并挂载到本地 Agent DSL。")
|
|
365
|
+
def add_agent_tool(
|
|
366
|
+
ctx: typer.Context,
|
|
367
|
+
tool_type: str = typer.Option(
|
|
368
|
+
"builtin",
|
|
369
|
+
"--type",
|
|
370
|
+
help="工具类型:builtin|workflow;默认 builtin。",
|
|
371
|
+
),
|
|
372
|
+
dsl: Path = typer.Option(..., "--dsl", help="本地 Agent DSL JSON 文件。"),
|
|
373
|
+
provider: str = typer.Option(
|
|
374
|
+
...,
|
|
375
|
+
"--provider",
|
|
376
|
+
help="builtin Provider ID;workflow 时为 workflow_tool_id(不是 app ID)。",
|
|
377
|
+
),
|
|
378
|
+
tool: str = typer.Option(
|
|
379
|
+
...,
|
|
380
|
+
"--tool",
|
|
381
|
+
help="builtin action 名称或 Workflow Tool 机器名。",
|
|
382
|
+
),
|
|
383
|
+
parameters: Path | None = typer.Option(
|
|
384
|
+
None,
|
|
385
|
+
"--parameters",
|
|
386
|
+
help="工具 form 参数 JSON 对象,不得包含 Provider 凭据。",
|
|
387
|
+
),
|
|
388
|
+
replace: bool = typer.Option(
|
|
389
|
+
False,
|
|
390
|
+
"--replace",
|
|
391
|
+
help="替换同类型、同 Provider、同名称的已挂载工具。",
|
|
392
|
+
),
|
|
393
|
+
yes: bool = typer.Option(False, "--yes", help="确认修改本地 DSL。"),
|
|
394
|
+
) -> None:
|
|
395
|
+
_require_agent_tool_confirmation(ctx, yes)
|
|
396
|
+
if tool_type not in {"builtin", "workflow"}:
|
|
397
|
+
exit_with_error(
|
|
398
|
+
ctx,
|
|
399
|
+
TacoError(
|
|
400
|
+
code="AGENT_TOOL_TYPE_INVALID",
|
|
401
|
+
message="Agent 工具类型必须是 builtin 或 workflow。",
|
|
402
|
+
exit_code=2,
|
|
403
|
+
),
|
|
404
|
+
json_default=True,
|
|
405
|
+
)
|
|
406
|
+
cli_context = get_cli_context(ctx)
|
|
407
|
+
try:
|
|
408
|
+
_ensure_mutable_agent_dsl(dsl)
|
|
409
|
+
model_config = _read_json_object(dsl)
|
|
410
|
+
tools = _agent_tools(model_config)
|
|
411
|
+
parameter_values = _read_json_object(parameters) if parameters else {}
|
|
412
|
+
if tool_type == "builtin":
|
|
413
|
+
resolved = ToolService().resolve_agent_tool(
|
|
414
|
+
provider=provider,
|
|
415
|
+
tool=tool,
|
|
416
|
+
parameters=parameter_values,
|
|
417
|
+
profile=cli_context.profile,
|
|
418
|
+
)
|
|
419
|
+
else:
|
|
420
|
+
resolved = WorkflowToolService().resolve_agent_tool(
|
|
421
|
+
workflow_tool_id=provider,
|
|
422
|
+
tool_name=tool,
|
|
423
|
+
parameters=parameter_values,
|
|
424
|
+
profile=cli_context.profile,
|
|
425
|
+
)
|
|
426
|
+
matching_indices = [
|
|
427
|
+
position
|
|
428
|
+
for position, item in enumerate(tools)
|
|
429
|
+
if _same_agent_tool(item, tool_type, provider, tool)
|
|
430
|
+
]
|
|
431
|
+
if matching_indices and not replace:
|
|
432
|
+
raise TacoError(
|
|
433
|
+
code="AGENT_TOOL_EXISTS",
|
|
434
|
+
message="本地 DSL 已挂载同一个工具。",
|
|
435
|
+
exit_code=6,
|
|
436
|
+
hint="确认替换后传入 --replace --yes。",
|
|
437
|
+
)
|
|
438
|
+
replaced_count = len(matching_indices)
|
|
439
|
+
if not matching_indices:
|
|
440
|
+
tools.append(resolved)
|
|
441
|
+
else:
|
|
442
|
+
first_index = matching_indices[0]
|
|
443
|
+
tools[:] = [
|
|
444
|
+
item
|
|
445
|
+
for item in tools
|
|
446
|
+
if not _same_agent_tool(item, tool_type, provider, tool)
|
|
447
|
+
]
|
|
448
|
+
tools.insert(first_index, resolved)
|
|
449
|
+
validate_agent_config(model_config)
|
|
450
|
+
_write_json_file(dsl, model_config)
|
|
451
|
+
except TacoError as error:
|
|
452
|
+
exit_with_error(ctx, error, json_default=True)
|
|
453
|
+
emit_success(
|
|
454
|
+
ctx,
|
|
455
|
+
{
|
|
456
|
+
"path": str(dsl),
|
|
457
|
+
"tool": resolved,
|
|
458
|
+
"tools_count": len(tools),
|
|
459
|
+
"replaced_count": replaced_count,
|
|
460
|
+
},
|
|
461
|
+
json_default=True,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@agent_tool_app.command("remove", help="从本地 Agent DSL 移除已挂载工具。")
|
|
466
|
+
def remove_agent_tool(
|
|
467
|
+
ctx: typer.Context,
|
|
468
|
+
tool_type: str = typer.Option(
|
|
469
|
+
"builtin",
|
|
470
|
+
"--type",
|
|
471
|
+
help="工具类型:builtin|workflow;默认 builtin。",
|
|
472
|
+
),
|
|
473
|
+
dsl: Path = typer.Option(..., "--dsl", help="本地 Agent DSL JSON 文件。"),
|
|
474
|
+
provider: str = typer.Option(
|
|
475
|
+
...,
|
|
476
|
+
"--provider",
|
|
477
|
+
help="builtin Provider ID;workflow 时为 workflow_tool_id(不是 app ID)。",
|
|
478
|
+
),
|
|
479
|
+
tool: str = typer.Option(
|
|
480
|
+
...,
|
|
481
|
+
"--tool",
|
|
482
|
+
help="builtin action 名称或 Workflow Tool 机器名。",
|
|
483
|
+
),
|
|
484
|
+
yes: bool = typer.Option(False, "--yes", help="确认修改本地 DSL。"),
|
|
485
|
+
) -> None:
|
|
486
|
+
_require_agent_tool_confirmation(ctx, yes)
|
|
487
|
+
if tool_type not in {"builtin", "workflow"}:
|
|
488
|
+
exit_with_error(
|
|
489
|
+
ctx,
|
|
490
|
+
TacoError(
|
|
491
|
+
code="AGENT_TOOL_TYPE_INVALID",
|
|
492
|
+
message="Agent 工具类型必须是 builtin 或 workflow。",
|
|
493
|
+
exit_code=2,
|
|
494
|
+
),
|
|
495
|
+
json_default=True,
|
|
496
|
+
)
|
|
497
|
+
try:
|
|
498
|
+
_ensure_mutable_agent_dsl(dsl)
|
|
499
|
+
model_config = _read_json_object(dsl)
|
|
500
|
+
tools = _agent_tools(model_config)
|
|
501
|
+
remaining = [
|
|
502
|
+
item
|
|
503
|
+
for item in tools
|
|
504
|
+
if not _same_agent_tool(item, tool_type, provider, tool)
|
|
505
|
+
]
|
|
506
|
+
if len(remaining) == len(tools):
|
|
507
|
+
raise TacoError(
|
|
508
|
+
code="AGENT_TOOL_NOT_FOUND",
|
|
509
|
+
message="本地 DSL 未挂载指定工具。",
|
|
510
|
+
exit_code=6,
|
|
511
|
+
)
|
|
512
|
+
model_config["agent_mode"]["tools"] = remaining
|
|
513
|
+
validate_agent_config(model_config)
|
|
514
|
+
_write_json_file(dsl, model_config)
|
|
515
|
+
except TacoError as error:
|
|
516
|
+
exit_with_error(ctx, error, json_default=True)
|
|
517
|
+
emit_success(
|
|
518
|
+
ctx,
|
|
519
|
+
{"path": str(dsl), "tools_count": len(remaining)},
|
|
520
|
+
json_default=True,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
@agent_app.command("publish", help="发布本地 Agent DSL 并可在成功后删除文件。")
|
|
525
|
+
def publish_agent(
|
|
526
|
+
ctx: typer.Context,
|
|
527
|
+
app_id: str = typer.Argument(..., help="应用 ID。"),
|
|
528
|
+
dsl: Path = typer.Option(..., "--dsl", help="本地 Agent DSL JSON 文件。"),
|
|
529
|
+
yes: bool = typer.Option(False, "--yes", help="确认发布本地 DSL。"),
|
|
530
|
+
keep_dsl: bool = typer.Option(False, "--keep-dsl", help="发布成功后保留本地 DSL 文件。"),
|
|
531
|
+
) -> None:
|
|
532
|
+
cli_context = get_cli_context(ctx)
|
|
533
|
+
if not yes:
|
|
534
|
+
exit_with_error(
|
|
535
|
+
ctx,
|
|
536
|
+
TacoError(
|
|
537
|
+
code="AGENT_PUBLISH_CONFIRM_REQUIRED",
|
|
538
|
+
message="发布 Agent 前必须显式传入 --yes。",
|
|
539
|
+
exit_code=2,
|
|
540
|
+
hint="确认本地 DSL 已调试通过后,重新执行 taco agent publish --yes。",
|
|
541
|
+
),
|
|
542
|
+
json_default=True,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
try:
|
|
546
|
+
model_config = _read_json_object(dsl)
|
|
547
|
+
result = AgentService().publish_agent(
|
|
548
|
+
app_id,
|
|
549
|
+
model_config=model_config,
|
|
550
|
+
profile=cli_context.profile,
|
|
551
|
+
)
|
|
552
|
+
except TacoError as error:
|
|
553
|
+
exit_with_error(ctx, error, json_default=True)
|
|
554
|
+
|
|
555
|
+
dsl_deleted = False
|
|
556
|
+
if not keep_dsl:
|
|
557
|
+
try:
|
|
558
|
+
dsl.unlink()
|
|
559
|
+
except OSError:
|
|
560
|
+
exit_with_error(
|
|
561
|
+
ctx,
|
|
562
|
+
TacoError(
|
|
563
|
+
code="AGENT_DSL_CLEANUP_FAILED",
|
|
564
|
+
message="Agent 已发布,但本地 DSL 删除失败。",
|
|
565
|
+
exit_code=2,
|
|
566
|
+
hint=f"请手动删除文件:{dsl}",
|
|
567
|
+
),
|
|
568
|
+
json_default=True,
|
|
569
|
+
)
|
|
570
|
+
dsl_deleted = True
|
|
571
|
+
|
|
572
|
+
data = dict(result) if isinstance(result, dict) else {"result": result}
|
|
573
|
+
data.update({"dsl_deleted": dsl_deleted, "path": str(dsl)})
|
|
574
|
+
emit_success(ctx, data, json_default=True)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _read_json_object(path: Path | None) -> dict[str, Any]:
|
|
578
|
+
if path is None:
|
|
579
|
+
return {}
|
|
580
|
+
try:
|
|
581
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
582
|
+
except (OSError, JSONDecodeError) as error:
|
|
583
|
+
raise TacoError(
|
|
584
|
+
code="AGENT_DSL_INVALID",
|
|
585
|
+
message="Agent DSL 文件不存在或不是合法 JSON。",
|
|
586
|
+
exit_code=2,
|
|
587
|
+
hint="请确认 --dsl/--inputs 指向有效 JSON 文件。",
|
|
588
|
+
) from error
|
|
589
|
+
|
|
590
|
+
if not isinstance(payload, dict):
|
|
591
|
+
raise TacoError(
|
|
592
|
+
code="AGENT_DSL_INVALID",
|
|
593
|
+
message="Agent DSL JSON 必须是对象。",
|
|
594
|
+
exit_code=2,
|
|
595
|
+
hint="请将本地 DSL 调整为 JSON object。",
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
return payload
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _read_json_array(path: Path) -> list[dict[str, Any]]:
|
|
602
|
+
try:
|
|
603
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
604
|
+
except (OSError, JSONDecodeError) as error:
|
|
605
|
+
raise TacoError(
|
|
606
|
+
code="AGENT_FILES_INVALID",
|
|
607
|
+
message="Agent files 文件不存在或不是合法 JSON。",
|
|
608
|
+
exit_code=2,
|
|
609
|
+
) from error
|
|
610
|
+
if not isinstance(payload, list) or any(
|
|
611
|
+
not isinstance(item, dict) for item in payload
|
|
612
|
+
):
|
|
613
|
+
raise TacoError(
|
|
614
|
+
code="AGENT_FILES_INVALID",
|
|
615
|
+
message="Agent files JSON 必须是对象数组。",
|
|
616
|
+
exit_code=2,
|
|
617
|
+
)
|
|
618
|
+
return payload
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _agent_tools(model_config: dict[str, Any]) -> list[dict[str, Any]]:
|
|
622
|
+
agent_mode = model_config.get("agent_mode")
|
|
623
|
+
tools = agent_mode.get("tools") if isinstance(agent_mode, dict) else None
|
|
624
|
+
if not isinstance(tools, list) or any(
|
|
625
|
+
not isinstance(item, dict) for item in tools
|
|
626
|
+
):
|
|
627
|
+
raise TacoError(
|
|
628
|
+
code="AGENT_DSL_VALIDATION_FAILED",
|
|
629
|
+
message="Agent DSL 的 agent_mode.tools 必须是对象数组。",
|
|
630
|
+
exit_code=6,
|
|
631
|
+
)
|
|
632
|
+
return tools
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _same_agent_tool(
|
|
636
|
+
tool: dict[str, Any],
|
|
637
|
+
provider_type: str,
|
|
638
|
+
provider: str,
|
|
639
|
+
tool_name: str,
|
|
640
|
+
) -> bool:
|
|
641
|
+
return (
|
|
642
|
+
tool.get("provider_type") == provider_type
|
|
643
|
+
and _provider_ids_match(
|
|
644
|
+
provider_type,
|
|
645
|
+
tool.get("provider_id"),
|
|
646
|
+
provider,
|
|
647
|
+
)
|
|
648
|
+
and tool.get("tool_name") == tool_name
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _provider_ids_match(
|
|
653
|
+
provider_type: str,
|
|
654
|
+
actual: Any,
|
|
655
|
+
expected: str,
|
|
656
|
+
) -> bool:
|
|
657
|
+
if not isinstance(actual, str) or provider_type != "workflow":
|
|
658
|
+
return actual == expected
|
|
659
|
+
try:
|
|
660
|
+
return UUID(actual) == UUID(expected)
|
|
661
|
+
except ValueError:
|
|
662
|
+
return actual == expected
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _ensure_mutable_agent_dsl(path: Path) -> None:
|
|
666
|
+
if path.is_symlink():
|
|
667
|
+
raise TacoError(
|
|
668
|
+
code="AGENT_DSL_SYMLINK_UNSUPPORTED",
|
|
669
|
+
message="修改 Agent 工具时不允许 DSL 路径是符号链接。",
|
|
670
|
+
exit_code=2,
|
|
671
|
+
hint="请使用普通文件路径。",
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _require_agent_tool_confirmation(ctx: typer.Context, yes: bool) -> None:
|
|
676
|
+
if not yes:
|
|
677
|
+
exit_with_error(
|
|
678
|
+
ctx,
|
|
679
|
+
TacoError(
|
|
680
|
+
code="AGENT_TOOL_CONFIRM_REQUIRED",
|
|
681
|
+
message="修改本地 Agent 工具前必须显式传入 --yes。",
|
|
682
|
+
exit_code=2,
|
|
683
|
+
),
|
|
684
|
+
json_default=True,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _prepare_agent_output(path: Path | None, *, overwrite: bool) -> None:
|
|
689
|
+
parent = (path.parent if path is not None else Path.cwd()).resolve()
|
|
690
|
+
if path is not None and path.is_symlink():
|
|
691
|
+
raise TacoError(
|
|
692
|
+
code="AGENT_DSL_PATH_INVALID",
|
|
693
|
+
message=f"Agent DSL 输出不能是符号链接:{path}",
|
|
694
|
+
exit_code=2,
|
|
695
|
+
)
|
|
696
|
+
if path is not None and path.exists():
|
|
697
|
+
if not path.is_file():
|
|
698
|
+
raise TacoError(
|
|
699
|
+
code="AGENT_DSL_PATH_INVALID",
|
|
700
|
+
message=f"Agent DSL 输出必须是普通文件路径:{path}",
|
|
701
|
+
exit_code=2,
|
|
702
|
+
)
|
|
703
|
+
if not overwrite:
|
|
704
|
+
raise TacoError(
|
|
705
|
+
code="AGENT_DSL_EXISTS",
|
|
706
|
+
message=f"本地 Agent DSL 已存在:{path}",
|
|
707
|
+
exit_code=2,
|
|
708
|
+
hint="请显式传入 --overwrite;Agent create 还需 --yes。",
|
|
709
|
+
)
|
|
710
|
+
if not os.access(path, os.W_OK):
|
|
711
|
+
raise TacoError(
|
|
712
|
+
code="AGENT_DSL_PATH_INVALID",
|
|
713
|
+
message=f"本地 Agent DSL 不可覆盖:{path}",
|
|
714
|
+
exit_code=2,
|
|
715
|
+
)
|
|
716
|
+
try:
|
|
717
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
718
|
+
with tempfile.NamedTemporaryFile(
|
|
719
|
+
dir=parent,
|
|
720
|
+
prefix=".taco-write-check-",
|
|
721
|
+
):
|
|
722
|
+
pass
|
|
723
|
+
except OSError as error:
|
|
724
|
+
raise TacoError(
|
|
725
|
+
code="AGENT_DSL_WRITE_FAILED",
|
|
726
|
+
message="创建远端 Agent 前无法确认本地 DSL 目录可写。",
|
|
727
|
+
exit_code=2,
|
|
728
|
+
hint=f"请检查目录权限和磁盘空间:{parent}",
|
|
729
|
+
) from error
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _write_json_file(path: Path, payload: Any) -> None:
|
|
733
|
+
temporary: Path | None = None
|
|
734
|
+
try:
|
|
735
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
736
|
+
descriptor, temporary_name = tempfile.mkstemp(
|
|
737
|
+
dir=path.parent,
|
|
738
|
+
prefix=f".{path.name}.",
|
|
739
|
+
suffix=".tmp",
|
|
740
|
+
text=True,
|
|
741
|
+
)
|
|
742
|
+
temporary = Path(temporary_name)
|
|
743
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as file:
|
|
744
|
+
file.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
|
745
|
+
file.flush()
|
|
746
|
+
os.fsync(file.fileno())
|
|
747
|
+
temporary.replace(path)
|
|
748
|
+
except OSError as error:
|
|
749
|
+
raise TacoError(
|
|
750
|
+
code="AGENT_DSL_WRITE_FAILED",
|
|
751
|
+
message="无法写入 Agent DSL 文件。",
|
|
752
|
+
exit_code=2,
|
|
753
|
+
hint=f"请检查目录权限和磁盘空间:{path}",
|
|
754
|
+
) from error
|
|
755
|
+
finally:
|
|
756
|
+
if temporary is not None and temporary.exists():
|
|
757
|
+
try:
|
|
758
|
+
temporary.unlink()
|
|
759
|
+
except OSError:
|
|
760
|
+
pass
|