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/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0.dev36"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Developer-center Agent data OpenAPI support."""
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from importlib import resources
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
from types import MappingProxyType
|
|
8
|
+
from typing import Any
|
|
9
|
+
from urllib.parse import urlsplit
|
|
10
|
+
|
|
11
|
+
from jsonschema import Draft202012Validator
|
|
12
|
+
|
|
13
|
+
from taco.agent_openapi.models import AgentCatalog, AgentOperation, freeze
|
|
14
|
+
from taco.core.errors import TacoError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_ID_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
18
|
+
_ZH_PATTERN = re.compile(r"[\u3400-\u9fff]")
|
|
19
|
+
_RESERVED = {"accesskeyid", "expires", "timestamp", "signature", "authorization"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_catalog(root: Path | None = None) -> AgentCatalog:
|
|
23
|
+
try:
|
|
24
|
+
if root is None:
|
|
25
|
+
package_root = resources.files("taco.assets").joinpath("agent_openapi")
|
|
26
|
+
payload = json.loads(package_root.joinpath("catalog.json").read_text("utf-8"))
|
|
27
|
+
entries = _entries(payload)
|
|
28
|
+
operation_payloads = {
|
|
29
|
+
entry: json.loads(package_root.joinpath("operations", f"{entry}.json").read_text("utf-8"))
|
|
30
|
+
for entry in entries
|
|
31
|
+
}
|
|
32
|
+
actual = sorted(_resource_entries(package_root.joinpath("operations")))
|
|
33
|
+
else:
|
|
34
|
+
payload = json.loads((root / "catalog.json").read_text("utf-8"))
|
|
35
|
+
entries = _entries(payload)
|
|
36
|
+
operation_payloads = {
|
|
37
|
+
entry: json.loads((root / "operations" / f"{entry}.json").read_text("utf-8"))
|
|
38
|
+
for entry in entries
|
|
39
|
+
}
|
|
40
|
+
actual = sorted(
|
|
41
|
+
path.relative_to(root / "operations").as_posix()[:-5]
|
|
42
|
+
for path in (root / "operations").rglob("*.json")
|
|
43
|
+
)
|
|
44
|
+
if sorted(entries) != actual:
|
|
45
|
+
raise _catalog_error("catalog 登记项与 operation 文件不一致")
|
|
46
|
+
operations = tuple(_build_operation(entry, operation_payloads[entry]) for entry in entries)
|
|
47
|
+
if len({item.id for item in operations}) != len(operations):
|
|
48
|
+
raise _catalog_error("operation id 重复")
|
|
49
|
+
return AgentCatalog(
|
|
50
|
+
catalog_version=payload["catalog_version"],
|
|
51
|
+
source=freeze(payload.get("source", {})),
|
|
52
|
+
operations=operations,
|
|
53
|
+
by_id=MappingProxyType({item.id: item for item in operations}),
|
|
54
|
+
)
|
|
55
|
+
except TacoError:
|
|
56
|
+
raise
|
|
57
|
+
except (OSError, ValueError, TypeError, KeyError) as error:
|
|
58
|
+
raise _catalog_error(str(error)) from error
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def operation_not_found(operation_id: str) -> TacoError:
|
|
62
|
+
return TacoError(
|
|
63
|
+
code="AGENT_OPENAPI_OPERATION_NOT_FOUND",
|
|
64
|
+
message=f"未知 Agent OpenAPI operation:{operation_id}",
|
|
65
|
+
exit_code=2,
|
|
66
|
+
hint="请执行 taco agent operation search 或 list 查找候选接口。",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _entries(payload: Any) -> list[str]:
|
|
71
|
+
if not isinstance(payload, dict):
|
|
72
|
+
raise _catalog_error("catalog 必须是对象")
|
|
73
|
+
version = payload.get("catalog_version")
|
|
74
|
+
if not isinstance(version, str) or version.split(".", 1)[0] != "1":
|
|
75
|
+
raise _catalog_error("不支持的 catalog_version")
|
|
76
|
+
if payload.get("json_schema_dialect") != "https://json-schema.org/draft/2020-12/schema":
|
|
77
|
+
raise _catalog_error("不支持的 JSON Schema 方言")
|
|
78
|
+
entries = payload.get("operations")
|
|
79
|
+
if not isinstance(entries, list) or any(not isinstance(item, str) for item in entries):
|
|
80
|
+
raise _catalog_error("operations 必须是字符串数组")
|
|
81
|
+
if len(entries) != len(set(entries)):
|
|
82
|
+
raise _catalog_error("catalog operation 重复")
|
|
83
|
+
for entry in entries:
|
|
84
|
+
path = PurePosixPath(entry)
|
|
85
|
+
if path.is_absolute() or len(path.parts) != 2 or not all(_ID_PATTERN.fullmatch(part) for part in path.parts):
|
|
86
|
+
raise _catalog_error(f"非法 operation 路径:{entry}")
|
|
87
|
+
return entries
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _resource_entries(root: Any, prefix: str = "") -> list[str]:
|
|
91
|
+
result: list[str] = []
|
|
92
|
+
for child in root.iterdir() if root.is_dir() else ():
|
|
93
|
+
relative = f"{prefix}/{child.name}" if prefix else child.name
|
|
94
|
+
if child.is_dir():
|
|
95
|
+
result.extend(_resource_entries(child, relative))
|
|
96
|
+
elif child.name.endswith(".json"):
|
|
97
|
+
result.append(relative[:-5])
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _build_operation(entry: str, payload: Any) -> AgentOperation:
|
|
102
|
+
if not isinstance(payload, dict) or str(payload.get("schema_version", "")).split(".", 1)[0] != "1":
|
|
103
|
+
raise _catalog_error(f"{entry} schema 不受支持")
|
|
104
|
+
group, filename = entry.split("/")
|
|
105
|
+
if payload.get("id") != filename or payload.get("group") != group:
|
|
106
|
+
raise _catalog_error(f"{entry} 的 id/group 与文件名不一致")
|
|
107
|
+
for field in ("title", "description"):
|
|
108
|
+
if not isinstance(payload.get(field), str) or not payload[field].strip():
|
|
109
|
+
raise _catalog_error(f"{entry} 缺少 {field}")
|
|
110
|
+
keywords = payload.get("keywords")
|
|
111
|
+
source = payload.get("source")
|
|
112
|
+
if not isinstance(keywords, list) or any(not isinstance(item, str) for item in keywords):
|
|
113
|
+
raise _catalog_error(f"{entry} keywords 不合法")
|
|
114
|
+
if not isinstance(source, dict) or not all(isinstance(source.get(key), str) and source[key] for key in ("url", "anchor", "snapshot_date")):
|
|
115
|
+
raise _catalog_error(f"{entry} source 不完整")
|
|
116
|
+
http = payload.get("http")
|
|
117
|
+
if not isinstance(http, dict) or http.get("method") != "POST" or not _safe_path(http.get("path")):
|
|
118
|
+
raise _catalog_error(f"{entry} HTTP method/path 不合法")
|
|
119
|
+
query_schema = http.get("query_schema")
|
|
120
|
+
body_schema = http.get("body_schema")
|
|
121
|
+
for name, schema in (("query", query_schema), ("body", body_schema)):
|
|
122
|
+
if not isinstance(schema, dict) or schema.get("type") != "object" or schema.get("additionalProperties") is not False:
|
|
123
|
+
raise _catalog_error(f"{entry} {name}_schema 不合法")
|
|
124
|
+
Draft202012Validator.check_schema(schema)
|
|
125
|
+
_validate_descriptions(schema, request=True, root=True)
|
|
126
|
+
fields = set(query_schema.get("properties", {})) | set(body_schema.get("properties", {}))
|
|
127
|
+
if set(query_schema.get("properties", {})) & set(body_schema.get("properties", {})):
|
|
128
|
+
raise _catalog_error(f"{entry} query/body 参数重名")
|
|
129
|
+
if any(field.lower() in _RESERVED for field in fields):
|
|
130
|
+
raise _catalog_error(f"{entry} 声明了保留认证参数")
|
|
131
|
+
response = payload.get("response")
|
|
132
|
+
if not isinstance(response, dict) or not isinstance(response.get("schema"), dict):
|
|
133
|
+
raise _catalog_error(f"{entry} response 不合法")
|
|
134
|
+
Draft202012Validator.check_schema(response["schema"])
|
|
135
|
+
_validate_descriptions(response["schema"], request=False, root=True)
|
|
136
|
+
risk = payload.get("risk")
|
|
137
|
+
if risk != {"level": "read", "requires_confirmation": False, "target_fields": []}:
|
|
138
|
+
raise _catalog_error(f"{entry} 只允许只读风险")
|
|
139
|
+
if payload.get("execution") != {"kind": "http"}:
|
|
140
|
+
raise _catalog_error(f"{entry} execution 不合法")
|
|
141
|
+
examples = payload.get("examples")
|
|
142
|
+
if not isinstance(examples, list) or any(not isinstance(item, dict) for item in examples):
|
|
143
|
+
raise _catalog_error(f"{entry} examples 不合法")
|
|
144
|
+
allowed = fields
|
|
145
|
+
for example in examples:
|
|
146
|
+
params = example.get("params")
|
|
147
|
+
if not isinstance(example.get("title"), str) or not isinstance(params, dict) or set(params) - allowed:
|
|
148
|
+
raise _catalog_error(f"{entry} example 含未知参数")
|
|
149
|
+
query = {key: value for key, value in params.items() if key in query_schema.get("properties", {})}
|
|
150
|
+
body = {key: value for key, value in params.items() if key in body_schema.get("properties", {})}
|
|
151
|
+
if next(Draft202012Validator(query_schema).iter_errors(query), None) or next(Draft202012Validator(body_schema).iter_errors(body), None):
|
|
152
|
+
raise _catalog_error(f"{entry} example 未通过 schema")
|
|
153
|
+
return AgentOperation(
|
|
154
|
+
id=filename,
|
|
155
|
+
title=payload["title"],
|
|
156
|
+
description=payload["description"],
|
|
157
|
+
group=group,
|
|
158
|
+
keywords=tuple(keywords),
|
|
159
|
+
source=freeze(source),
|
|
160
|
+
http=freeze(http),
|
|
161
|
+
response=freeze(response),
|
|
162
|
+
risk=freeze(risk),
|
|
163
|
+
execution=freeze(payload["execution"]),
|
|
164
|
+
examples=tuple(freeze(examples)),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _safe_path(value: Any) -> bool:
|
|
169
|
+
if not isinstance(value, str) or not value.startswith("/agent/v1/data/"):
|
|
170
|
+
return False
|
|
171
|
+
parsed = urlsplit(value)
|
|
172
|
+
return not parsed.scheme and not parsed.netloc and not parsed.query and not parsed.fragment and ".." not in PurePosixPath(parsed.path).parts
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _validate_descriptions(schema: dict[str, Any], *, request: bool, root: bool = False) -> None:
|
|
176
|
+
if not root:
|
|
177
|
+
required = ("title", "description") if request else ("description",)
|
|
178
|
+
if not all(isinstance(schema.get(field), str) and schema[field].strip() for field in required):
|
|
179
|
+
raise _catalog_error("schema 字段缺少说明")
|
|
180
|
+
if not _ZH_PATTERN.search(schema["description"]):
|
|
181
|
+
raise _catalog_error("schema description 必须包含中文")
|
|
182
|
+
if "enum" in schema:
|
|
183
|
+
descriptions = schema.get("x-enum-descriptions")
|
|
184
|
+
if not isinstance(descriptions, dict) or set(map(str, schema["enum"])) != set(descriptions):
|
|
185
|
+
raise _catalog_error("enum 缺少完整说明")
|
|
186
|
+
for child in schema.get("properties", {}).values():
|
|
187
|
+
_validate_descriptions(child, request=request)
|
|
188
|
+
if isinstance(schema.get("items"), dict):
|
|
189
|
+
_validate_descriptions(schema["items"], request=request)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _catalog_error(detail: str) -> TacoError:
|
|
193
|
+
return TacoError(
|
|
194
|
+
code="AGENT_OPENAPI_CATALOG_INVALID",
|
|
195
|
+
message="内置 Agent OpenAPI operation catalog 不合法。",
|
|
196
|
+
exit_code=2,
|
|
197
|
+
hint=detail,
|
|
198
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
JsonMapping = Mapping[str, Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class AgentOperation:
|
|
13
|
+
id: str
|
|
14
|
+
title: str
|
|
15
|
+
description: str
|
|
16
|
+
group: str
|
|
17
|
+
keywords: tuple[str, ...]
|
|
18
|
+
source: JsonMapping
|
|
19
|
+
http: JsonMapping
|
|
20
|
+
response: JsonMapping
|
|
21
|
+
risk: JsonMapping
|
|
22
|
+
execution: JsonMapping
|
|
23
|
+
examples: tuple[JsonMapping, ...]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class AgentCatalog:
|
|
28
|
+
catalog_version: str
|
|
29
|
+
source: JsonMapping
|
|
30
|
+
operations: tuple[AgentOperation, ...]
|
|
31
|
+
by_id: Mapping[str, AgentOperation]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def freeze(value: Any) -> Any:
|
|
35
|
+
if isinstance(value, dict):
|
|
36
|
+
return MappingProxyType({key: freeze(item) for key, item in value.items()})
|
|
37
|
+
if isinstance(value, list):
|
|
38
|
+
return tuple(freeze(item) for item in value)
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def thaw(value: Any) -> Any:
|
|
43
|
+
if isinstance(value, Mapping):
|
|
44
|
+
return {key: thaw(item) for key, item in value.items()}
|
|
45
|
+
if isinstance(value, tuple):
|
|
46
|
+
return [thaw(item) for item in value]
|
|
47
|
+
return value
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
from jsonschema import Draft202012Validator
|
|
8
|
+
|
|
9
|
+
from taco.agent_openapi.models import AgentOperation, thaw
|
|
10
|
+
from taco.core.errors import TacoError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_RESERVED = {"accesskeyid", "expires", "timestamp", "signature", "authorization"}
|
|
14
|
+
_MAX_QUERY_SPAN_SECONDS = 90 * 24 * 60 * 60
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_parameters(
|
|
18
|
+
operation: AgentOperation,
|
|
19
|
+
*,
|
|
20
|
+
params: list[str] | None = None,
|
|
21
|
+
params_file: Path | None = None,
|
|
22
|
+
body_file: Path | None = None,
|
|
23
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
24
|
+
query_schema = operation.http["query_schema"]
|
|
25
|
+
body_schema = operation.http["body_schema"]
|
|
26
|
+
query_fields = query_schema.get("properties", {})
|
|
27
|
+
body_fields = body_schema.get("properties", {})
|
|
28
|
+
values: dict[str, Any] = {}
|
|
29
|
+
for expression in params or []:
|
|
30
|
+
key, separator, raw = expression.partition("=")
|
|
31
|
+
if not separator or not key:
|
|
32
|
+
raise _parameter_error("--param 必须使用 key=value 格式。")
|
|
33
|
+
if key in values:
|
|
34
|
+
raise _parameter_error(f"参数重复:/{key}")
|
|
35
|
+
schema = query_fields.get(key) or body_fields.get(key)
|
|
36
|
+
if schema is None:
|
|
37
|
+
raise _parameter_error(f"未知参数:/{key}")
|
|
38
|
+
values[key] = _parse_value(raw, schema, key)
|
|
39
|
+
if params_file is not None:
|
|
40
|
+
file_values = _read_object(params_file)
|
|
41
|
+
conflicts = set(values) & set(file_values)
|
|
42
|
+
if conflicts:
|
|
43
|
+
raise _parameter_error(f"参数重复:/{sorted(conflicts)[0]}")
|
|
44
|
+
values.update(file_values)
|
|
45
|
+
if any(key.lower() in _RESERVED for key in values):
|
|
46
|
+
raise _parameter_error("不能传入公共签名或认证参数。")
|
|
47
|
+
unknown = set(values) - set(query_fields) - set(body_fields)
|
|
48
|
+
if unknown:
|
|
49
|
+
raise _parameter_error(f"未知参数:/{sorted(unknown)[0]}")
|
|
50
|
+
query = {key: value for key, value in values.items() if key in query_fields}
|
|
51
|
+
body = {key: value for key, value in values.items() if key in body_fields}
|
|
52
|
+
if body_file is not None:
|
|
53
|
+
if body:
|
|
54
|
+
raise _parameter_error("--body-file 不能与映射到 body 的 --param 同时使用。")
|
|
55
|
+
body = _read_object(body_file)
|
|
56
|
+
_validate(query_schema, query)
|
|
57
|
+
_validate(body_schema, body)
|
|
58
|
+
_validate_conversation_range(operation, body)
|
|
59
|
+
return query, body
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _parse_value(raw: str, schema: Mapping[str, Any], key: str) -> Any:
|
|
63
|
+
expected = schema.get("type")
|
|
64
|
+
try:
|
|
65
|
+
if expected == "string":
|
|
66
|
+
return raw
|
|
67
|
+
if expected == "integer":
|
|
68
|
+
return int(raw, 10)
|
|
69
|
+
if expected == "number":
|
|
70
|
+
return float(raw)
|
|
71
|
+
if expected == "boolean":
|
|
72
|
+
if raw.casefold() in {"true", "1"}:
|
|
73
|
+
return True
|
|
74
|
+
if raw.casefold() in {"false", "0"}:
|
|
75
|
+
return False
|
|
76
|
+
raise ValueError
|
|
77
|
+
if expected in {"array", "object"}:
|
|
78
|
+
return json.loads(raw)
|
|
79
|
+
except (ValueError, json.JSONDecodeError):
|
|
80
|
+
raise _parameter_error(f"参数 /{key} 类型错误,期望 {expected}。") from None
|
|
81
|
+
raise _parameter_error(f"参数 /{key} 使用了不支持的类型。")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _read_object(path: Path) -> dict[str, Any]:
|
|
85
|
+
try:
|
|
86
|
+
payload = json.loads(path.read_text("utf-8"))
|
|
87
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
88
|
+
raise _parameter_error(f"参数文件不是合法 UTF-8 JSON 对象:{path}") from None
|
|
89
|
+
if not isinstance(payload, dict):
|
|
90
|
+
raise _parameter_error(f"参数文件必须是 JSON 对象:{path}")
|
|
91
|
+
return payload
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _validate(schema: Mapping[str, Any], value: Any) -> None:
|
|
95
|
+
error = next(Draft202012Validator(thaw(schema)).iter_errors(value), None)
|
|
96
|
+
if error is not None:
|
|
97
|
+
pointer = "/" + "/".join(str(part) for part in error.absolute_path)
|
|
98
|
+
raise _parameter_error(f"参数 {pointer or '/'} 校验失败,期望 {error.validator}。")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _validate_conversation_range(operation: AgentOperation, body: dict[str, Any]) -> None:
|
|
102
|
+
if operation.id != "list-conversations":
|
|
103
|
+
return
|
|
104
|
+
start = body["created_at_start"]
|
|
105
|
+
end = body["created_at_end"]
|
|
106
|
+
if end < start or end - start > _MAX_QUERY_SPAN_SECONDS:
|
|
107
|
+
raise _parameter_error("会话创建时间范围必须正序且不能超过 90 天。")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _parameter_error(message: str) -> TacoError:
|
|
111
|
+
return TacoError(code="AGENT_OPENAPI_PARAMETER_INVALID", message=message, exit_code=2)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from jsonschema import Draft202012Validator
|
|
7
|
+
|
|
8
|
+
from taco.agent_openapi.catalog import load_catalog, operation_not_found
|
|
9
|
+
from taco.agent_openapi.models import AgentOperation, thaw
|
|
10
|
+
from taco.core.config_store import ConfigStore
|
|
11
|
+
from taco.core.developer_center_client import DeveloperCenterClient
|
|
12
|
+
from taco.core.errors import TacoError
|
|
13
|
+
from taco.core.profile_resolver import ProfileResolver
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentOpenApiService:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
store: ConfigStore | None = None,
|
|
20
|
+
*,
|
|
21
|
+
profile_resolver: ProfileResolver | None = None,
|
|
22
|
+
developer_http_client: httpx.Client | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.store = store or ConfigStore()
|
|
25
|
+
self.profile_resolver = profile_resolver or ProfileResolver(self.store)
|
|
26
|
+
self.catalog = load_catalog()
|
|
27
|
+
self.developer_http_client = developer_http_client
|
|
28
|
+
|
|
29
|
+
def call(
|
|
30
|
+
self,
|
|
31
|
+
operation_id: str,
|
|
32
|
+
*,
|
|
33
|
+
query: dict[str, Any],
|
|
34
|
+
body: dict[str, Any],
|
|
35
|
+
profile: str | None = None,
|
|
36
|
+
) -> dict[str, Any]:
|
|
37
|
+
operation = self.catalog.by_id.get(operation_id)
|
|
38
|
+
if operation is None:
|
|
39
|
+
raise operation_not_found(operation_id)
|
|
40
|
+
resolved = self.profile_resolver.resolve(profile)
|
|
41
|
+
client = DeveloperCenterClient(
|
|
42
|
+
endpoint=resolved.endpoint,
|
|
43
|
+
access_key_id=resolved.access_key_id,
|
|
44
|
+
access_key_secret=resolved.access_key_secret,
|
|
45
|
+
http_client=self.developer_http_client,
|
|
46
|
+
)
|
|
47
|
+
try:
|
|
48
|
+
payload = client.request_json(
|
|
49
|
+
operation.http["method"],
|
|
50
|
+
operation.http["path"],
|
|
51
|
+
query=query or None,
|
|
52
|
+
json_body=body or None,
|
|
53
|
+
)
|
|
54
|
+
return validate_response(operation, payload)
|
|
55
|
+
finally:
|
|
56
|
+
client.close()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def validate_response(operation: AgentOperation, payload: Any) -> dict[str, Any]:
|
|
60
|
+
request_id = payload.get("requestId") if isinstance(payload, dict) and isinstance(payload.get("requestId"), str) else None
|
|
61
|
+
error = next(Draft202012Validator(thaw(operation.response["schema"])).iter_errors(payload), None)
|
|
62
|
+
if error is not None:
|
|
63
|
+
pointer = "/" + "/".join(str(part) for part in error.absolute_path)
|
|
64
|
+
raise TacoError(
|
|
65
|
+
code="AGENT_OPENAPI_RESPONSE_INVALID",
|
|
66
|
+
message=f"Agent OpenAPI 响应 {pointer or '/'} 校验失败,期望 {error.validator}。",
|
|
67
|
+
exit_code=4,
|
|
68
|
+
request_id=request_id,
|
|
69
|
+
path=operation.http["path"],
|
|
70
|
+
)
|
|
71
|
+
business = dict(payload)
|
|
72
|
+
business.pop("requestId", None)
|
|
73
|
+
return {"request_id": request_id, "data": business}
|
taco/aikb/__init__.py
ADDED