Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,14 @@ backup_count = 5
# en: Log thinking output (default: on).
log_thinking = true

# zh: Tools Schema 兼容性(可选)。
# en: Tools schema compatibility (optional).
# zh: 某些 OpenAI-compatible 网关会对 "tools[].function.description" 做更严格的校验,可能触发 400 错误
# en: Some OpenAI-compatible gateways strictly validate "tools[].function.description" and may return 400.
# zh: Tools 兼容性(可选)。
# en: Tools compatibility (optional).
# zh: 部分 OpenAI-compatible 网关会对 tools schema 做更严格的校验(尤其是 tools[].function.name / description),可能触发 400。
# en: Some OpenAI-compatible gateways strictly validate tools schema (especially tools[].function.name/description) and may return 400.
[tools]
# zh: 是否在请求前清洗 tools schema(默认关闭)
# en: Sanitize tools schema before requests (default: off).
sanitize = false
# zh: 工具名分隔符:当工具原始名称包含 '.'(例如 scheduler.create_schedule_task / mcp.server.tool)时,发送给模型前会把 '.' 映射为该分隔符
# en: Tool-name delimiter: map '.' to this delimiter before sending tools to the model.
dot_delimiter = "-_-"
# zh: description 最大长度(超出会截断)。
# en: Description max length (truncated when exceeded).
description_max_len = 1024
Expand Down
2 changes: 1 addition & 1 deletion res/prompts/undefined.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
你的 content 必须始终始终始终始终为空字符串 ""。
所有消息必须通过 OpenAI tool call 格式调用工具发送。
可用工具:send_message (发送消息), end (结束对话)
**注意:部分高级工具位于工具集 (toolsets) 中,名称带有前缀(如 scheduler.create_schedule_task)。请根据工具定义的名称准确调用。**
**注意:工具集原始命名用 '.' 分隔(如 scheduler.create_schedule_task)。但由于部分模型服务商要求 function.name 只能包含 [a-zA-Z0-9_-],系统会把 '.' 映射为 '-_-'。因此你在 tool call 里应使用 scheduler-_-create_schedule_task(原 scheduler.create_schedule_task)。MCP 工具同理,例如 mcp-_-server-_-tool(原 mcp.server.tool)。请始终以 tools 列表中的 name 为准。**
**可以多次调用 send_message 工具,特别是在需要分段发送内容时。**
**长回复可分多条发送,但条数要克制,避免刷屏。**
</detail>
Expand Down
62 changes: 48 additions & 14 deletions src/Undefined/ai/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,17 @@ async def ask(
tool_choice="auto",
)

tool_name_map = (
result.get("_tool_name_map") if isinstance(result, dict) else None
)
api_to_internal: dict[str, str] = {}
if isinstance(tool_name_map, dict):
raw_api_to_internal = tool_name_map.get("api_to_internal")
if isinstance(raw_api_to_internal, dict):
api_to_internal = {
str(k): str(v) for k, v in raw_api_to_internal.items()
}

choice = result.get("choices", [{}])[0]
message = choice.get("message", {})
content: str = message.get("content") or ""
Expand Down Expand Up @@ -566,77 +577,100 @@ async def ask(

tool_tasks = []
tool_call_ids = []
tool_names = []
tool_api_names: list[str] = []
tool_internal_names: list[str] = []

for tool_call in tool_calls:
call_id = tool_call.get("id", "")
function = tool_call.get("function", {})
function_name = function.get("name", "")
api_function_name = function.get("name", "")
raw_args = function.get("arguments")

logger.info(f"[工具准备] 准备调用: {function_name} (ID={call_id})")
internal_function_name = api_to_internal.get(
str(api_function_name), str(api_function_name)
)

if internal_function_name != api_function_name:
logger.info(
"[工具准备] 准备调用: %s (原名: %s) (ID=%s)",
internal_function_name,
api_function_name,
call_id,
)
else:
logger.info(
"[工具准备] 准备调用: %s (ID=%s)",
api_function_name,
call_id,
)
logger.debug(
f"[工具参数] {function_name} 参数: {redact_string(str(raw_args))}"
f"[工具参数] {api_function_name} 参数: {redact_string(str(raw_args))}"
)

function_args = parse_tool_arguments(
raw_args,
logger=logger,
tool_name=function_name,
tool_name=str(api_function_name),
)

if not isinstance(function_args, dict):
function_args = {}

tool_call_ids.append(call_id)
tool_names.append(function_name)
tool_api_names.append(str(api_function_name))
tool_internal_names.append(str(internal_function_name))
tool_tasks.append(
self.tool_manager.execute_tool(
function_name, function_args, tool_context
str(internal_function_name), function_args, tool_context
)
)

if tool_tasks:
logger.info(
f"[工具执行] 开始并发执行 {len(tool_tasks)} 个工具调用: {', '.join(tool_names)}"
"[工具执行] 开始并发执行 %s 个工具调用: %s",
len(tool_tasks),
", ".join(tool_internal_names),
)
tool_results = await asyncio.gather(
*tool_tasks, return_exceptions=True
)

for i, tool_result in enumerate(tool_results):
call_id = tool_call_ids[i]
fname = tool_names[i]
api_fname = tool_api_names[i]
internal_fname = tool_internal_names[i]

if isinstance(tool_result, Exception):
logger.error(
f"[工具异常] {fname} (ID={call_id}) 执行抛出异常: {tool_result}"
f"[工具异常] {internal_fname} (ID={call_id}) 执行抛出异常: {tool_result}"
)
content_str = f"执行失败: {str(tool_result)}"
else:
content_str = str(tool_result)
logger.debug(
f"[工具响应] {fname} (ID={call_id}) 返回内容长度: {len(content_str)}"
f"[工具响应] {internal_fname} (ID={call_id}) 返回内容长度: {len(content_str)}"
)
if logger.isEnabledFor(logging.DEBUG):
log_debug_json(
logger,
f"[工具响应体] {fname} (ID={call_id})",
f"[工具响应体] {internal_fname} (ID={call_id})",
content_str,
)

messages.append(
{
"role": "tool",
"tool_call_id": call_id,
"name": fname,
"name": api_fname,
"content": content_str,
}
)

if tool_context.get("conversation_ended"):
conversation_ended = True
logger.info(f"[会话状态] 工具 {fname} 触发了会话结束标记")
logger.info(
f"[会话状态] 工具 {internal_fname} 触发了会话结束标记"
)

if conversation_ended:
logger.info("对话已结束(调用 end 工具)")
Expand Down
Loading