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
@@ -0,0 +1,217 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from urllib.parse import urlsplit
5
+
6
+ from taco.core.access_token_manager import credential_fingerprint
7
+ from taco.core.config_store import ConfigStore, TacoConfig
8
+ from taco.core.errors import TacoError
9
+ from taco.core.output import mask_token
10
+ from taco.core.profile_resolver import ProfileResolver, ResolvedProfile
11
+
12
+
13
+ class ProfileManager:
14
+ def __init__(
15
+ self,
16
+ store: ConfigStore | None = None,
17
+ *,
18
+ resolver: ProfileResolver | None = None,
19
+ ) -> None:
20
+ self.store = store or ConfigStore()
21
+ self.resolver = resolver or ProfileResolver(self.store)
22
+
23
+ def set_profile(
24
+ self,
25
+ *,
26
+ name: str,
27
+ endpoint: str,
28
+ access_key_id: str,
29
+ access_key_secret: str,
30
+ ) -> dict[str, Any]:
31
+ normalized_name = name.strip()
32
+ normalized_endpoint = endpoint.strip().rstrip("/")
33
+ normalized_access_key_id = access_key_id.strip()
34
+ if not normalized_name:
35
+ raise TacoError(code="PROFILE_NAME_REQUIRED", message="profile name 不能为空。", exit_code=2)
36
+ parsed = urlsplit(normalized_endpoint)
37
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
38
+ raise TacoError(code="PROFILE_ENDPOINT_INVALID", message="endpoint 不是有效 HTTP(S) 地址。", exit_code=2)
39
+ if not normalized_access_key_id or not access_key_secret:
40
+ raise TacoError(code="ACCESS_KEY_PROFILE_INVALID", message="AccessKey profile 配置不完整。", exit_code=2)
41
+ profile_data = {
42
+ "endpoint": normalized_endpoint,
43
+ "access_key_id": normalized_access_key_id,
44
+ "access_key_secret": access_key_secret,
45
+ }
46
+
47
+ def mutate(config: TacoConfig) -> None:
48
+ if config.profiles.get(normalized_name) != profile_data:
49
+ config.auth_tokens.pop(normalized_name, None)
50
+ config.profiles[normalized_name] = profile_data
51
+ config.current_profile = normalized_name
52
+
53
+ self.store.update(mutate)
54
+ return {
55
+ "profile": normalized_name,
56
+ "current": True,
57
+ "endpoint": normalized_endpoint,
58
+ "access_key_id": normalized_access_key_id,
59
+ "access_key_secret": "********",
60
+ "has_access_token": False,
61
+ }
62
+
63
+ def list_profiles(self) -> list[dict[str, Any]]:
64
+ config = self.store.load()
65
+ entries: list[tuple[str, str, dict[str, Any], bool]] = []
66
+ zenava = self.resolver.load_zenava()
67
+ if zenava is not None:
68
+ entries.extend(
69
+ (
70
+ name,
71
+ "zenava",
72
+ value,
73
+ name == zenava["current_profile"],
74
+ )
75
+ for name, value in zenava["profiles"].items()
76
+ )
77
+ entries.extend(
78
+ (
79
+ name,
80
+ "taco",
81
+ value,
82
+ name == config.current_profile,
83
+ )
84
+ for name, value in config.profiles.items()
85
+ )
86
+
87
+ name_counts: dict[str, int] = {}
88
+ for name, _, _, _ in entries:
89
+ name_counts[name] = name_counts.get(name, 0) + 1
90
+
91
+ profiles: list[dict[str, Any]] = []
92
+ for name, source, value, current in entries:
93
+ token = _string(value.get("token") or value.get("access_token"))
94
+ token_updated_at = _string(
95
+ value.get("token_updated_at") or value.get("access_token_updated_at")
96
+ )
97
+ cached = config.auth_tokens.get(name, {})
98
+ if _cache_belongs_to_profile(
99
+ name,
100
+ source,
101
+ value,
102
+ cached,
103
+ name_is_unique=name_counts[name] == 1,
104
+ ):
105
+ token = _string(cached.get("access_token")) or token
106
+ token_updated_at = _string(cached.get("updated_at")) or token_updated_at
107
+ profiles.append(
108
+ self._public_profile(
109
+ name,
110
+ value,
111
+ source=source,
112
+ current=current,
113
+ token=token,
114
+ token_updated_at=token_updated_at,
115
+ )
116
+ )
117
+
118
+ source_order = {"zenava": 0, "taco": 1}
119
+ return sorted(
120
+ profiles,
121
+ key=lambda item: (
122
+ not item["current"],
123
+ source_order[item["source"]],
124
+ item["key"],
125
+ ),
126
+ )
127
+
128
+ def use(self, name: str) -> dict[str, str]:
129
+ def mutate(config: TacoConfig) -> None:
130
+ if name not in config.profiles:
131
+ raise _profile_not_found_error(name)
132
+ config.current_profile = name
133
+
134
+ self.store.update(mutate)
135
+ return {"current_profile": name}
136
+
137
+ def delete(self, name: str) -> dict[str, str | None]:
138
+ current: list[str | None] = []
139
+
140
+ def mutate(config: TacoConfig) -> None:
141
+ if name not in config.profiles:
142
+ raise _profile_not_found_error(name)
143
+ if name == config.current_profile:
144
+ raise TacoError(
145
+ code="PROFILE_DELETE_CURRENT_FORBIDDEN",
146
+ message=f"不能删除当前 profile `{name}`。",
147
+ exit_code=2,
148
+ hint="请先执行 taco profile use 切换到其他 profile。",
149
+ )
150
+ del config.profiles[name]
151
+ config.auth_tokens.pop(name, None)
152
+ current.append(config.current_profile)
153
+
154
+ self.store.update(mutate)
155
+ return {"deleted": name, "current_profile": current[0]}
156
+
157
+ @staticmethod
158
+ def _public_profile(
159
+ name: str,
160
+ value: dict[str, Any],
161
+ *,
162
+ source: str = "taco",
163
+ current: bool,
164
+ token: str = "",
165
+ token_updated_at: str = "",
166
+ ) -> dict[str, Any]:
167
+ return {
168
+ "key": name,
169
+ "source": source,
170
+ "current": current,
171
+ "endpoint": value.get("endpoint", ""),
172
+ "access_key_id": value.get("access_key_id", ""),
173
+ "access_key_secret": "********" if value.get("access_key_secret") else "",
174
+ "access_token": mask_token(token),
175
+ "access_token_updated_at": token_updated_at,
176
+ }
177
+
178
+
179
+ def _profile_not_found_error(name: str) -> TacoError:
180
+ return TacoError(
181
+ code="PROFILE_NOT_FOUND",
182
+ message=f"profile `{name}` 不存在。",
183
+ exit_code=2,
184
+ hint="请执行 taco profile list 查看可用 profile。",
185
+ )
186
+
187
+
188
+ def _string(value: Any) -> str:
189
+ return value if isinstance(value, str) else ""
190
+
191
+
192
+ def _cache_belongs_to_profile(
193
+ name: str,
194
+ source: str,
195
+ value: dict[str, Any],
196
+ cached: dict[str, Any],
197
+ *,
198
+ name_is_unique: bool,
199
+ ) -> bool:
200
+ if not _string(cached.get("access_token")):
201
+ return False
202
+ fingerprint = _string(cached.get("credential_fingerprint"))
203
+ if not fingerprint:
204
+ return name_is_unique
205
+ endpoint = _string(value.get("endpoint")).strip().rstrip("/")
206
+ access_key_id = _string(value.get("access_key_id")).strip()
207
+ access_key_secret = _string(value.get("access_key_secret"))
208
+ if not endpoint or not access_key_id or not access_key_secret:
209
+ return False
210
+ profile = ResolvedProfile(
211
+ name=name,
212
+ source="zenava" if source == "zenava" else "taco",
213
+ endpoint=endpoint,
214
+ access_key_id=access_key_id,
215
+ access_key_secret=access_key_secret,
216
+ )
217
+ return fingerprint == credential_fingerprint(profile)
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from json import JSONDecodeError
6
+ from pathlib import Path
7
+ from typing import Any, Literal
8
+ from urllib.parse import urlsplit
9
+
10
+ from taco.core.config_store import ConfigStore
11
+ from taco.core.errors import TacoError
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class ResolvedProfile:
16
+ name: str
17
+ source: Literal["zenava", "taco"]
18
+ endpoint: str
19
+ access_key_id: str
20
+ access_key_secret: str
21
+ legacy_access_token: str | None = None
22
+
23
+
24
+ class ProfileResolver:
25
+ def __init__(
26
+ self,
27
+ store: ConfigStore | None = None,
28
+ *,
29
+ zenava_path: Path | None = None,
30
+ ) -> None:
31
+ self.store = store or ConfigStore()
32
+ self.zenava_path = zenava_path or Path.home() / ".zenava" / "profile.json"
33
+
34
+ def resolve(self, name: str | None = None) -> ResolvedProfile:
35
+ if name is not None:
36
+ zenava = self._load_zenava()
37
+ if zenava is not None and name in zenava["profiles"]:
38
+ return _resolve_profile(
39
+ name,
40
+ zenava["profiles"][name],
41
+ source="zenava",
42
+ )
43
+ else:
44
+ try:
45
+ zenava = self._load_zenava()
46
+ zenava_name = zenava["current_profile"] if zenava else None
47
+ if zenava_name is not None and zenava_name in zenava["profiles"]:
48
+ try:
49
+ return _resolve_profile(
50
+ zenava_name,
51
+ zenava["profiles"][zenava_name],
52
+ source="zenava",
53
+ )
54
+ except TacoError:
55
+ pass
56
+ except TacoError:
57
+ pass
58
+
59
+ taco = self.store.load()
60
+ taco_name = name if name is not None else taco.current_profile
61
+ if taco_name is None or taco_name not in taco.profiles:
62
+ raise TacoError(
63
+ code="PROFILE_NOT_FOUND",
64
+ message=f"profile `{taco_name or ''}` 不存在。",
65
+ exit_code=2,
66
+ hint="请执行 taco profile set 创建配置,或使用 --profile 选择已有配置。",
67
+ )
68
+ return _resolve_profile(taco_name, taco.profiles[taco_name], source="taco")
69
+
70
+ def load_zenava(self) -> dict[str, Any] | None:
71
+ """读取 Zenava profile 集合,供只读发现命令使用。"""
72
+ return self._load_zenava()
73
+
74
+ def known_names(self) -> set[str]:
75
+ names = set(self.store.load().profiles)
76
+ zenava = self._load_zenava()
77
+ if zenava is not None:
78
+ names.update(zenava["profiles"])
79
+ return names
80
+
81
+ def _load_zenava(self) -> dict[str, Any] | None:
82
+ if not self.zenava_path.is_file():
83
+ return None
84
+ try:
85
+ payload = json.loads(self.zenava_path.read_text(encoding="utf-8"))
86
+ except (OSError, JSONDecodeError) as error:
87
+ raise _zenava_invalid_error() from error
88
+ if not isinstance(payload, dict):
89
+ raise _zenava_invalid_error()
90
+ current_profile = payload.get("currentProfile")
91
+ profiles = payload.get("profiles", {})
92
+ if (
93
+ (current_profile is not None and not isinstance(current_profile, str))
94
+ or not isinstance(profiles, dict)
95
+ or any(
96
+ not isinstance(profile_name, str) or not isinstance(value, dict)
97
+ for profile_name, value in profiles.items()
98
+ )
99
+ ):
100
+ raise _zenava_invalid_error()
101
+ return {"current_profile": current_profile, "profiles": profiles}
102
+
103
+
104
+ def _resolve_profile(
105
+ name: str,
106
+ value: dict[str, Any],
107
+ *,
108
+ source: Literal["zenava", "taco"],
109
+ ) -> ResolvedProfile:
110
+ required = ("endpoint", "access_key_id", "access_key_secret")
111
+ missing = [
112
+ field
113
+ for field in required
114
+ if not isinstance(value.get(field), str) or not str(value[field]).strip()
115
+ ]
116
+ if missing:
117
+ raise TacoError(
118
+ code="ACCESS_KEY_PROFILE_INVALID",
119
+ message=f"{source} profile `{name}` 缺少字段:{', '.join(missing)}。",
120
+ exit_code=2,
121
+ )
122
+ endpoint = str(value["endpoint"]).strip().rstrip("/")
123
+ parsed = urlsplit(endpoint)
124
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
125
+ raise TacoError(
126
+ code="ACCESS_KEY_PROFILE_INVALID",
127
+ message=f"{source} profile `{name}` 的 endpoint 不是有效 HTTP(S) 地址。",
128
+ exit_code=2,
129
+ )
130
+ legacy_access_token = value.get("access_token") if source == "taco" else None
131
+ return ResolvedProfile(
132
+ name=name,
133
+ source=source,
134
+ endpoint=endpoint,
135
+ access_key_id=str(value["access_key_id"]).strip(),
136
+ access_key_secret=str(value["access_key_secret"]),
137
+ legacy_access_token=(
138
+ legacy_access_token
139
+ if isinstance(legacy_access_token, str) and legacy_access_token
140
+ else None
141
+ ),
142
+ )
143
+
144
+
145
+ def _zenava_invalid_error() -> TacoError:
146
+ return TacoError(
147
+ code="ZENAVA_CONFIG_INVALID",
148
+ message="Zenava profile 配置格式不合法。",
149
+ exit_code=2,
150
+ hint="请检查 ~/.zenava/profile.json。",
151
+ )
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import errno
4
+ import os
5
+ import stat
6
+ from pathlib import Path
7
+
8
+
9
+ class SecureFileReadError(Exception):
10
+ def __init__(self, reason: str) -> None:
11
+ self.reason = reason
12
+ super().__init__(reason)
13
+
14
+
15
+ def read_regular_file(path: Path | str, *, max_bytes: int) -> bytes:
16
+ """Read a bounded regular file without following path replacement races."""
17
+ source = Path(path)
18
+ try:
19
+ path_stat = os.lstat(source)
20
+ except OSError as error:
21
+ raise SecureFileReadError("unreadable") from error
22
+ if stat.S_ISLNK(path_stat.st_mode):
23
+ raise SecureFileReadError("symlink")
24
+ if not stat.S_ISREG(path_stat.st_mode):
25
+ raise SecureFileReadError("not_regular")
26
+
27
+ flags = os.O_RDONLY
28
+ flags |= getattr(os, "O_CLOEXEC", 0)
29
+ flags |= getattr(os, "O_NONBLOCK", 0)
30
+ flags |= getattr(os, "O_NOFOLLOW", 0)
31
+ try:
32
+ descriptor = os.open(source, flags)
33
+ except OSError as error:
34
+ if error.errno == errno.ELOOP:
35
+ reason = "symlink"
36
+ elif error.errno == errno.ENOENT:
37
+ reason = "changed"
38
+ else:
39
+ reason = "unreadable"
40
+ raise SecureFileReadError(reason) from error
41
+
42
+ try:
43
+ opened_stat = os.fstat(descriptor)
44
+ if not stat.S_ISREG(opened_stat.st_mode):
45
+ raise SecureFileReadError("not_regular")
46
+ if (path_stat.st_dev, path_stat.st_ino) != (
47
+ opened_stat.st_dev,
48
+ opened_stat.st_ino,
49
+ ):
50
+ raise SecureFileReadError("changed")
51
+ if opened_stat.st_size > max_bytes:
52
+ raise SecureFileReadError("too_large")
53
+
54
+ content = bytearray()
55
+ while len(content) <= max_bytes:
56
+ chunk = os.read(
57
+ descriptor,
58
+ min(64 * 1024, max_bytes + 1 - len(content)),
59
+ )
60
+ if not chunk:
61
+ break
62
+ content.extend(chunk)
63
+ if len(content) > max_bytes:
64
+ raise SecureFileReadError("too_large")
65
+ if os.fstat(descriptor).st_size > max_bytes:
66
+ raise SecureFileReadError("too_large")
67
+ return bytes(content)
68
+ except SecureFileReadError:
69
+ raise
70
+ except OSError as error:
71
+ raise SecureFileReadError("unreadable") from error
72
+ finally:
73
+ try:
74
+ os.close(descriptor)
75
+ except OSError:
76
+ pass