Исправлен tool loop: tool_choice auto после выполнения tools
This commit is contained in:
+40
-27
@@ -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:
|
||||||
specs = self._extract_tool_specs(tools_map)
|
if not payload.get("tools"):
|
||||||
if specs:
|
specs = self._extract_tool_specs(tools_map)
|
||||||
payload["tools"] = specs
|
if 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}."
|
||||||
|
|||||||
Reference in New Issue
Block a user