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.
- package/README.md +318 -0
- package/package.json +45 -0
- package/pyproject.toml +50 -0
- package/src/my_agent_core/__init__.py +123 -0
- package/src/my_agent_core/agent.py +441 -0
- package/src/my_agent_core/background.py +121 -0
- package/src/my_agent_core/context.py +505 -0
- package/src/my_agent_core/events.py +153 -0
- package/src/my_agent_core/extensions/__init__.py +9 -0
- package/src/my_agent_core/extensions/core.py +197 -0
- package/src/my_agent_core/hooks.py +130 -0
- package/src/my_agent_core/loop.py +709 -0
- package/src/my_agent_core/main.py +134 -0
- package/src/my_agent_core/memory.py +241 -0
- package/src/my_agent_core/message_queue.py +110 -0
- package/src/my_agent_core/plugins.py +212 -0
- package/src/my_agent_core/registry.py +186 -0
- package/src/my_agent_core/session/__init__.py +79 -0
- package/src/my_agent_core/session/entries.py +197 -0
- package/src/my_agent_core/session/jsonl.py +60 -0
- package/src/my_agent_core/session/memory.py +137 -0
- package/src/my_agent_core/session/session.py +400 -0
- package/src/my_agent_core/session/storage.py +245 -0
- package/src/my_agent_core/session/store.py +131 -0
- package/src/my_agent_core/session/tree.py +86 -0
- package/src/my_agent_core/skills.py +149 -0
- package/src/my_agent_core/subagent_tasks.py +170 -0
- package/src/my_agent_core/subagents.py +148 -0
- package/src/my_agent_core/task_store.py +248 -0
- package/src/my_agent_core/tool_history.py +189 -0
- package/src/my_agent_core/tools/__init__.py +5 -0
- package/src/my_agent_core/tools/builtin/__init__.py +5 -0
- package/src/my_agent_core/tools/builtin/task.py +30 -0
- package/src/my_agent_core/tools/builtin/task_tools.py +215 -0
- package/src/my_agent_core/tools/core.py +239 -0
- package/src/my_agent_llm/__init__.py +45 -0
- package/src/my_agent_llm/auth/__init__.py +46 -0
- package/src/my_agent_llm/auth/antigravity.py +209 -0
- package/src/my_agent_llm/auth/manager.py +259 -0
- package/src/my_agent_llm/auth/quota.py +56 -0
- package/src/my_agent_llm/auth/schema.py +94 -0
- package/src/my_agent_llm/client.py +116 -0
- package/src/my_agent_llm/config.py +17 -0
- package/src/my_agent_llm/events.py +84 -0
- package/src/my_agent_llm/models.py +195 -0
- package/src/my_agent_llm/providers/__init__.py +4 -0
- package/src/my_agent_llm/providers/_base.py +94 -0
- package/src/my_agent_llm/providers/anthropic.py +298 -0
- package/src/my_agent_llm/providers/antigravity.py +480 -0
- package/src/my_agent_llm/providers/deepseek.py +196 -0
- package/src/my_agent_llm/providers/openai.py +364 -0
- package/src/my_agent_llm/providers/registry.py +16 -0
- package/src/my_agent_llm/stream.py +218 -0
- package/src/my_coding_agent/__init__.py +66 -0
- package/src/my_coding_agent/agent.py +208 -0
- package/src/my_coding_agent/cli.py +78 -0
- package/src/my_coding_agent/file_reference.py +80 -0
- package/src/my_coding_agent/macro.py +408 -0
- package/src/my_coding_agent/mcp.py +243 -0
- package/src/my_coding_agent/mutation_queue.py +37 -0
- package/src/my_coding_agent/paths.py +119 -0
- package/src/my_coding_agent/permissions.py +84 -0
- package/src/my_coding_agent/prompt.py +54 -0
- package/src/my_coding_agent/rpc_server.py +2817 -0
- package/src/my_coding_agent/settings.py +126 -0
- package/src/my_coding_agent/tools/__init__.py +55 -0
- package/src/my_coding_agent/tools/base.py +58 -0
- package/src/my_coding_agent/tools/bash.py +206 -0
- package/src/my_coding_agent/tools/edit.py +226 -0
- package/src/my_coding_agent/tools/find.py +118 -0
- package/src/my_coding_agent/tools/grep.py +177 -0
- package/src/my_coding_agent/tools/ls.py +112 -0
- package/src/my_coding_agent/tools/read.py +113 -0
- package/src/my_coding_agent/tools/write.py +72 -0
- package/tui/README.md +27 -0
- package/tui/bin/my-agent.js +98 -0
- package/tui/dist/app.d.ts +41 -0
- package/tui/dist/app.js +110 -0
- package/tui/dist/bridge/event-translator.d.ts +92 -0
- package/tui/dist/bridge/event-translator.js +216 -0
- package/tui/dist/bridge/kernel-bridge.d.ts +48 -0
- package/tui/dist/bridge/kernel-bridge.js +132 -0
- package/tui/dist/client.d.ts +63 -0
- package/tui/dist/client.js +239 -0
- package/tui/dist/components/assistant-message.d.ts +19 -0
- package/tui/dist/components/assistant-message.js +90 -0
- package/tui/dist/components/compaction-summary-message.d.ts +19 -0
- package/tui/dist/components/compaction-summary-message.js +46 -0
- package/tui/dist/components/custom-editor.d.ts +18 -0
- package/tui/dist/components/custom-editor.js +56 -0
- package/tui/dist/components/dynamic-border.d.ts +9 -0
- package/tui/dist/components/dynamic-border.js +14 -0
- package/tui/dist/components/footer.d.ts +39 -0
- package/tui/dist/components/footer.js +199 -0
- package/tui/dist/components/header.d.ts +4 -0
- package/tui/dist/components/header.js +21 -0
- package/tui/dist/components/keys.d.ts +5 -0
- package/tui/dist/components/keys.js +12 -0
- package/tui/dist/components/login-selector.d.ts +26 -0
- package/tui/dist/components/login-selector.js +181 -0
- package/tui/dist/components/logout-selector.d.ts +19 -0
- package/tui/dist/components/logout-selector.js +88 -0
- package/tui/dist/components/model-selector.d.ts +40 -0
- package/tui/dist/components/model-selector.js +268 -0
- package/tui/dist/components/session-selector.d.ts +54 -0
- package/tui/dist/components/session-selector.js +393 -0
- package/tui/dist/components/settings-selector.d.ts +24 -0
- package/tui/dist/components/settings-selector.js +146 -0
- package/tui/dist/components/status-indicator.d.ts +25 -0
- package/tui/dist/components/status-indicator.js +60 -0
- package/tui/dist/components/theme-selector.d.ts +14 -0
- package/tui/dist/components/theme-selector.js +77 -0
- package/tui/dist/components/thinking-selector.d.ts +21 -0
- package/tui/dist/components/thinking-selector.js +128 -0
- package/tui/dist/components/tool-execution.d.ts +31 -0
- package/tui/dist/components/tool-execution.js +206 -0
- package/tui/dist/components/tree-selector.d.ts +40 -0
- package/tui/dist/components/tree-selector.js +173 -0
- package/tui/dist/components/user-message-selector.d.ts +21 -0
- package/tui/dist/components/user-message-selector.js +103 -0
- package/tui/dist/components/user-message.d.ts +5 -0
- package/tui/dist/components/user-message.js +15 -0
- package/tui/dist/index.d.ts +11 -0
- package/tui/dist/index.js +11 -0
- package/tui/dist/interactive/chat-viewport.d.ts +19 -0
- package/tui/dist/interactive/chat-viewport.js +41 -0
- package/tui/dist/interactive/components.d.ts +1 -0
- package/tui/dist/interactive/components.js +1 -0
- package/tui/dist/interactive/interactive-mode.d.ts +89 -0
- package/tui/dist/interactive/interactive-mode.js +1625 -0
- package/tui/dist/interactive/theme.d.ts +1 -0
- package/tui/dist/interactive/theme.js +1 -0
- package/tui/dist/interactive/tui-renderer.d.ts +8 -0
- package/tui/dist/interactive/tui-renderer.js +10 -0
- package/tui/dist/protocol.d.ts +78 -0
- package/tui/dist/protocol.js +1 -0
- package/tui/dist/theme/dark.json +54 -0
- package/tui/dist/theme/light.json +71 -0
- package/tui/dist/theme/theme.d.ts +20 -0
- package/tui/dist/theme/theme.js +86 -0
- 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,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
|
+
)
|