Исправлен tool loop: tool_choice auto после выполнения tools

This commit is contained in:
2026-08-11 16:37:42 +03:00
parent 2e99367dff
commit 03f115f751
+37 -24
View File
@@ -105,9 +105,9 @@ class Pipe:
"complex bug", "complex problem", "multi-agent", "multiple services", "complex bug", "complex problem", "multi-agent", "multiple services",
"distributed system", "design a system", "build a system", "agentic", "distributed system", "design a system", "build a system", "agentic",
} }
if any(self._contains_phrase(lowered, item) for item in complex_phrases) or any( if any(self._contains_phrase(lowered, item) for item in complex_phrases):
word.startswith("архитектур") for word in words return "complex"
): if any(word.startswith("архитектур") for word in words):
return "complex" return "complex"
coding_words = { coding_words = {
@@ -128,11 +128,9 @@ class Pipe:
"перепиши код", "рефакторинг", "stack trace", "error log", "виртуальная машина", "перепиши код", "рефакторинг", "stack trace", "error log", "виртуальная машина",
"reverse proxy", "nginx proxy manager", "pull request", "reverse proxy", "nginx proxy manager", "pull request",
} }
coding_stems = {"программ", "функци", "библиотек", "ошиб", "сертификат"}
if words.intersection(coding_words): if words.intersection(coding_words):
return "coding" return "coding"
if any(word.startswith(stem) for word in words for stem in coding_stems): if any(word.startswith(stem) for word in words for stem in {"программ", "функци", "библиотек", "ошиб", "сертификат"}):
return "coding" return "coding"
if any(self._contains_phrase(lowered, item) for item in coding_phrases): if any(self._contains_phrase(lowered, item) for item in coding_phrases):
return "coding" return "coding"
@@ -189,13 +187,15 @@ class Pipe:
raise RuntimeError("Request failed without a captured error") raise RuntimeError("Request failed without a captured error")
def _parse_tool_arguments(self, raw_arguments): def _parse_tool_arguments(self, raw_arguments):
"""Разбирает JSON-аргументы вызова инструмента.""" """Разбирает JSON-аргументы инструмента."""
if raw_arguments is None: if raw_arguments is None:
return {} return {}
if isinstance(raw_arguments, dict): if isinstance(raw_arguments, dict):
return raw_arguments return raw_arguments
if not isinstance(raw_arguments, str) or not raw_arguments.strip(): if not isinstance(raw_arguments, str):
return {} if isinstance(raw_arguments, str) else None return None
if not raw_arguments.strip():
return {}
try: try:
arguments = json.loads(raw_arguments) arguments = json.loads(raw_arguments)
except json.JSONDecodeError: except json.JSONDecodeError:
@@ -225,7 +225,7 @@ class Pipe:
result.append({"type": "function", "function": spec}) result.append({"type": "function", "function": spec})
return result return result
def _build_payload(self, body, model, messages, tools_map): def _build_payload(self, body, model, messages, tools_map, tool_iteration=0):
"""Собирает payload для OpenRouter.""" """Собирает payload для OpenRouter."""
payload = dict(body) payload = dict(body)
payload.update({"model": model, "messages": messages, "stream": False}) payload.update({"model": model, "messages": messages, "stream": False})
@@ -235,24 +235,33 @@ class Pipe:
payload["max_tokens"] = self.valves.MAX_TOKENS payload["max_tokens"] = self.valves.MAX_TOKENS
for field in {"user", "reasoning_effort", "metadata", "store"}: for field in {"user", "reasoning_effort", "metadata", "store"}:
payload.pop(field, None) payload.pop(field, None)
if not self.valves.ENABLE_TOOLS: if not self.valves.ENABLE_TOOLS:
for field in {"tools", "tool_choice", "parallel_tool_calls"}: for field in {"tools", "tool_choice", "parallel_tool_calls"}:
payload.pop(field, None) payload.pop(field, None)
elif not payload.get("tools"): else:
if not payload.get("tools"):
specs = self._extract_tool_specs(tools_map) specs = self._extract_tool_specs(tools_map)
if specs: if specs:
payload["tools"] = specs payload["tools"] = specs
# После первого tool call модель должна иметь возможность
# завершить ответ обычным текстом, даже если Open WebUI
# передал tool_choice="required" или фиксированный tool.
if tool_iteration > 0:
payload["tool_choice"] = "auto"
return payload return payload
async def _execute_tool_call(self, tool_call, tools_map): async def _execute_tool_call(self, call, tools_map):
"""Выполняет tool call; фатальна только ошибка tool_call_id.""" """Выполняет tool call; фатальна только ошибка tool_call_id."""
if not isinstance(tool_call, dict): if not isinstance(call, dict):
return {"fatal": True, "result": "Ошибка: некорректный формат tool call."} return {"fatal": True, "result": "Ошибка: некорректный формат tool call."}
call_id = tool_call.get("id") call_id = call.get("id")
if not isinstance(call_id, str) or not call_id.strip(): if not isinstance(call_id, str) or not call_id.strip():
return {"fatal": True, "result": "Ошибка: отсутствует корректный tool_call_id."} return {"fatal": True, "result": "Ошибка: отсутствует корректный tool_call_id."}
function = tool_call.get("function") function = call.get("function")
if not isinstance(function, dict): if not isinstance(function, dict):
return {"fatal": False, "tool_call_id": call_id, "name": "unknown", "arguments": {}, "result": "Ошибка: отсутствует описание функции."} return {"fatal": False, "tool_call_id": call_id, "name": "unknown", "arguments": {}, "result": "Ошибка: отсутствует описание функции."}
name = function.get("name") name = function.get("name")
@@ -327,11 +336,11 @@ class Pipe:
try: try:
async with httpx.AsyncClient(timeout=timeout) as client: async with httpx.AsyncClient(timeout=timeout) as client:
for _ in range(self.valves.MAX_TOOL_ITERATIONS): for iteration in range(self.valves.MAX_TOOL_ITERATIONS):
data = await self._request( payload = self._build_payload(
self._build_payload(body, model, messages, tools_map), body, model, messages, tools_map, iteration
client,
) )
data = await self._request(payload, client)
try: try:
message = data["choices"][0]["message"] message = data["choices"][0]["message"]
except (KeyError, IndexError, TypeError): except (KeyError, IndexError, TypeError):
@@ -350,21 +359,25 @@ class Pipe:
break break
messages.append({"role": "assistant", "content": message.get("content"), "tool_calls": tool_calls}) messages.append({"role": "assistant", "content": message.get("content"), "tool_calls": tool_calls})
fatal_stop = False stop = False
for call in tool_calls: for call in tool_calls:
function = call.get("function", {}) if isinstance(call, dict) else {} function = call.get("function", {}) if isinstance(call, dict) else {}
key = json.dumps({"name": function.get("name"), "arguments": function.get("arguments")}, ensure_ascii=False, sort_keys=True) key = json.dumps(
{"name": function.get("name"), "arguments": function.get("arguments")},
ensure_ascii=False,
sort_keys=True,
)
repeated[key] = repeated.get(key, 0) + 1 repeated[key] = repeated.get(key, 0) + 1
if repeated[key] > self.valves.MAX_SAME_TOOL_CALLS: if repeated[key] > self.valves.MAX_SAME_TOOL_CALLS:
stopped_reason = "⚠️ Один и тот же tool call повторился слишком много раз." stopped_reason = "⚠️ Один и тот же tool call повторился слишком много раз."
fatal_stop = True stop = True
break break
execution = await self._execute_tool_call(call, tools_map) execution = await self._execute_tool_call(call, tools_map)
if execution["fatal"]: if execution["fatal"]:
stopped_reason = execution["result"] stopped_reason = execution["result"]
final_content = content final_content = content
fatal_stop = True stop = True
break break
trace.append(execution) trace.append(execution)
messages.append({ messages.append({
@@ -373,7 +386,7 @@ class Pipe:
"name": execution["name"], "name": execution["name"],
"content": execution["result"], "content": execution["result"],
}) })
if fatal_stop: if stop:
break break
else: else:
stopped_reason = f"⚠️ Достигнут лимит циклов tool calling: {self.valves.MAX_TOOL_ITERATIONS}." stopped_reason = f"⚠️ Достигнут лимит циклов tool calling: {self.valves.MAX_TOOL_ITERATIONS}."