feat(engine): 添加技能查看工具并优化异步任务管理 - 添加SkillViewTool到引擎加载器中,增强技能管理功能 - 在AgentLoop中引入_active_direct_task来跟踪活跃任务 - 实现直接任务执行时的同步处理逻辑 - 更新工具实例化方式以支持依赖注入 feat(config): 增加智能体运行时参数配置支持 - 扩展AgentDefaultsConfig添加max_tokens和temperature字段 - 实现配置解析函数_first_config_value处理多个配置源 - 支持通过Web API动态更新智能体运行时参数 - 添加前端页面配置表单和验证逻辑 refactor(provider): 统一最大令牌数参数类型为可选整型 - 将所有LLM提供者的max_tokens参数改为int | None类型 - 为AnthropicProvider实现模型特定的最大令牌数默认值 - 调整参数传递逻辑,优先级:调用参数 > 配置文件 > 模型默认值 - 移除硬编码的默认值,改用条件判断 feat(process): 增强事件投影功能 - 添加工具调用开始/结束事件的映射逻辑 - 实现技能激活事件的识别和展示 - 添加辅助函数处理工具调用名称和参数提取 - 优化运行记录关联逻辑,提升事件匹配准确性 fix(web): 更新网络请求客户端信任环境设置 - 将WebFetchTool和WebSearchTool的trust_env参数设为True - 确保HTTP客户端能够正确使用系统代理配置 - 修复可能的网络连接问题 test: 添加配置加载和事件投影相关测试 - 新增智能体默认参数配置测试用例 - 实现API配置持久化和重载测试 - 添加技能卡片和工具事件的投影测试 ```
191 lines
6.9 KiB
Python
191 lines
6.9 KiB
Python
"""Native Anthropic Messages API provider."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from typing import Any
|
||
|
||
from .base import LLMProvider, LLMResponse, ToolCallRequest
|
||
|
||
try: # pragma: no cover - optional dependency
|
||
import anthropic
|
||
except ModuleNotFoundError: # pragma: no cover
|
||
anthropic = None # type: ignore[assignment]
|
||
|
||
|
||
class AnthropicProvider(LLMProvider):
|
||
"""使用 Anthropic 原生 Messages API,而不是强行走 OpenAI-compatible path。"""
|
||
|
||
def __init__(
|
||
self,
|
||
api_key: str | None = None,
|
||
default_model: str = "claude-sonnet-4-5",
|
||
api_base: str | None = None,
|
||
request_timeout_seconds: float | None = None,
|
||
) -> None:
|
||
super().__init__(api_key, api_base, request_timeout_seconds=request_timeout_seconds)
|
||
self.default_model = default_model
|
||
self._client = None
|
||
|
||
def _client_or_raise(self):
|
||
if anthropic is None:
|
||
raise RuntimeError("anthropic package is not installed")
|
||
if self._client is None:
|
||
self._client = anthropic.AsyncAnthropic(
|
||
api_key=self.api_key,
|
||
base_url=self.api_base,
|
||
timeout=self.request_timeout_seconds,
|
||
)
|
||
return self._client
|
||
|
||
async def chat(
|
||
self,
|
||
messages: list[dict[str, Any]],
|
||
tools: list[dict[str, Any]] | None = None,
|
||
model: str | None = None,
|
||
max_tokens: int | None = None,
|
||
temperature: float = 0.7,
|
||
thinking_enabled: bool | None = None,
|
||
) -> LLMResponse:
|
||
try:
|
||
client = self._client_or_raise()
|
||
except Exception as exc:
|
||
return LLMResponse(content=f"Error: {exc}", finish_reason="error", provider_name="anthropic")
|
||
|
||
system_prompt, anthropic_messages = _convert_messages(messages)
|
||
kwargs: dict[str, Any] = {
|
||
"model": model or self.default_model,
|
||
"system": system_prompt or "",
|
||
"messages": anthropic_messages,
|
||
"temperature": temperature,
|
||
}
|
||
resolved_max_tokens = (
|
||
_default_max_tokens_for_model(model or self.default_model)
|
||
if max_tokens is None
|
||
else max(1, max_tokens)
|
||
)
|
||
kwargs["max_tokens"] = resolved_max_tokens
|
||
if tools:
|
||
kwargs["tools"] = _convert_tools(tools)
|
||
|
||
try:
|
||
response = await client.messages.create(**kwargs)
|
||
except Exception as exc:
|
||
return LLMResponse(content=f"Error: {exc}", finish_reason="error", provider_name="anthropic")
|
||
|
||
content_parts: list[str] = []
|
||
tool_calls: list[ToolCallRequest] = []
|
||
for block in response.content:
|
||
if block.type == "text":
|
||
content_parts.append(block.text)
|
||
elif block.type == "tool_use":
|
||
tool_calls.append(
|
||
ToolCallRequest(
|
||
id=block.id,
|
||
name=block.name,
|
||
arguments=block.input,
|
||
)
|
||
)
|
||
usage_payload = {}
|
||
if getattr(response, "usage", None):
|
||
usage_payload = {
|
||
"input_tokens": getattr(response.usage, "input_tokens", 0),
|
||
"output_tokens": getattr(response.usage, "output_tokens", 0),
|
||
}
|
||
return LLMResponse(
|
||
content="".join(content_parts) or None,
|
||
tool_calls=tool_calls,
|
||
finish_reason=getattr(response, "stop_reason", "stop") or "stop",
|
||
usage=usage_payload,
|
||
provider_name="anthropic",
|
||
model=model or self.default_model,
|
||
)
|
||
|
||
def get_default_model(self) -> str:
|
||
return self.default_model
|
||
|
||
|
||
def _default_max_tokens_for_model(model: str) -> int:
|
||
"""Return a conservative native output ceiling for Anthropic Messages."""
|
||
|
||
normalized = model.lower().replace("_", "-")
|
||
if "sonnet-4" in normalized or "opus-4" in normalized or "3-7" in normalized or "3.7" in normalized:
|
||
return 64_000
|
||
if "haiku" in normalized:
|
||
return 4_096
|
||
return 8_192
|
||
|
||
|
||
def _convert_messages(messages: list[dict[str, Any]]) -> tuple[str, list[dict[str, Any]]]:
|
||
system_prompt = ""
|
||
converted: list[dict[str, Any]] = []
|
||
for message in messages:
|
||
role = message.get("role")
|
||
if role == "system":
|
||
content = message.get("content")
|
||
system_prompt = content if isinstance(content, str) else ""
|
||
continue
|
||
if role == "tool":
|
||
converted.append(
|
||
{
|
||
"role": "user",
|
||
"content": [
|
||
{
|
||
"type": "tool_result",
|
||
"tool_use_id": message.get("tool_call_id"),
|
||
"content": message.get("content") or "",
|
||
}
|
||
],
|
||
}
|
||
)
|
||
continue
|
||
if role == "assistant" and message.get("tool_calls"):
|
||
content_blocks: list[dict[str, Any]] = []
|
||
if message.get("content"):
|
||
content_blocks.append({"type": "text", "text": message["content"]})
|
||
for tool_call in message.get("tool_calls", []):
|
||
function = tool_call.get("function", tool_call)
|
||
arguments = function.get("arguments")
|
||
if isinstance(arguments, str):
|
||
try:
|
||
arguments = json.loads(arguments)
|
||
except json.JSONDecodeError:
|
||
arguments = {}
|
||
content_blocks.append(
|
||
{
|
||
"type": "tool_use",
|
||
"id": tool_call.get("id"),
|
||
"name": function.get("name"),
|
||
"input": arguments or {},
|
||
}
|
||
)
|
||
converted.append({"role": "assistant", "content": content_blocks})
|
||
continue
|
||
|
||
content = message.get("content")
|
||
if isinstance(content, list):
|
||
blocks = []
|
||
for item in content:
|
||
if isinstance(item, dict) and item.get("type") == "text":
|
||
blocks.append({"type": "text", "text": item.get("text", "")})
|
||
converted.append({"role": role, "content": blocks or [{"type": "text", "text": ""}]})
|
||
else:
|
||
converted.append({"role": role, "content": content or ""})
|
||
return system_prompt, converted
|
||
|
||
|
||
def _convert_tools(tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||
converted: list[dict[str, Any]] = []
|
||
for tool in tools:
|
||
fn = (tool.get("function") or {}) if tool.get("type") == "function" else tool
|
||
if not fn.get("name"):
|
||
continue
|
||
converted.append(
|
||
{
|
||
"name": fn["name"],
|
||
"description": fn.get("description") or "",
|
||
"input_schema": fn.get("parameters") or {"type": "object", "properties": {}},
|
||
}
|
||
)
|
||
return converted
|