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,195 @@
1
+ """统一数据模型:保证内部数据流通,屏蔽 provider 差异。"""
2
+
3
+ import json
4
+ from enum import Enum
5
+ from typing import Any, Literal
6
+
7
+ from pydantic import BaseModel, Field, model_validator
8
+
9
+
10
+ class TurnOutcome(str, Enum):
11
+ """Provider 中立的单轮终止原因(对标 pig-llm)。"""
12
+
13
+ COMPLETED = "completed"
14
+ TOOL_CALLS = "tool_calls"
15
+ LENGTH = "length"
16
+ CONTENT_FILTER = "content_filter"
17
+ ABORTED = "aborted"
18
+ PROVIDER_ERROR = "provider_error"
19
+ UNKNOWN = "unknown"
20
+
21
+
22
+ _COMPLETED_REASONS = {
23
+ "stop",
24
+ "end_turn",
25
+ "complete",
26
+ "completed",
27
+ "stop_sequence",
28
+ "natural",
29
+ "success",
30
+ "done",
31
+ }
32
+ _TOOL_REASONS = {"tool_calls", "tool_use", "function_call", "function_calls"}
33
+ _LENGTH_REASONS = {
34
+ "length",
35
+ "max_tokens",
36
+ "max_output_tokens",
37
+ "model_length",
38
+ "token_limit",
39
+ }
40
+ _FILTER_REASONS = {"content_filter", "safety", "blocked", "recitation", "prohibited"}
41
+ _ABORT_REASONS = {"aborted", "cancelled", "canceled", "interrupt", "interrupted"}
42
+ _ERROR_REASONS = {"error", "failed", "failure", "provider_error"}
43
+
44
+
45
+ def normalize_finish_reason(
46
+ reason: str | None, has_tool_calls: bool = False
47
+ ) -> TurnOutcome:
48
+ """归一化各厂商私有 finish_reason 为中立枚举。"""
49
+ if reason is None:
50
+ return TurnOutcome.TOOL_CALLS if has_tool_calls else TurnOutcome.COMPLETED
51
+ norm = reason.strip().lower().rsplit(".", maxsplit=1)[-1]
52
+ if norm in _COMPLETED_REASONS:
53
+ return TurnOutcome.TOOL_CALLS if has_tool_calls else TurnOutcome.COMPLETED
54
+ if norm in _TOOL_REASONS:
55
+ return TurnOutcome.TOOL_CALLS
56
+ if norm in _LENGTH_REASONS:
57
+ return TurnOutcome.LENGTH
58
+ if norm in _FILTER_REASONS:
59
+ return TurnOutcome.CONTENT_FILTER
60
+ if norm in _ABORT_REASONS:
61
+ return TurnOutcome.ABORTED
62
+ if norm in _ERROR_REASONS:
63
+ return TurnOutcome.PROVIDER_ERROR
64
+ return TurnOutcome.TOOL_CALLS if has_tool_calls else TurnOutcome.UNKNOWN
65
+
66
+
67
+ class Message(BaseModel):
68
+ """统一消息:role + content + 附加元数据(tool_calls / tool_call_id 等)。"""
69
+
70
+ role: Literal["system", "developer", "user", "assistant", "tool"]
71
+ content: str
72
+ metadata: dict[str, Any] | None = None
73
+
74
+
75
+ class ToolCallFunction(BaseModel):
76
+ """tool_call 的 function 子对象(保留以兼容历史导入)。"""
77
+
78
+ name: str
79
+ arguments: str
80
+
81
+
82
+ class ToolCall(BaseModel):
83
+ """统一结构化工具调用对象(对标 Tau / Pi)。
84
+
85
+ 参数在模型层完成反序列化,核心层直接消费字典,彻底告别四重 JSON 编解码。
86
+ """
87
+
88
+ id: str
89
+ name: str
90
+ args: dict[str, Any] = Field(default_factory=dict)
91
+ error: str | None = None
92
+
93
+ @model_validator(mode="before")
94
+ @classmethod
95
+ def _normalize_wire_dict(cls, data: Any) -> Any:
96
+ if isinstance(data, dict) and "function" in data and "name" not in data:
97
+ fn = data.get("function")
98
+ if isinstance(fn, dict):
99
+ name = fn.get("name", "")
100
+ raw_args = fn.get("arguments", "{}")
101
+ else:
102
+ name = getattr(fn, "name", "")
103
+ raw_args = getattr(fn, "arguments", "{}")
104
+ error = None
105
+ if isinstance(raw_args, str):
106
+ try:
107
+ if raw_args.strip():
108
+ parsed = json.loads(raw_args)
109
+ if isinstance(parsed, dict):
110
+ args = parsed
111
+ else:
112
+ error = f"Tool arguments must be a dict, got {type(parsed).__name__}"
113
+ args = {}
114
+ else:
115
+ error = "Malformed JSON arguments: empty string"
116
+ args = {}
117
+ except Exception as exc:
118
+ args = {}
119
+ error = f"Malformed JSON arguments: {exc}"
120
+ elif isinstance(raw_args, dict):
121
+ args = raw_args
122
+ else:
123
+ args = {}
124
+ return {
125
+ "id": data.get("id", ""),
126
+ "name": name,
127
+ "args": args,
128
+ "error": error,
129
+ }
130
+ return data
131
+
132
+ def to_wire_dict(self) -> dict[str, Any]:
133
+ """兼容 OpenAI wire 形状 dict(用于对外导出或与旧协议对接)。"""
134
+ return {
135
+ "id": self.id,
136
+ "type": "function",
137
+ "function": {
138
+ "name": self.name,
139
+ "arguments": json.dumps(self.args, ensure_ascii=False),
140
+ },
141
+ }
142
+
143
+
144
+ class Response(BaseModel):
145
+ """统一响应:文本 + 工具调用 + usage + reasoning。"""
146
+
147
+ content: str
148
+ model: str
149
+ tool_calls: list[ToolCall] | None = None
150
+ reasoning_content: str | None = None
151
+ usage: dict[str, int] | None = None
152
+ finish_reason: str | None = None
153
+
154
+ @property
155
+ def outcome(self) -> TurnOutcome:
156
+ """中立化的终止状态。"""
157
+ return normalize_finish_reason(self.finish_reason, bool(self.tool_calls))
158
+
159
+ def to_message(
160
+ self,
161
+ role: Literal["system", "developer", "user", "assistant", "tool"] = "assistant",
162
+ stop_reason: str | None = None,
163
+ ) -> Message:
164
+ """将模型层完整响应直接转换为标准 Message 实体,彻底消除调度层手动累加拼装。"""
165
+ meta: dict[str, Any] = {}
166
+ if self.tool_calls:
167
+ meta["tool_calls"] = [
168
+ tc.model_dump() if hasattr(tc, "model_dump") else tc
169
+ for tc in self.tool_calls
170
+ ]
171
+ if self.usage:
172
+ meta["usage"] = self.usage
173
+ if self.reasoning_content:
174
+ meta["reasoning_content"] = self.reasoning_content
175
+ effective_stop = stop_reason or self.outcome.value
176
+ if effective_stop:
177
+ meta["stop_reason"] = effective_stop
178
+ return Message(role=role, content=self.content, metadata=meta if meta else None)
179
+
180
+
181
+ class StreamChunk(BaseModel):
182
+ """流式增量块:文本增量 + 末块携带完整 tool_calls 与终态已拼装好的 Response。"""
183
+
184
+ content: str
185
+ finish_reason: str | None = None
186
+ tool_calls: list[ToolCall] | None = None
187
+ usage: dict[str, int] | None = None
188
+ metadata: dict[str, Any] | None = None
189
+ response: Response | None = None
190
+
191
+ @property
192
+ def outcome(self) -> TurnOutcome | None:
193
+ if self.finish_reason is None and not self.tool_calls:
194
+ return None
195
+ return normalize_finish_reason(self.finish_reason, bool(self.tool_calls))
@@ -0,0 +1,4 @@
1
+ """Provider 实现集合。"""
2
+ from ._base import Provider
3
+
4
+ __all__ = ["Provider"]
@@ -0,0 +1,94 @@
1
+ """Provider 抽象基类:统一接口,翻译全在子类内部。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
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
+
17
+
18
+ class Provider(ABC):
19
+ """各 provider 的统一接口。"""
20
+
21
+ @abstractmethod
22
+ def __init__(self, config: Config):
23
+ """统一构造契约:所有 provider 都收 Config。"""
24
+
25
+ @abstractmethod
26
+ def chat(
27
+ self,
28
+ messages: list[Message],
29
+ *,
30
+ model: str,
31
+ tools: list[dict] | None = None,
32
+ **kwargs,
33
+ ) -> Response:
34
+ """同步对话。"""
35
+
36
+ @abstractmethod
37
+ def stream(
38
+ self,
39
+ messages: list[Message],
40
+ *,
41
+ model: str,
42
+ tools: list[dict] | None = None,
43
+ **kwargs,
44
+ ) -> Iterator[StreamChunk]:
45
+ """同步流式。"""
46
+
47
+ @abstractmethod
48
+ async def achat(
49
+ self,
50
+ messages: list[Message],
51
+ *,
52
+ model: str,
53
+ tools: list[dict] | None = None,
54
+ **kwargs,
55
+ ) -> Response:
56
+ """异步对话。"""
57
+
58
+ @abstractmethod
59
+ async def achat_stream(
60
+ self,
61
+ messages: list[Message],
62
+ *,
63
+ model: str,
64
+ tools: list[dict] | None = None,
65
+ **kwargs,
66
+ ) -> AsyncIterator[StreamChunk]:
67
+ """异步流式。"""
68
+ yield StreamChunk(
69
+ content=""
70
+ ) # 抽象标记:子类必须实现为异步生成器(基类永不执行)
71
+
72
+ async def astream_events(
73
+ self,
74
+ messages: list[Message],
75
+ *,
76
+ model: str,
77
+ tools: list[dict] | None = None,
78
+ signal: Any | None = None,
79
+ **kwargs,
80
+ ) -> AsyncIterator[StreamEvent]:
81
+ """异步高阶流式事件流(对标 Tau stream_response)。
82
+
83
+ 默认实现:使用 StreamAccumulator 包装底层 achat_stream 原始流。
84
+ """
85
+ from ..stream import ( # pyright: ignore[reportMissingImports]
86
+ StreamAccumulator,
87
+ )
88
+
89
+ acc = StreamAccumulator()
90
+ async for ev in acc.stream(
91
+ self.achat_stream(messages, model=model, tools=tools, **kwargs),
92
+ signal=signal,
93
+ ):
94
+ yield ev
@@ -0,0 +1,298 @@
1
+ # pyright: reportArgumentType=false, reportCallIssue=false
2
+ """Anthropic provider:block 双向翻译 + 原生 web_search 增强。"""
3
+
4
+ import json
5
+ from collections.abc import AsyncIterator, Iterator
6
+
7
+ import anthropic
8
+
9
+ from ..config import Config
10
+ from ..models import Message, Response, StreamChunk, ToolCall
11
+ from ._base import Provider
12
+
13
+
14
+ class AnthropicProvider(Provider):
15
+ """Anthropic (Claude) provider 实现。"""
16
+
17
+ def __init__(self, config: Config, client=None, async_client=None):
18
+ """初始化。client/async_client 可注入(测试缝隙)。"""
19
+ self.config = config
20
+ if client is not None:
21
+ self.client = client
22
+ self.async_client = async_client
23
+ return
24
+ kwargs = {
25
+ "api_key": config.api_key,
26
+ "timeout": config.timeout,
27
+ "max_retries": config.max_retries,
28
+ }
29
+ if config.base_url:
30
+ kwargs["base_url"] = config.base_url
31
+ self.client = anthropic.Anthropic(**kwargs)
32
+ self.async_client = anthropic.AsyncAnthropic(**kwargs)
33
+
34
+ def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[dict]]:
35
+ """Message → Anthropic 格式。返回 (system, messages)。"""
36
+ system_message = None
37
+ anthropic_messages = []
38
+ for msg in messages:
39
+ if msg.role == "system":
40
+ system_message = msg.content
41
+ elif msg.role == "assistant" and msg.metadata and "tool_calls" in msg.metadata:
42
+ content = []
43
+ if msg.content:
44
+ content.append({"type": "text", "text": msg.content})
45
+ for tc in msg.metadata["tool_calls"]:
46
+ if isinstance(tc, ToolCall):
47
+ tc_id = tc.id
48
+ tc_name = tc.name
49
+ tc_args = tc.args
50
+ elif isinstance(tc, dict):
51
+ tc_id = tc.get("id", "")
52
+ if "name" in tc and "args" in tc:
53
+ tc_name = tc["name"]
54
+ tc_args = tc["args"]
55
+ elif "function" in tc:
56
+ tc_name = tc["function"].get("name", "")
57
+ raw = tc["function"].get("arguments", "{}")
58
+ try:
59
+ tc_args = (
60
+ json.loads(raw)
61
+ if isinstance(raw, str) and raw.strip()
62
+ else (raw if isinstance(raw, dict) else {})
63
+ )
64
+ except Exception:
65
+ tc_args = {}
66
+ else:
67
+ tc_name = tc.get("name", "")
68
+ tc_args = tc.get("args", {})
69
+ else:
70
+ tc_id = getattr(tc, "id", "")
71
+ tc_name = getattr(tc, "name", "")
72
+ tc_args = getattr(tc, "args", {})
73
+ content.append(
74
+ {
75
+ "type": "tool_use",
76
+ "id": tc_id,
77
+ "name": tc_name,
78
+ "input": tc_args,
79
+ }
80
+ )
81
+ anthropic_messages.append({"role": "assistant", "content": content})
82
+ elif msg.role == "tool" and msg.metadata:
83
+ anthropic_messages.append(
84
+ {
85
+ "role": "user",
86
+ "content": [
87
+ {
88
+ "type": "tool_result",
89
+ "tool_use_id": msg.metadata.get("tool_call_id"),
90
+ "content": msg.content,
91
+ }
92
+ ],
93
+ }
94
+ )
95
+ else:
96
+ anthropic_messages.append({"role": msg.role, "content": msg.content})
97
+ return system_message, anthropic_messages
98
+
99
+ def _convert_tools(self, tools: list[dict] | None) -> list[dict] | None:
100
+ """OpenAI 形状 tools → Anthropic 格式。"""
101
+ if not tools:
102
+ return None
103
+ out = []
104
+ for tool in tools:
105
+ if tool.get("type") == "function":
106
+ func = tool["function"]
107
+ out.append(
108
+ {
109
+ "name": func["name"],
110
+ "description": func.get("description", ""),
111
+ "input_schema": func.get("parameters", {}),
112
+ }
113
+ )
114
+ return out or None
115
+
116
+ def _resolve_tools(self, tools, kwargs) -> list[dict] | None:
117
+ """function tools + 可选原生 web_search。"""
118
+ enable_web = kwargs.pop("enable_web_search", False)
119
+ max_uses = kwargs.pop("web_search_max_uses", 5)
120
+ out = self._convert_tools(tools) or []
121
+ if enable_web:
122
+ out = list(out)
123
+ out.append(
124
+ {
125
+ "type": "web_search_20250305",
126
+ "name": "web_search",
127
+ "max_uses": max_uses,
128
+ }
129
+ )
130
+ return out or None
131
+
132
+ @staticmethod
133
+ def _extract_content(blocks) -> str:
134
+ """text blocks → content。"""
135
+ return "".join(b.text for b in blocks if getattr(b, "type", None) == "text")
136
+
137
+ @staticmethod
138
+ def _extract_reasoning(blocks) -> str | None:
139
+ """thinking blocks → reasoning_content。"""
140
+ parts = [b.thinking for b in blocks if getattr(b, "type", None) == "thinking" and getattr(b, "thinking", None)]
141
+ return "".join(parts) or None
142
+
143
+ @staticmethod
144
+ def _extract_tool_calls(blocks) -> list[ToolCall] | None:
145
+ """tool_use blocks → 统一 ToolCall。原生使用 block.input (dict),消灭冗余 dumps。"""
146
+ out = []
147
+ for block in blocks:
148
+ if getattr(block, "type", None) == "tool_use":
149
+ input_args = getattr(block, "input", None)
150
+ args = input_args if isinstance(input_args, dict) else {}
151
+ out.append(
152
+ ToolCall(
153
+ id=getattr(block, "id", ""),
154
+ name=getattr(block, "name", ""),
155
+ args=args,
156
+ )
157
+ )
158
+ return out or None
159
+
160
+ @staticmethod
161
+ def _extract_usage(response) -> dict[str, int] | None:
162
+ """从 Anthropic 响应提取完整 usage(含 Prompt 缓存读取与写入)。"""
163
+ u = getattr(response, "usage", None)
164
+ if u is None:
165
+ return None
166
+ try:
167
+ in_t = int(getattr(u, "input_tokens", 0) or 0)
168
+ out_t = int(getattr(u, "output_tokens", 0) or 0)
169
+ cache_read = int(getattr(u, "cache_read_input_tokens", 0) or 0)
170
+ cache_write = int(getattr(u, "cache_creation_input_tokens", 0) or 0)
171
+ except (TypeError, ValueError):
172
+ in_t, out_t, cache_read, cache_write = 0, 0, 0, 0
173
+ res = {
174
+ "prompt_tokens": in_t,
175
+ "completion_tokens": out_t,
176
+ "total_tokens": in_t + out_t + cache_read,
177
+ }
178
+ if cache_read > 0:
179
+ res["cache_read_tokens"] = cache_read
180
+ if cache_write > 0:
181
+ res["cache_write_tokens"] = cache_write
182
+ return res
183
+
184
+ def chat(self, messages, *, model, tools=None, **kwargs) -> Response:
185
+ system, ant_messages = self._convert_messages(messages)
186
+ ant_tools = self._resolve_tools(tools, kwargs)
187
+ kwargs.pop("tools", None)
188
+ response = self.client.messages.create(
189
+ model=model,
190
+ messages=ant_messages,
191
+ system=system,
192
+ max_tokens=kwargs.pop("max_tokens", 4096),
193
+ **({"tools": ant_tools} if ant_tools else {}),
194
+ **kwargs,
195
+ )
196
+ return Response(
197
+ content=self._extract_content(response.content),
198
+ model=response.model,
199
+ reasoning_content=self._extract_reasoning(response.content),
200
+ usage=self._extract_usage(response),
201
+ finish_reason=response.stop_reason,
202
+ tool_calls=self._extract_tool_calls(response.content),
203
+ )
204
+
205
+ def stream(self, messages, *, model, tools=None, **kwargs) -> Iterator[StreamChunk]:
206
+ system, ant_messages = self._convert_messages(messages)
207
+ ant_tools = self._resolve_tools(tools, kwargs)
208
+ kwargs.pop("tools", None)
209
+ with self.client.messages.stream(
210
+ model=model,
211
+ messages=ant_messages,
212
+ system=system,
213
+ max_tokens=kwargs.pop("max_tokens", 4096),
214
+ **({"tools": ant_tools} if ant_tools else {}),
215
+ **kwargs,
216
+ ) as stream:
217
+ for text in stream.text_stream:
218
+ yield StreamChunk(content=text, finish_reason=None)
219
+ final = stream.get_final_message()
220
+ tool_calls = self._extract_tool_calls(final.content)
221
+ reasoning = self._extract_reasoning(final.content)
222
+ usage = self._extract_usage(final)
223
+ final_response = Response(
224
+ content=self._extract_content(final.content),
225
+ model=final.model,
226
+ reasoning_content=reasoning,
227
+ usage=usage,
228
+ finish_reason=final.stop_reason,
229
+ tool_calls=tool_calls,
230
+ )
231
+ yield StreamChunk(
232
+ content="",
233
+ tool_calls=tool_calls,
234
+ usage=usage,
235
+ finish_reason=final.stop_reason,
236
+ metadata={"reasoning_content": reasoning} if reasoning else None,
237
+ response=final_response,
238
+ )
239
+
240
+ async def achat(self, messages, *, model, tools=None, **kwargs) -> Response:
241
+ if self.async_client is None:
242
+ raise RuntimeError("async_client not provided; cannot run async methods")
243
+ system, ant_messages = self._convert_messages(messages)
244
+ ant_tools = self._resolve_tools(tools, kwargs)
245
+ kwargs.pop("tools", None)
246
+ response = await self.async_client.messages.create( # pyright: ignore[reportCallIssue, reportArgumentType]
247
+ model=model,
248
+ messages=ant_messages,
249
+ system=system,
250
+ max_tokens=kwargs.pop("max_tokens", 4096),
251
+ **({"tools": ant_tools} if ant_tools else {}),
252
+ **kwargs,
253
+ )
254
+ return Response(
255
+ content=self._extract_content(response.content),
256
+ model=response.model,
257
+ reasoning_content=self._extract_reasoning(response.content),
258
+ usage=self._extract_usage(response),
259
+ finish_reason=response.stop_reason,
260
+ tool_calls=self._extract_tool_calls(response.content),
261
+ )
262
+
263
+ async def achat_stream(self, messages, *, model, tools=None, **kwargs) -> AsyncIterator[StreamChunk]:
264
+ if self.async_client is None:
265
+ raise RuntimeError("async_client not provided; cannot run async methods")
266
+ system, ant_messages = self._convert_messages(messages)
267
+ ant_tools = self._resolve_tools(tools, kwargs)
268
+ kwargs.pop("tools", None)
269
+ async with self.async_client.messages.stream( # pyright: ignore[reportCallIssue, reportArgumentType]
270
+ model=model,
271
+ messages=ant_messages,
272
+ system=system,
273
+ max_tokens=kwargs.pop("max_tokens", 4096),
274
+ **({"tools": ant_tools} if ant_tools else {}),
275
+ **kwargs,
276
+ ) as stream:
277
+ async for text in stream.text_stream:
278
+ yield StreamChunk(content=text, finish_reason=None)
279
+ final = await stream.get_final_message()
280
+ tool_calls = self._extract_tool_calls(final.content)
281
+ reasoning = self._extract_reasoning(final.content)
282
+ usage = self._extract_usage(final)
283
+ final_response = Response(
284
+ content=self._extract_content(final.content),
285
+ model=final.model,
286
+ reasoning_content=reasoning,
287
+ usage=usage,
288
+ finish_reason=final.stop_reason,
289
+ tool_calls=tool_calls,
290
+ )
291
+ yield StreamChunk(
292
+ content="",
293
+ tool_calls=tool_calls,
294
+ usage=usage,
295
+ finish_reason=final.stop_reason,
296
+ metadata={"reasoning_content": reasoning} if reasoning else None,
297
+ response=final_response,
298
+ )