my-pi-agent 0.1.0

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 (141) hide show
  1. package/README.md +318 -0
  2. package/package.json +45 -0
  3. package/pyproject.toml +50 -0
  4. package/src/my_agent_core/__init__.py +123 -0
  5. package/src/my_agent_core/agent.py +441 -0
  6. package/src/my_agent_core/background.py +121 -0
  7. package/src/my_agent_core/context.py +505 -0
  8. package/src/my_agent_core/events.py +153 -0
  9. package/src/my_agent_core/extensions/__init__.py +9 -0
  10. package/src/my_agent_core/extensions/core.py +197 -0
  11. package/src/my_agent_core/hooks.py +130 -0
  12. package/src/my_agent_core/loop.py +709 -0
  13. package/src/my_agent_core/main.py +134 -0
  14. package/src/my_agent_core/memory.py +241 -0
  15. package/src/my_agent_core/message_queue.py +110 -0
  16. package/src/my_agent_core/plugins.py +212 -0
  17. package/src/my_agent_core/registry.py +186 -0
  18. package/src/my_agent_core/session/__init__.py +79 -0
  19. package/src/my_agent_core/session/entries.py +197 -0
  20. package/src/my_agent_core/session/jsonl.py +60 -0
  21. package/src/my_agent_core/session/memory.py +137 -0
  22. package/src/my_agent_core/session/session.py +400 -0
  23. package/src/my_agent_core/session/storage.py +245 -0
  24. package/src/my_agent_core/session/store.py +131 -0
  25. package/src/my_agent_core/session/tree.py +86 -0
  26. package/src/my_agent_core/skills.py +149 -0
  27. package/src/my_agent_core/subagent_tasks.py +170 -0
  28. package/src/my_agent_core/subagents.py +148 -0
  29. package/src/my_agent_core/task_store.py +248 -0
  30. package/src/my_agent_core/tool_history.py +189 -0
  31. package/src/my_agent_core/tools/__init__.py +5 -0
  32. package/src/my_agent_core/tools/builtin/__init__.py +5 -0
  33. package/src/my_agent_core/tools/builtin/task.py +30 -0
  34. package/src/my_agent_core/tools/builtin/task_tools.py +215 -0
  35. package/src/my_agent_core/tools/core.py +239 -0
  36. package/src/my_agent_llm/__init__.py +45 -0
  37. package/src/my_agent_llm/auth/__init__.py +46 -0
  38. package/src/my_agent_llm/auth/antigravity.py +209 -0
  39. package/src/my_agent_llm/auth/manager.py +259 -0
  40. package/src/my_agent_llm/auth/quota.py +56 -0
  41. package/src/my_agent_llm/auth/schema.py +94 -0
  42. package/src/my_agent_llm/client.py +116 -0
  43. package/src/my_agent_llm/config.py +17 -0
  44. package/src/my_agent_llm/events.py +84 -0
  45. package/src/my_agent_llm/models.py +195 -0
  46. package/src/my_agent_llm/providers/__init__.py +4 -0
  47. package/src/my_agent_llm/providers/_base.py +94 -0
  48. package/src/my_agent_llm/providers/anthropic.py +298 -0
  49. package/src/my_agent_llm/providers/antigravity.py +480 -0
  50. package/src/my_agent_llm/providers/deepseek.py +196 -0
  51. package/src/my_agent_llm/providers/openai.py +364 -0
  52. package/src/my_agent_llm/providers/registry.py +16 -0
  53. package/src/my_agent_llm/stream.py +218 -0
  54. package/src/my_coding_agent/__init__.py +66 -0
  55. package/src/my_coding_agent/agent.py +208 -0
  56. package/src/my_coding_agent/cli.py +78 -0
  57. package/src/my_coding_agent/file_reference.py +80 -0
  58. package/src/my_coding_agent/macro.py +408 -0
  59. package/src/my_coding_agent/mcp.py +243 -0
  60. package/src/my_coding_agent/mutation_queue.py +37 -0
  61. package/src/my_coding_agent/paths.py +119 -0
  62. package/src/my_coding_agent/permissions.py +84 -0
  63. package/src/my_coding_agent/prompt.py +54 -0
  64. package/src/my_coding_agent/rpc_server.py +2817 -0
  65. package/src/my_coding_agent/settings.py +126 -0
  66. package/src/my_coding_agent/tools/__init__.py +55 -0
  67. package/src/my_coding_agent/tools/base.py +58 -0
  68. package/src/my_coding_agent/tools/bash.py +206 -0
  69. package/src/my_coding_agent/tools/edit.py +226 -0
  70. package/src/my_coding_agent/tools/find.py +118 -0
  71. package/src/my_coding_agent/tools/grep.py +177 -0
  72. package/src/my_coding_agent/tools/ls.py +112 -0
  73. package/src/my_coding_agent/tools/read.py +113 -0
  74. package/src/my_coding_agent/tools/write.py +72 -0
  75. package/tui/README.md +27 -0
  76. package/tui/bin/my-agent.js +98 -0
  77. package/tui/dist/app.d.ts +41 -0
  78. package/tui/dist/app.js +110 -0
  79. package/tui/dist/bridge/event-translator.d.ts +92 -0
  80. package/tui/dist/bridge/event-translator.js +216 -0
  81. package/tui/dist/bridge/kernel-bridge.d.ts +48 -0
  82. package/tui/dist/bridge/kernel-bridge.js +132 -0
  83. package/tui/dist/client.d.ts +63 -0
  84. package/tui/dist/client.js +239 -0
  85. package/tui/dist/components/assistant-message.d.ts +19 -0
  86. package/tui/dist/components/assistant-message.js +90 -0
  87. package/tui/dist/components/compaction-summary-message.d.ts +19 -0
  88. package/tui/dist/components/compaction-summary-message.js +46 -0
  89. package/tui/dist/components/custom-editor.d.ts +18 -0
  90. package/tui/dist/components/custom-editor.js +56 -0
  91. package/tui/dist/components/dynamic-border.d.ts +9 -0
  92. package/tui/dist/components/dynamic-border.js +14 -0
  93. package/tui/dist/components/footer.d.ts +39 -0
  94. package/tui/dist/components/footer.js +199 -0
  95. package/tui/dist/components/header.d.ts +4 -0
  96. package/tui/dist/components/header.js +21 -0
  97. package/tui/dist/components/keys.d.ts +5 -0
  98. package/tui/dist/components/keys.js +12 -0
  99. package/tui/dist/components/login-selector.d.ts +26 -0
  100. package/tui/dist/components/login-selector.js +181 -0
  101. package/tui/dist/components/logout-selector.d.ts +19 -0
  102. package/tui/dist/components/logout-selector.js +88 -0
  103. package/tui/dist/components/model-selector.d.ts +40 -0
  104. package/tui/dist/components/model-selector.js +268 -0
  105. package/tui/dist/components/session-selector.d.ts +54 -0
  106. package/tui/dist/components/session-selector.js +393 -0
  107. package/tui/dist/components/settings-selector.d.ts +24 -0
  108. package/tui/dist/components/settings-selector.js +146 -0
  109. package/tui/dist/components/status-indicator.d.ts +25 -0
  110. package/tui/dist/components/status-indicator.js +60 -0
  111. package/tui/dist/components/theme-selector.d.ts +14 -0
  112. package/tui/dist/components/theme-selector.js +77 -0
  113. package/tui/dist/components/thinking-selector.d.ts +21 -0
  114. package/tui/dist/components/thinking-selector.js +128 -0
  115. package/tui/dist/components/tool-execution.d.ts +31 -0
  116. package/tui/dist/components/tool-execution.js +206 -0
  117. package/tui/dist/components/tree-selector.d.ts +40 -0
  118. package/tui/dist/components/tree-selector.js +173 -0
  119. package/tui/dist/components/user-message-selector.d.ts +21 -0
  120. package/tui/dist/components/user-message-selector.js +103 -0
  121. package/tui/dist/components/user-message.d.ts +5 -0
  122. package/tui/dist/components/user-message.js +15 -0
  123. package/tui/dist/index.d.ts +11 -0
  124. package/tui/dist/index.js +11 -0
  125. package/tui/dist/interactive/chat-viewport.d.ts +19 -0
  126. package/tui/dist/interactive/chat-viewport.js +41 -0
  127. package/tui/dist/interactive/components.d.ts +1 -0
  128. package/tui/dist/interactive/components.js +1 -0
  129. package/tui/dist/interactive/interactive-mode.d.ts +89 -0
  130. package/tui/dist/interactive/interactive-mode.js +1625 -0
  131. package/tui/dist/interactive/theme.d.ts +1 -0
  132. package/tui/dist/interactive/theme.js +1 -0
  133. package/tui/dist/interactive/tui-renderer.d.ts +8 -0
  134. package/tui/dist/interactive/tui-renderer.js +10 -0
  135. package/tui/dist/protocol.d.ts +78 -0
  136. package/tui/dist/protocol.js +1 -0
  137. package/tui/dist/theme/dark.json +54 -0
  138. package/tui/dist/theme/light.json +71 -0
  139. package/tui/dist/theme/theme.d.ts +20 -0
  140. package/tui/dist/theme/theme.js +86 -0
  141. package/tui/package.json +25 -0
@@ -0,0 +1,259 @@
1
+ """AuthManager: 管理 ~/.my-pi-agent/auth.json 的线程与进程安全凭据中心。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import time
10
+ from typing import Any
11
+
12
+ from filelock import FileLock # pyright: ignore[reportMissingImports]
13
+ import httpx
14
+
15
+ from my_agent_llm.auth.schema import (
16
+ ApiKeyCredential,
17
+ AuthStore,
18
+ OAuthCredential,
19
+ )
20
+
21
+ __all__ = [
22
+ "DEFAULT_ANTIGRAVITY_CLIENT_ID",
23
+ "DEFAULT_ANTIGRAVITY_CLIENT_SECRET",
24
+ "GOOGLE_TOKEN_URL",
25
+ "AuthManager",
26
+ ]
27
+
28
+ DEFAULT_ANTIGRAVITY_CLIENT_ID = os.environ.get(
29
+ "ANTIGRAVITY_CLIENT_ID",
30
+ "my-pi-agent-desktop-client-id",
31
+ )
32
+ DEFAULT_ANTIGRAVITY_CLIENT_SECRET = os.environ.get(
33
+ "ANTIGRAVITY_CLIENT_SECRET",
34
+ "my-pi-agent-desktop-client-secret",
35
+ )
36
+ GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
37
+
38
+
39
+ class AuthManager:
40
+ """产品级全局凭据管理器。
41
+
42
+ 提供线程安全与跨进程文件锁保护的 auth.json 读写、Profile 调度与 OAuth 静默续期。
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ auth_path: Path | str | None = None,
48
+ lock_path: Path | str | None = None,
49
+ lock_timeout: float = 5.0,
50
+ ) -> None:
51
+ if auth_path:
52
+ self.auth_path = Path(auth_path).resolve()
53
+ else:
54
+ home = Path(
55
+ os.environ.get("MY_AGENT_HOME")
56
+ or Path(os.environ.get("USERPROFILE") or os.environ.get("HOME") or "~").expanduser() / ".my-pi-agent"
57
+ ).resolve()
58
+ self.auth_path = home / "auth.json"
59
+
60
+ if lock_path:
61
+ self.lock_path = Path(lock_path).resolve()
62
+ else:
63
+ self.lock_path = self.auth_path.with_name(f"{self.auth_path.name}.lock")
64
+ self.lock_timeout = lock_timeout
65
+
66
+ def load_store(self) -> AuthStore:
67
+ """从 auth.json 加载并自动归一化格式。
68
+
69
+ 支持标准的 AuthStore 结构以及简写平铺格式。如果文件不存在或损坏,优雅降级为空 AuthStore。
70
+ """
71
+ if not self.auth_path.exists():
72
+ return AuthStore()
73
+
74
+ try:
75
+ content = self.auth_path.read_text(encoding="utf-8")
76
+ data = json.loads(content)
77
+ if not isinstance(data, dict):
78
+ return AuthStore()
79
+
80
+ # 兼容扁平平铺简写格式:{"deepseek": {"type": "api_key", ...}}
81
+ if "providers" not in data:
82
+ normalized_providers: dict[str, Any] = {}
83
+ for prov, item in data.items():
84
+ if isinstance(item, dict) and "type" in item:
85
+ normalized_providers[prov] = {"default": item}
86
+ active_profiles = data.get("active_profiles")
87
+ if not isinstance(active_profiles, dict):
88
+ active_profiles = {}
89
+ version = data.get("version", 1)
90
+ return AuthStore(
91
+ version=version,
92
+ active_profiles=active_profiles,
93
+ providers=normalized_providers,
94
+ )
95
+ return AuthStore.model_validate(data)
96
+ except Exception:
97
+ return AuthStore()
98
+
99
+ def save_store(self, store: AuthStore) -> None:
100
+ """以 0o600 权限与临时文件原子替换保存凭证。"""
101
+ self.auth_path.parent.mkdir(parents=True, exist_ok=True)
102
+ tmp_path = self.auth_path.with_name(f"{self.auth_path.name}.tmp")
103
+ tmp_path.write_text(store.model_dump_json(indent=2), encoding="utf-8")
104
+ with contextlib.suppress(Exception):
105
+ tmp_path.chmod(0o600)
106
+ tmp_path.replace(self.auth_path)
107
+
108
+ def set_api_key(
109
+ self,
110
+ provider: str,
111
+ key: str,
112
+ profile: str = "default",
113
+ base_url: str | None = None,
114
+ env: dict[str, str] | None = None,
115
+ **kwargs: Any,
116
+ ) -> None:
117
+ """安全设置指定提供商的 API Key 凭据。"""
118
+ self.lock_path.parent.mkdir(parents=True, exist_ok=True)
119
+ with FileLock(str(self.lock_path), timeout=self.lock_timeout):
120
+ store = self.load_store()
121
+ prov_dict = store.providers.setdefault(provider, {})
122
+ prov_dict[profile] = ApiKeyCredential(
123
+ key=key,
124
+ base_url=base_url,
125
+ env=env or {},
126
+ )
127
+ store.active_profiles.setdefault(provider, profile)
128
+ self.save_store(store)
129
+
130
+ def set_oauth(
131
+ self,
132
+ provider: str,
133
+ access: str,
134
+ refresh: str,
135
+ expires: int,
136
+ profile: str = "default",
137
+ project_id: str = "aicode-consumers",
138
+ email: str | None = None,
139
+ client_id: str | None = None,
140
+ client_secret: str | None = None,
141
+ **kwargs: Any,
142
+ ) -> None:
143
+ """安全设置指定提供商的 OAuth 凭据。"""
144
+ self.lock_path.parent.mkdir(parents=True, exist_ok=True)
145
+ with FileLock(str(self.lock_path), timeout=self.lock_timeout):
146
+ store = self.load_store()
147
+ prov_dict = store.providers.setdefault(provider, {})
148
+ prov_dict[profile] = OAuthCredential(
149
+ access=access,
150
+ refresh=refresh,
151
+ expires=expires,
152
+ project_id=project_id,
153
+ email=email,
154
+ client_id=client_id,
155
+ client_secret=client_secret,
156
+ )
157
+ store.active_profiles.setdefault(provider, profile)
158
+ self.save_store(store)
159
+
160
+ def get_credential(self, provider: str, profile: str | None = None) -> ApiKeyCredential | OAuthCredential | None:
161
+ """获取指定提供商在指定 Profile 下的凭证(缺省为 active profile 或 default)。"""
162
+ store = self.load_store()
163
+ profile_name = profile or store.active_profiles.get(provider, "default")
164
+ return store.providers.get(provider, {}).get(profile_name)
165
+
166
+ def remove_credential(self, provider: str, profile: str | None = None) -> bool:
167
+ """安全删除指定提供商的凭据。
168
+
169
+ 若未指定 profile,则删除该 provider 下的所有凭据及 active_profiles 映射;
170
+ 若指定了 profile,则删除对应 Profile 的凭据;若删除后该 provider 无其他 Profile,
171
+ 则同时清理其映射。
172
+ 返回 True 表示成功删除,False 表示目标凭据不存在。
173
+ """
174
+ self.lock_path.parent.mkdir(parents=True, exist_ok=True)
175
+ with FileLock(str(self.lock_path), timeout=self.lock_timeout):
176
+ store = self.load_store()
177
+ removed = False
178
+ if profile is None:
179
+ if provider in store.providers:
180
+ del store.providers[provider]
181
+ removed = True
182
+ if provider in store.active_profiles:
183
+ del store.active_profiles[provider]
184
+ removed = True
185
+ else:
186
+ if provider in store.providers and profile in store.providers[provider]:
187
+ del store.providers[provider][profile]
188
+ removed = True
189
+ if not store.providers[provider]:
190
+ del store.providers[provider]
191
+ if store.active_profiles.get(provider) == profile:
192
+ if provider in store.providers and store.providers[provider]:
193
+ store.active_profiles[provider] = next(iter(store.providers[provider]))
194
+ else:
195
+ store.active_profiles.pop(provider, None)
196
+ if removed:
197
+ self.save_store(store)
198
+ return removed
199
+
200
+ async def get_valid_token(self, provider: str, profile: str | None = None) -> str:
201
+ """获取有效的调用 Token,自动完成 API Key 变量解析或 OAuth 静默刷新。"""
202
+ self.lock_path.parent.mkdir(parents=True, exist_ok=True)
203
+ with FileLock(str(self.lock_path), timeout=max(self.lock_timeout, 10.0)):
204
+ store = self.load_store()
205
+ profile_name = profile or store.active_profiles.get(provider, "default")
206
+ prov_dict = store.providers.get(provider, {})
207
+ cred = prov_dict.get(profile_name)
208
+
209
+ if not cred:
210
+ raise RuntimeError(f"未找到提供商 '{provider}' (Profile: {profile_name}) 的有效凭据,请先配置或登录。")
211
+
212
+ if isinstance(cred, ApiKeyCredential):
213
+ return cred.resolve_key()
214
+
215
+ if isinstance(cred, OAuthCredential):
216
+ if not cred.is_expired():
217
+ return cred.access
218
+
219
+ # 执行 OAuth 静默刷新
220
+ client_id = cred.client_id or DEFAULT_ANTIGRAVITY_CLIENT_ID
221
+ client_secret = cred.client_secret or DEFAULT_ANTIGRAVITY_CLIENT_SECRET
222
+ try:
223
+ async with httpx.AsyncClient(timeout=15.0) as client:
224
+ resp = await client.post(
225
+ GOOGLE_TOKEN_URL,
226
+ data={
227
+ "client_id": client_id,
228
+ "client_secret": client_secret,
229
+ "refresh_token": cred.refresh,
230
+ "grant_type": "refresh_token",
231
+ },
232
+ )
233
+ if resp.status_code != 200:
234
+ raise RuntimeError(f"OAuth Token 自动刷新失败 ({resp.status_code}): {resp.text}")
235
+ data = resp.json()
236
+ except httpx.RequestError as exc:
237
+ raise RuntimeError(f"OAuth Token 自动刷新网络连接失败: {exc}") from exc
238
+
239
+ new_access = data["access_token"]
240
+ expires_in = data.get("expires_in", 3600)
241
+ try:
242
+ exp_seconds = int(expires_in)
243
+ now_ms = int(time.time() * 1000)
244
+ except (ValueError, TypeError):
245
+ exp_seconds = 3600
246
+ now_ms = 0
247
+ update_kwargs: dict[str, Any] = {
248
+ "access": new_access,
249
+ "expires": now_ms + (exp_seconds - 300) * 1000,
250
+ }
251
+ if "refresh_token" in data and data["refresh_token"]:
252
+ update_kwargs["refresh"] = data["refresh_token"]
253
+
254
+ updated_cred = cred.model_copy(update=update_kwargs)
255
+ prov_dict[profile_name] = updated_cred
256
+ self.save_store(store)
257
+ return new_access
258
+
259
+ raise RuntimeError(f"未知的凭据类型: {type(cred)}")
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import httpx
6
+
7
+ from .antigravity import (
8
+ ANTIGRAVITY_USER_AGENT,
9
+ DEFAULT_ANTIGRAVITY_ENDPOINT,
10
+ AntigravityCredentials,
11
+ )
12
+
13
+
14
+ @dataclass
15
+ class QuotaBucket:
16
+ """单个模型或操作类型的配额桶。"""
17
+
18
+ bucket_id: str
19
+ display_name: str
20
+ remaining_fraction: float
21
+ reset_time: str | None = None
22
+
23
+ @property
24
+ def remaining_percent(self) -> int:
25
+ """配额剩余百分比(0-100 整数)。"""
26
+ return int(round(max(0.0, min(1.0, self.remaining_fraction)) * 100))
27
+
28
+
29
+ def retrieve_user_quota_summary(creds: AntigravityCredentials) -> list[QuotaBucket]:
30
+ """向 Google Cloud Code 网关查询当前用户各模型配额余量。"""
31
+ url = f"{DEFAULT_ANTIGRAVITY_ENDPOINT}/v1internal:retrieveUserQuotaSummary"
32
+ headers = {
33
+ "Authorization": f"Bearer {creds.access_token}",
34
+ "x-goog-user-project": creds.project_id,
35
+ "User-Agent": ANTIGRAVITY_USER_AGENT,
36
+ "Content-Type": "application/json",
37
+ }
38
+ try:
39
+ res = httpx.post(url, headers=headers, json={}, timeout=10.0)
40
+ if res.status_code != 200:
41
+ return []
42
+ data = res.json()
43
+ buckets: list[QuotaBucket] = []
44
+ for group in data.get("groups", []):
45
+ for b in group.get("buckets", []):
46
+ buckets.append(
47
+ QuotaBucket(
48
+ bucket_id=b.get("bucketId", ""),
49
+ display_name=b.get("displayName", ""),
50
+ remaining_fraction=float(b.get("remainingFraction", 0.0)),
51
+ reset_time=b.get("resetTime"),
52
+ )
53
+ )
54
+ return buckets
55
+ except Exception:
56
+ return []
@@ -0,0 +1,94 @@
1
+ """AuthStore 强类型凭证实体模型与模式定义。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+ import os
7
+ import shlex
8
+ import subprocess
9
+ import time
10
+ from typing import Annotated, Literal, Union
11
+ from pydantic import BaseModel, Field
12
+
13
+ __all__ = [
14
+ "CredentialType",
15
+ "ApiKeyCredential",
16
+ "OAuthCredential",
17
+ "Credential",
18
+ "AuthStore",
19
+ ]
20
+
21
+
22
+ class CredentialType(str, Enum):
23
+ """凭证类型枚举。"""
24
+
25
+ API_KEY = "api_key"
26
+ OAUTH = "oauth"
27
+
28
+
29
+ class ApiKeyCredential(BaseModel):
30
+ """API Key 凭据模型。"""
31
+
32
+ type: Literal[CredentialType.API_KEY, "api_key"] = CredentialType.API_KEY
33
+ key: str
34
+ base_url: str | None = None
35
+ env: dict[str, str] = Field(default_factory=dict)
36
+
37
+ def resolve_key(self) -> str:
38
+ """解析 API Key:处理字面量、!command 子进程输出与 $ENV_VAR/${ENV_VAR} 语法。"""
39
+ raw = self.key.strip()
40
+ if raw.startswith("!"):
41
+ cmd = raw[1:].strip()
42
+ if os.name == "nt":
43
+ parts = [
44
+ p.strip('"') if (p.startswith('"') and p.endswith('"')) else p
45
+ for p in shlex.split(cmd, posix=False)
46
+ ]
47
+ else:
48
+ parts = shlex.split(cmd)
49
+ if not parts:
50
+ return ""
51
+ res = subprocess.run(parts, capture_output=True, text=True, check=True)
52
+ return res.stdout.strip()
53
+ if raw.startswith("$"):
54
+ var_name = raw[1:].strip("{}")
55
+ return os.environ.get(var_name, "")
56
+ return raw
57
+
58
+
59
+ class OAuthCredential(BaseModel):
60
+ """OAuth 凭据模型(支持 Google Cloud Code / Antigravity)。"""
61
+
62
+ type: Literal[CredentialType.OAUTH, "oauth"] = CredentialType.OAUTH
63
+ access: str
64
+ refresh: str
65
+ expires: int = 0 # 毫秒时间戳,<=0 表示永久或不计算过期
66
+ project_id: str = "aicode-consumers"
67
+ email: str | None = None
68
+ client_id: str | None = None
69
+ client_secret: str | None = None
70
+
71
+ def is_expired(self, buffer_ms: int = 300_000) -> bool:
72
+ """判定凭证是否已过期。
73
+
74
+ 默认包含 5 分钟 (300,000ms) 缓冲期,保证在真正过期前完成自动刷新。
75
+ 当 expires <= 0 时视为不计算过期。
76
+ """
77
+ if self.expires <= 0:
78
+ return False
79
+ return self.expires <= int(time.time() * 1000) + buffer_ms
80
+
81
+
82
+ # 使用 Pydantic 区分联合体,基于 `type` 字段精准反序列化
83
+ Credential = Annotated[
84
+ Union[ApiKeyCredential, OAuthCredential],
85
+ Field(discriminator="type"),
86
+ ]
87
+
88
+
89
+ class AuthStore(BaseModel):
90
+ """全局统一凭证存储实体(映射 ~/.my-pi-agent/auth.json)。"""
91
+
92
+ version: int = 1
93
+ active_profiles: dict[str, str] = Field(default_factory=dict)
94
+ providers: dict[str, dict[str, Credential]] = Field(default_factory=dict)
@@ -0,0 +1,116 @@
1
+ # pyright: reportUnreachable=false
2
+ """LLM 门面:按 provider 路由到对应实现,对外一套 API,只透传不碰 SDK。"""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import AsyncIterator, Iterator
7
+ from typing import Any
8
+
9
+ from .config import Config # pyright: ignore[reportMissingImports]
10
+ from .events import StreamEvent # pyright: ignore[reportMissingImports]
11
+ from .models import ( # pyright: ignore[reportMissingImports]
12
+ Message,
13
+ Response,
14
+ StreamChunk,
15
+ )
16
+ from .providers import Provider # pyright: ignore[reportMissingImports]
17
+ from .providers.registry import (
18
+ PROVIDER_REGISTRY, # pyright: ignore[reportMissingImports]
19
+ )
20
+
21
+
22
+ class LLM:
23
+ """统一 LLM 客户端门面:严格接收强类型 Config 配置,多态路由至具体 Provider。"""
24
+
25
+ def __init__(self, config: Config) -> None:
26
+ """标准工程构造:严格接收 Config 实例。"""
27
+ if not isinstance(config, Config): # pyright: ignore[reportUnreachable]
28
+ raise TypeError(f"LLM expects a Config instance, got {type(config).__name__}") # pyright: ignore[reportUnreachable]
29
+ if config.provider not in PROVIDER_REGISTRY:
30
+ raise ValueError(f"Unknown provider '{config.provider}'. Available: {', '.join(sorted(PROVIDER_REGISTRY))}")
31
+ if not config.api_key and config.provider != "antigravity":
32
+ raise ValueError(f"No API key for provider: {config.provider}")
33
+ provider_cls = PROVIDER_REGISTRY[config.provider]
34
+ self._provider: Provider = provider_cls(config)
35
+ self.config = config
36
+
37
+ @property
38
+ def model(self) -> str:
39
+ """当前配置的模型名。"""
40
+ if not self.config.model:
41
+ raise ValueError("No model specified. Pass model=... or set Config.model.")
42
+ return self.config.model
43
+
44
+ def _resolve_kwargs(self, kwargs: dict[str, Any]) -> dict[str, Any]:
45
+ """统一合并调用参数与全局 Config 中的默认采样配置。"""
46
+ opts = dict(kwargs)
47
+ if self.config.temperature is not None:
48
+ opts.setdefault("temperature", self.config.temperature)
49
+ if self.config.max_tokens is not None:
50
+ opts.setdefault("max_tokens", self.config.max_tokens)
51
+ return opts
52
+
53
+ def chat(
54
+ self,
55
+ messages: list[Message],
56
+ *,
57
+ tools: list[dict] | None = None,
58
+ model: str | None = None,
59
+ **kwargs: Any,
60
+ ) -> Response:
61
+ """同步对话:完整历史 + 可选工具。"""
62
+ opts = self._resolve_kwargs(kwargs)
63
+ return self._provider.chat(messages, model=model or self.model, tools=tools, **opts)
64
+
65
+ def stream(
66
+ self,
67
+ messages: list[Message],
68
+ *,
69
+ tools: list[dict] | None = None,
70
+ model: str | None = None,
71
+ **kwargs: Any,
72
+ ) -> Iterator[StreamChunk]:
73
+ """同步流式。"""
74
+ opts = self._resolve_kwargs(kwargs)
75
+ return self._provider.stream(messages, model=model or self.model, tools=tools, **opts)
76
+
77
+ async def achat(
78
+ self,
79
+ messages: list[Message],
80
+ *,
81
+ tools: list[dict] | None = None,
82
+ model: str | None = None,
83
+ **kwargs: Any,
84
+ ) -> Response:
85
+ """异步对话。"""
86
+ opts = self._resolve_kwargs(kwargs)
87
+ return await self._provider.achat(messages, model=model or self.model, tools=tools, **opts)
88
+
89
+ async def achat_stream(
90
+ self,
91
+ messages: list[Message],
92
+ *,
93
+ tools: list[dict] | None = None,
94
+ model: str | None = None,
95
+ **kwargs: Any,
96
+ ) -> AsyncIterator[StreamChunk]:
97
+ """异步流式。调用方直接 `async for chunk in llm.achat_stream(...)` 迭代,不 await。"""
98
+ opts = self._resolve_kwargs(kwargs)
99
+ async for chunk in self._provider.achat_stream(messages, model=model or self.model, tools=tools, **opts):
100
+ yield chunk
101
+
102
+ async def astream_events(
103
+ self,
104
+ messages: list[Message],
105
+ *,
106
+ tools: list[dict] | None = None,
107
+ model: str | None = None,
108
+ signal: Any | None = None,
109
+ **kwargs: Any,
110
+ ) -> AsyncIterator[StreamEvent]:
111
+ """异步高阶流式事件流:直接产出 StreamStartEvent/TextDeltaEvent/StreamDoneEvent/StreamErrorEvent。"""
112
+ opts = self._resolve_kwargs(kwargs)
113
+ async for ev in self._provider.astream_events(
114
+ messages, model=model or self.model, tools=tools, signal=signal, **opts
115
+ ):
116
+ yield ev
@@ -0,0 +1,17 @@
1
+ """LLM 客户端配置:集中校验、可复用、不可变。"""
2
+ from pydantic import BaseModel, ConfigDict, Field
3
+
4
+
5
+ class Config(BaseModel):
6
+ """统一配置:provider/model/api_key/采样参数/网络参数。"""
7
+
8
+ provider: str = "openai"
9
+ model: str | None = None
10
+ api_key: str | None = None
11
+ temperature: float = Field(default=0.7, ge=0.0, le=2.0)
12
+ max_tokens: int | None = Field(default=None, ge=1)
13
+ timeout: int = Field(default=30, ge=1)
14
+ max_retries: int = Field(default=3, ge=0)
15
+ base_url: str | None = None
16
+
17
+ model_config = ConfigDict(frozen=True)
@@ -0,0 +1,84 @@
1
+ """模型边界层高阶流式事件模型(对标 Tau provider_events)。
2
+
3
+ 提供供应商中立的高阶流式生命周期事件,携带实时累积的 partial: Message 快照。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+
10
+ from my_agent_llm.models import Message, ToolCall
11
+
12
+ __all__ = [
13
+ "StreamDoneEvent",
14
+ "StreamErrorEvent",
15
+ "StreamEvent",
16
+ "StreamStartEvent",
17
+ "TextDeltaEvent",
18
+ "ThinkingDeltaEvent",
19
+ "ToolCallDeltaEvent",
20
+ "ToolCallDoneEvent",
21
+ ]
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class StreamEvent:
26
+ """高阶流式事件基类。"""
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class StreamStartEvent(StreamEvent):
31
+ """首个 Token 生成时发射,携带初始 partial Message 快照。"""
32
+
33
+ partial: Message
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class TextDeltaEvent(StreamEvent):
38
+ """正文文本增量事件。"""
39
+
40
+ delta: str
41
+ partial: Message
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class ThinkingDeltaEvent(StreamEvent):
46
+ """思维链推理增量事件(对标 DeepSeek-R1 / Claude 3.7 Thinking)。"""
47
+
48
+ delta: str
49
+ partial: Message
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class ToolCallDeltaEvent(StreamEvent):
54
+ """工具参数增量事件。"""
55
+
56
+ index: int
57
+ delta: str
58
+ partial: Message
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class ToolCallDoneEvent(StreamEvent):
63
+ """单个工具调用参数组装完毕事件。"""
64
+
65
+ index: int
66
+ tool_call: ToolCall
67
+ partial: Message
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class StreamDoneEvent(StreamEvent):
72
+ """流式正常结束事件,携带完整完型的 Message 实体与 Usage。"""
73
+
74
+ message: Message
75
+ usage: dict[str, int] | None = None
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class StreamErrorEvent(StreamEvent):
80
+ """流式异常或中途取消事件(Never-Throw 保证)。"""
81
+
82
+ error: Message
83
+ stop_reason: str = "error" # "error" | "cancelled"
84
+ exc: Exception | None = None