feat: 重构MCP功能和AI服务提供者架构

This commit is contained in:
xiamuceer-j
2026-01-09 17:13:19 +08:00
parent f3c224261d
commit 77c5489ff8
49 changed files with 4763 additions and 4307 deletions
@@ -71,7 +71,18 @@ class AnthropicClient:
temperature: float,
max_tokens: int,
system_prompt: Optional[str] = None,
) -> AsyncGenerator[str, None]:
tools: Optional[list] = None,
tool_choice: Optional[str] = None,
) -> AsyncGenerator[Dict[str, Any], None]:
"""
流式生成,支持工具调用
Yields:
Dict with keys:
- content: str - 文本内容块
- tool_calls: list - 工具调用列表(如果有)
- done: bool - 是否结束
"""
kwargs = {
"model": model,
"max_tokens": max_tokens,
@@ -80,12 +91,42 @@ class AnthropicClient:
}
if system_prompt:
kwargs["system"] = system_prompt
if tools:
kwargs["tools"] = tools
if tool_choice == "required":
kwargs["tool_choice"] = {"type": "any"}
elif tool_choice == "auto":
kwargs["tool_choice"] = {"type": "auto"}
try:
async with self.client.messages.stream(**kwargs) as stream:
try:
async for text in stream.text_stream:
yield text
tool_calls = []
async for chunk in stream:
# 处理不同类型的块
if chunk.type == "text_delta":
yield {"content": chunk.text}
elif chunk.type == "tool_use_delta":
# 工具调用增量
if not tool_calls or tool_calls[-1].get("id") != chunk.id:
tool_calls.append({
"id": chunk.id,
"type": "function",
"function": {
"name": chunk.name,
"arguments": ""
}
})
# 追加参数
if tool_calls[-1]["function"]["arguments"] is None:
tool_calls[-1]["function"]["arguments"] = ""
tool_calls[-1]["function"]["arguments"] += chunk.input_gets_new_text or ""
elif chunk.type == "message_delta":
if chunk.stop_reason:
# 流结束
if tool_calls:
yield {"tool_calls": tool_calls}
yield {"done": True, "finish_reason": chunk.stop_reason}
except GeneratorExit:
# 生成器被关闭,这是正常的清理过程
logger.debug("Anthropic 流式响应生成器被关闭(GeneratorExit)")
@@ -111,7 +111,18 @@ class GeminiClient:
temperature: float,
max_tokens: int,
system_prompt: Optional[str] = None,
) -> AsyncGenerator[str, None]:
tools: Optional[list] = None,
tool_choice: Optional[str] = None,
) -> AsyncGenerator[Dict[str, Any], None]:
"""
流式生成,支持工具调用
Yields:
Dict with keys:
- content: str - 文本内容块
- tool_calls: list - 工具调用列表(如果有)
- done: bool - 是否结束
"""
url = f"{self.base_url}/models/{model}:streamGenerateContent?key={self.api_key}&alt=sse"
contents = []
@@ -125,6 +136,8 @@ class GeminiClient:
}
if system_prompt:
payload["systemInstruction"] = {"parts": [{"text": system_prompt}]}
if tools:
payload["tools"] = self._convert_tools_to_gemini(tools)
try:
async with self.client.stream("POST", url, json=payload) as response:
@@ -139,9 +152,26 @@ class GeminiClient:
if candidates and len(candidates) > 0:
parts = candidates[0].get("content", {}).get("parts", [])
if parts and len(parts) > 0:
text = parts[0].get("text", "")
text = ""
function_calls = []
for part in parts:
if "text" in part:
text += part["text"]
elif "functionCall" in part:
fc = part["functionCall"]
function_calls.append({
"id": f"call_{fc['name']}",
"type": "function",
"function": {
"name": fc["name"],
"arguments": fc.get("args", {})
}
})
if text:
yield text
yield {"content": text}
if function_calls:
yield {"tool_calls": function_calls}
except json.JSONDecodeError:
continue
except GeneratorExit:
@@ -86,8 +86,21 @@ class OpenAIClient(BaseAIClient):
model: str,
temperature: float,
max_tokens: int,
) -> AsyncGenerator[str, None]:
payload = self._build_payload(messages, model, temperature, max_tokens, stream=True)
tools: Optional[list] = None,
tool_choice: Optional[str] = None,
) -> AsyncGenerator[Dict[str, Any], None]:
"""
流式生成,支持工具调用
Yields:
Dict with keys:
- content: str - 文本内容块
- tool_calls: list - 工具调用列表(如果有)
- done: bool - 是否结束
"""
payload = self._build_payload(messages, model, temperature, max_tokens, tools, tool_choice, stream=True)
tool_calls_buffer = {} # 收集工具调用块
try:
async with await self._request_with_retry("POST", "/chat/completions", payload, stream=True) as response:
@@ -97,14 +110,38 @@ class OpenAIClient(BaseAIClient):
if line.startswith("data: "):
data_str = line[6:]
if data_str.strip() == "[DONE]":
# 流结束,检查是否有工具调用需要处理
if tool_calls_buffer:
yield {"tool_calls": list(tool_calls_buffer.values()), "done": True}
yield {"done": True}
break
try:
data = json.loads(data_str)
choices = data.get("choices", [])
if choices and len(choices) > 0:
content = choices[0].get("delta", {}).get("content", "")
delta = choices[0].get("delta", {})
content = delta.get("content", "")
# 检查工具调用
tc_list = delta.get("tool_calls")
if tc_list:
for tc in tc_list:
index = tc.get("index", 0)
if index not in tool_calls_buffer:
tool_calls_buffer[index] = tc
else:
existing = tool_calls_buffer[index]
# 合并 function.arguments
if "function" in tc and "function" in existing:
if tc["function"].get("arguments"):
existing["function"]["arguments"] = (
existing["function"].get("arguments", "") +
tc["function"]["arguments"]
)
if content:
yield content
yield {"content": content}
except json.JSONDecodeError:
continue
except GeneratorExit: