w1x-cli 3.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- w1x/__init__.py +5 -0
- w1x/app.py +754 -0
- w1x/config.py +189 -0
- w1x/engine.py +565 -0
- w1x/memory.py +272 -0
- w1x/models.py +458 -0
- w1x/puller.py +955 -0
- w1x/terminal.py +661 -0
- w1x/testpy.py +325 -0
- w1x/tools.py +897 -0
- w1x_cli-3.5.0.dist-info/METADATA +178 -0
- w1x_cli-3.5.0.dist-info/RECORD +16 -0
- w1x_cli-3.5.0.dist-info/WHEEL +5 -0
- w1x_cli-3.5.0.dist-info/entry_points.txt +2 -0
- w1x_cli-3.5.0.dist-info/licenses/LICENSE +21 -0
- w1x_cli-3.5.0.dist-info/top_level.txt +1 -0
w1x/engine.py
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Движок генерации ответов модели, вызов инструментов и статистика."""
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from rich.console import Group
|
|
8
|
+
from rich.live import Live
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from ollama import chat
|
|
11
|
+
|
|
12
|
+
from .config import (
|
|
13
|
+
MAX_TOOL_ROUNDS,
|
|
14
|
+
MAX_RESPONSE_TOKENS,
|
|
15
|
+
TOOL_INSTRUCTIONS,
|
|
16
|
+
FORMAT_INSTRUCTIONS,
|
|
17
|
+
SPINNER,
|
|
18
|
+
get_keep_alive,
|
|
19
|
+
get_context_tokens,
|
|
20
|
+
get_max_history_chars,
|
|
21
|
+
)
|
|
22
|
+
from .terminal import (
|
|
23
|
+
console,
|
|
24
|
+
render_message,
|
|
25
|
+
run_with_spinner,
|
|
26
|
+
uses_modern_windows_terminal,
|
|
27
|
+
ChatMarkdown,
|
|
28
|
+
)
|
|
29
|
+
from .tools import (
|
|
30
|
+
calculate_datetime,
|
|
31
|
+
calculate_expression,
|
|
32
|
+
parse_math_request,
|
|
33
|
+
previous_math_expression,
|
|
34
|
+
split_datetime_request,
|
|
35
|
+
datetime_answer,
|
|
36
|
+
needs_clarification,
|
|
37
|
+
ask_antigravity,
|
|
38
|
+
TOOL_FUNCTIONS,
|
|
39
|
+
DIRECT_PASSTHROUGH_TOOLS,
|
|
40
|
+
format_direct_tool_result,
|
|
41
|
+
execute_tool_call,
|
|
42
|
+
)
|
|
43
|
+
from .memory import relevant_memory_block
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def trim_messages_for_model(messages):
|
|
47
|
+
"""Собрать историю для контекста модели с сохранением последних сообщений."""
|
|
48
|
+
system_messages = [item for item in messages if item.get("role") == "system"]
|
|
49
|
+
conversation = [
|
|
50
|
+
item for item in messages
|
|
51
|
+
if item.get("role") in {"user", "assistant"}
|
|
52
|
+
and isinstance(item.get("content"), str)
|
|
53
|
+
and item["content"].strip()
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
total_chars = 0
|
|
57
|
+
selected = []
|
|
58
|
+
max_history = get_max_history_chars()
|
|
59
|
+
# Всегда гарантированно сохраняем хотя бы последние 6 сообщений диалога
|
|
60
|
+
for idx, item in enumerate(reversed(conversation)):
|
|
61
|
+
content_len = len(item["content"])
|
|
62
|
+
if idx >= 6 and (total_chars + content_len > max_history):
|
|
63
|
+
break
|
|
64
|
+
selected.append({"role": item["role"], "content": item["content"]})
|
|
65
|
+
total_chars += content_len
|
|
66
|
+
|
|
67
|
+
selected.reverse()
|
|
68
|
+
system_payload = (
|
|
69
|
+
system_messages[0]["content"] if system_messages else "Отвечай по-русски."
|
|
70
|
+
)
|
|
71
|
+
return [{"role": "system", "content": system_payload}, *selected]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def weak_tool_answer(text):
|
|
75
|
+
normalized = " ".join(text.lower().split())
|
|
76
|
+
if not normalized:
|
|
77
|
+
return True
|
|
78
|
+
return bool(
|
|
79
|
+
re.search(
|
|
80
|
+
r"^(?:(?:к\s+сожалению|извините)[,\s]+)?(?:я\s+)?"
|
|
81
|
+
r"(?:не\s+могу|не\s+удалось|не\s+получилось|"
|
|
82
|
+
r"не\s+нашел|не\s+наш[её]л|не\s+вижу|нет\s+информации)",
|
|
83
|
+
normalized,
|
|
84
|
+
)
|
|
85
|
+
or re.search(
|
|
86
|
+
r"(?:поиск|инструмент).*(?:не\s+дал|не\s+вернул|не\s+помог|ошибк)",
|
|
87
|
+
normalized,
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def format_duration(nanoseconds):
|
|
93
|
+
if not nanoseconds:
|
|
94
|
+
return "—"
|
|
95
|
+
if nanoseconds < 1_000_000:
|
|
96
|
+
return "<1 мс"
|
|
97
|
+
seconds = nanoseconds / 1_000_000_000
|
|
98
|
+
return f"{seconds * 1000:.0f} мс" if seconds < 1 else f"{seconds:.2f} с"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def collect_stats(response):
|
|
102
|
+
if response is None:
|
|
103
|
+
return None
|
|
104
|
+
prompt_tokens = getattr(response, "prompt_eval_count", None) or 0
|
|
105
|
+
output_tokens = getattr(response, "eval_count", None) or 0
|
|
106
|
+
eval_duration = getattr(response, "eval_duration", None) or 0
|
|
107
|
+
speed = (
|
|
108
|
+
output_tokens / (eval_duration / 1_000_000_000)
|
|
109
|
+
if output_tokens and eval_duration
|
|
110
|
+
else 0
|
|
111
|
+
)
|
|
112
|
+
return {
|
|
113
|
+
"source": "model",
|
|
114
|
+
"prompt_tokens": prompt_tokens,
|
|
115
|
+
"output_tokens": output_tokens,
|
|
116
|
+
"total_tokens": prompt_tokens + output_tokens,
|
|
117
|
+
"total_duration": getattr(response, "total_duration", None),
|
|
118
|
+
"load_duration": getattr(response, "load_duration", None),
|
|
119
|
+
"prompt_duration": getattr(response, "prompt_eval_duration", None),
|
|
120
|
+
"eval_duration": eval_duration,
|
|
121
|
+
"speed": speed,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def direct_tool_stats(tool_name, started_ns):
|
|
126
|
+
duration_ns = time.perf_counter_ns() - started_ns
|
|
127
|
+
return {
|
|
128
|
+
"source": "direct_tool",
|
|
129
|
+
"tool_name": tool_name,
|
|
130
|
+
"prompt_tokens": 0,
|
|
131
|
+
"output_tokens": 0,
|
|
132
|
+
"total_tokens": 0,
|
|
133
|
+
"total_duration": duration_ns,
|
|
134
|
+
"load_duration": 0,
|
|
135
|
+
"prompt_duration": 0,
|
|
136
|
+
"eval_duration": duration_ns,
|
|
137
|
+
"speed": 0,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def merge_stats(aggregate, step_stats):
|
|
142
|
+
if step_stats is None:
|
|
143
|
+
return aggregate
|
|
144
|
+
if aggregate is None:
|
|
145
|
+
return dict(step_stats)
|
|
146
|
+
|
|
147
|
+
aggregate["prompt_tokens"] = max(
|
|
148
|
+
aggregate.get("prompt_tokens", 0),
|
|
149
|
+
step_stats.get("prompt_tokens", 0),
|
|
150
|
+
)
|
|
151
|
+
aggregate["output_tokens"] = (
|
|
152
|
+
aggregate.get("output_tokens", 0)
|
|
153
|
+
+ step_stats.get("output_tokens", 0)
|
|
154
|
+
)
|
|
155
|
+
aggregate["total_tokens"] = (
|
|
156
|
+
aggregate.get("prompt_tokens", 0)
|
|
157
|
+
+ aggregate.get("output_tokens", 0)
|
|
158
|
+
)
|
|
159
|
+
aggregate["eval_duration"] = (
|
|
160
|
+
aggregate.get("eval_duration", 0)
|
|
161
|
+
+ (step_stats.get("eval_duration") or 0)
|
|
162
|
+
)
|
|
163
|
+
return aggregate
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def finalize_stats(aggregate, started_ns):
|
|
167
|
+
if aggregate is None:
|
|
168
|
+
return None
|
|
169
|
+
total_duration = time.perf_counter_ns() - started_ns
|
|
170
|
+
eval_duration = aggregate.get("eval_duration") or 0
|
|
171
|
+
output_tokens = aggregate.get("output_tokens") or 0
|
|
172
|
+
speed = (
|
|
173
|
+
output_tokens / (eval_duration / 1_000_000_000)
|
|
174
|
+
if output_tokens and eval_duration
|
|
175
|
+
else 0
|
|
176
|
+
)
|
|
177
|
+
aggregate["total_duration"] = total_duration
|
|
178
|
+
aggregate["speed"] = speed
|
|
179
|
+
return aggregate
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def empty_totals():
|
|
183
|
+
return {
|
|
184
|
+
"requests": 0,
|
|
185
|
+
"prompt_tokens": 0,
|
|
186
|
+
"output_tokens": 0,
|
|
187
|
+
"total_tokens": 0,
|
|
188
|
+
"total_duration": 0,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def add_to_totals(totals, stats):
|
|
193
|
+
if stats is None:
|
|
194
|
+
return
|
|
195
|
+
totals["requests"] += 1
|
|
196
|
+
totals["prompt_tokens"] += stats.get("prompt_tokens", 0)
|
|
197
|
+
totals["output_tokens"] += stats.get("output_tokens", 0)
|
|
198
|
+
totals["total_tokens"] += stats.get("total_tokens", 0)
|
|
199
|
+
totals["total_duration"] += stats.get("total_duration") or 0
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def print_stats(stats, totals):
|
|
203
|
+
if stats is None:
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
if stats.get("source") == "direct_tool":
|
|
207
|
+
duration = format_duration(stats.get("total_duration"))
|
|
208
|
+
tool_label = stats.get("tool_name", "инструмент")
|
|
209
|
+
console.print(f"[dim]Ответ: {tool_label} • время: {duration}[/dim]")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
duration = format_duration(stats.get("total_duration"))
|
|
213
|
+
eval_time = format_duration(stats.get("eval_duration"))
|
|
214
|
+
prompt_tokens = stats.get("prompt_tokens", 0)
|
|
215
|
+
output_tokens = stats.get("output_tokens", 0)
|
|
216
|
+
speed = stats.get("speed", 0)
|
|
217
|
+
console.print(
|
|
218
|
+
f"[dim]Токены: {prompt_tokens} ввод + {output_tokens} ответ • "
|
|
219
|
+
f"скорость: {speed:.1f} т/с • время: {duration} (генерация {eval_time})[/dim]"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def generate_answer(model, messages, think_mode):
|
|
224
|
+
request_started_ns = time.perf_counter_ns()
|
|
225
|
+
last_user_text = next(
|
|
226
|
+
(
|
|
227
|
+
item["content"]
|
|
228
|
+
for item in reversed(messages)
|
|
229
|
+
if item.get("role") == "user"
|
|
230
|
+
),
|
|
231
|
+
"",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if needs_clarification(last_user_text):
|
|
235
|
+
answer = "Уточни, пожалуйста, что именно нужно сделать: повторить прошлый ответ, изменить его или решить новую задачу?"
|
|
236
|
+
console.print()
|
|
237
|
+
render_message("ИИ ›", role="assistant_label")
|
|
238
|
+
render_message(answer, role="assistant")
|
|
239
|
+
return answer, direct_tool_stats("clarification", request_started_ns), False
|
|
240
|
+
|
|
241
|
+
math_expression = parse_math_request(last_user_text)
|
|
242
|
+
if math_expression is None and re.search(
|
|
243
|
+
r"\bпроверь\w*\s+(?:это\s+)?калькулятор", last_user_text.casefold()
|
|
244
|
+
):
|
|
245
|
+
math_expression = previous_math_expression(messages[:-1])
|
|
246
|
+
if math_expression is not None:
|
|
247
|
+
result = calculate_expression(math_expression)
|
|
248
|
+
answer = format_direct_tool_result("calculate_expression", result)
|
|
249
|
+
console.print()
|
|
250
|
+
render_message("ИИ ›", role="assistant_label")
|
|
251
|
+
console.print()
|
|
252
|
+
render_message(f"Считаю: {math_expression}", role="tool")
|
|
253
|
+
render_message("Ответ ›", role="assistant_label")
|
|
254
|
+
render_message(answer, role="assistant")
|
|
255
|
+
return answer, direct_tool_stats("calculate_expression", request_started_ns), False
|
|
256
|
+
|
|
257
|
+
datetime_arguments, remaining_request = split_datetime_request(last_user_text)
|
|
258
|
+
if datetime_arguments is not None and not remaining_request:
|
|
259
|
+
answer = datetime_answer(datetime_arguments)
|
|
260
|
+
console.print()
|
|
261
|
+
render_message("ИИ ›", role="assistant_label")
|
|
262
|
+
console.print()
|
|
263
|
+
render_message("Вычисляю точные дату и время", role="tool")
|
|
264
|
+
render_message("Ответ ›", role="assistant_label")
|
|
265
|
+
render_message(answer, role="assistant")
|
|
266
|
+
return (
|
|
267
|
+
answer,
|
|
268
|
+
direct_tool_stats("calculate_datetime", request_started_ns),
|
|
269
|
+
False,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
working_messages = trim_messages_for_model(messages)
|
|
273
|
+
if datetime_arguments is not None and remaining_request:
|
|
274
|
+
for message in reversed(working_messages):
|
|
275
|
+
if message.get("role") == "user":
|
|
276
|
+
message["content"] = remaining_request
|
|
277
|
+
break
|
|
278
|
+
# Находим математические подвыражения в тексте для мгновенного точного авто-вычисления
|
|
279
|
+
precomputed_math = []
|
|
280
|
+
math_candidates = re.findall(r"[\d\s().,+\-*/%]{3,}", last_user_text.replace("×", "*").replace("÷", "/"))
|
|
281
|
+
for cand in math_candidates:
|
|
282
|
+
cand_clean = cand.strip(" ,.")
|
|
283
|
+
if re.search(r"[+\-*/%]", cand_clean) and re.search(r"\d", cand_clean) and len(cand_clean) >= 3:
|
|
284
|
+
val = calculate_expression(cand_clean)
|
|
285
|
+
if not val.startswith("Ошибка"):
|
|
286
|
+
precomputed_math.append(f"{cand_clean} = {val}")
|
|
287
|
+
|
|
288
|
+
math_pre_block = ""
|
|
289
|
+
if precomputed_math:
|
|
290
|
+
math_pre_block = (
|
|
291
|
+
"Точные математические вычисления системы:\n"
|
|
292
|
+
+ "\n".join(f"- {item}" for item in precomputed_math)
|
|
293
|
+
+ "\nИспользуй эти точные результаты в ответе без повторного вызова калькулятора."
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
now_dt = datetime.now()
|
|
297
|
+
date_info_block = (
|
|
298
|
+
f"Текущая реальная дата и время: {now_dt.strftime('%d.%m.%Y (%A)')}, текущий {now_dt.year} год. "
|
|
299
|
+
f"Для актуальных вопросов (события, игры, патчи, новости 'сейчас/сегодня') всегда учитывай "
|
|
300
|
+
f"{now_dt.year} год и составляй поисковые запросы с {now_dt.year} годом."
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
memory_block = relevant_memory_block(last_user_text)
|
|
304
|
+
working_messages[0]["content"] = (
|
|
305
|
+
f"{working_messages[0]['content']}\n\n"
|
|
306
|
+
f"Сейчас внутри W1x работает базовая модель Ollama: {model}. "
|
|
307
|
+
"Это не меняет твою личность W1x.\n\n"
|
|
308
|
+
f"{date_info_block}\n\n"
|
|
309
|
+
f"{TOOL_INSTRUCTIONS}\n\n{FORMAT_INSTRUCTIONS}"
|
|
310
|
+
+ (f"\n\n{memory_block}" if memory_block else "")
|
|
311
|
+
+ (f"\n\n{math_pre_block}" if math_pre_block else "")
|
|
312
|
+
)
|
|
313
|
+
aggregate_stats = None
|
|
314
|
+
tools_used = False
|
|
315
|
+
force_final_answer = False
|
|
316
|
+
followup_tool_functions = None
|
|
317
|
+
recovery_attempted = False
|
|
318
|
+
compact_tool_results = []
|
|
319
|
+
console.print()
|
|
320
|
+
render_message("ИИ ›", role="assistant_label")
|
|
321
|
+
|
|
322
|
+
for tool_round in range(MAX_TOOL_ROUNDS):
|
|
323
|
+
answer = ""
|
|
324
|
+
thinking = ""
|
|
325
|
+
tool_calls = []
|
|
326
|
+
last_chunk = None
|
|
327
|
+
interrupted = False
|
|
328
|
+
stream = None
|
|
329
|
+
chat_arguments = {
|
|
330
|
+
"model": model,
|
|
331
|
+
"messages": working_messages,
|
|
332
|
+
"stream": True,
|
|
333
|
+
"think": think_mode,
|
|
334
|
+
"keep_alive": get_keep_alive(),
|
|
335
|
+
"options": {
|
|
336
|
+
"temperature": 0.55 if force_final_answer else 0.72,
|
|
337
|
+
"top_p": 0.9,
|
|
338
|
+
"repeat_penalty": 1.1,
|
|
339
|
+
"num_ctx": get_context_tokens(),
|
|
340
|
+
"num_predict": MAX_RESPONSE_TOKENS,
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
if not force_final_answer:
|
|
344
|
+
chat_arguments["tools"] = TOOL_FUNCTIONS
|
|
345
|
+
elif followup_tool_functions:
|
|
346
|
+
chat_arguments["tools"] = followup_tool_functions
|
|
347
|
+
try:
|
|
348
|
+
stream = chat(**chat_arguments)
|
|
349
|
+
with Live(
|
|
350
|
+
console=console,
|
|
351
|
+
auto_refresh=False,
|
|
352
|
+
) as live:
|
|
353
|
+
for chunk in stream:
|
|
354
|
+
last_chunk = chunk
|
|
355
|
+
chunk_thinking = getattr(chunk.message, "thinking", None) or ""
|
|
356
|
+
chunk_answer = getattr(chunk.message, "content", None) or ""
|
|
357
|
+
chunk_tools = getattr(chunk.message, "tool_calls", None) or []
|
|
358
|
+
|
|
359
|
+
if chunk_tools:
|
|
360
|
+
tool_calls.extend(chunk_tools)
|
|
361
|
+
|
|
362
|
+
if chunk_thinking:
|
|
363
|
+
thinking += chunk_thinking
|
|
364
|
+
|
|
365
|
+
if chunk_answer:
|
|
366
|
+
answer += chunk_answer
|
|
367
|
+
|
|
368
|
+
renderables = []
|
|
369
|
+
if thinking:
|
|
370
|
+
renderables.append(
|
|
371
|
+
Panel(
|
|
372
|
+
ChatMarkdown(thinking),
|
|
373
|
+
title="Размышление",
|
|
374
|
+
border_style="bright_black",
|
|
375
|
+
padding=(0, 1),
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
if answer:
|
|
379
|
+
renderables.append(ChatMarkdown(answer))
|
|
380
|
+
|
|
381
|
+
if len(renderables) == 1:
|
|
382
|
+
live.update(renderables[0], refresh=True)
|
|
383
|
+
elif len(renderables) > 1:
|
|
384
|
+
live.update(Group(*renderables), refresh=True)
|
|
385
|
+
except KeyboardInterrupt:
|
|
386
|
+
interrupted = True
|
|
387
|
+
except Exception as error:
|
|
388
|
+
error_msg = str(error)
|
|
389
|
+
if not force_final_answer and (
|
|
390
|
+
"XML syntax error" in error_msg
|
|
391
|
+
or "status code: -1" in error_msg
|
|
392
|
+
or "tool" in error_msg.lower()
|
|
393
|
+
):
|
|
394
|
+
force_final_answer = True
|
|
395
|
+
continue
|
|
396
|
+
raise error
|
|
397
|
+
finally:
|
|
398
|
+
if stream is not None:
|
|
399
|
+
close = getattr(stream, "close", None)
|
|
400
|
+
if callable(close):
|
|
401
|
+
try:
|
|
402
|
+
close()
|
|
403
|
+
except Exception:
|
|
404
|
+
pass
|
|
405
|
+
|
|
406
|
+
should_recover = (
|
|
407
|
+
tools_used
|
|
408
|
+
and not tool_calls
|
|
409
|
+
and weak_tool_answer(answer)
|
|
410
|
+
and not recovery_attempted
|
|
411
|
+
)
|
|
412
|
+
should_escalate_after_recovery = (
|
|
413
|
+
tools_used
|
|
414
|
+
and not tool_calls
|
|
415
|
+
and weak_tool_answer(answer)
|
|
416
|
+
and recovery_attempted
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
aggregate_stats = merge_stats(aggregate_stats, collect_stats(last_chunk))
|
|
420
|
+
if interrupted:
|
|
421
|
+
return (
|
|
422
|
+
answer,
|
|
423
|
+
finalize_stats(aggregate_stats, request_started_ns),
|
|
424
|
+
True,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
if thinking or answer or tool_calls:
|
|
428
|
+
working_messages.append(
|
|
429
|
+
{
|
|
430
|
+
"role": "assistant",
|
|
431
|
+
"thinking": thinking,
|
|
432
|
+
"content": answer,
|
|
433
|
+
"tool_calls": tool_calls,
|
|
434
|
+
}
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
if not tool_calls:
|
|
438
|
+
if should_escalate_after_recovery:
|
|
439
|
+
render_message(
|
|
440
|
+
"Две локальные попытки не дали нормального ответа — ESCALATE",
|
|
441
|
+
role="notice",
|
|
442
|
+
)
|
|
443
|
+
escalated = run_with_spinner(
|
|
444
|
+
"Antigravity думает...",
|
|
445
|
+
lambda: ask_antigravity(
|
|
446
|
+
last_user_text,
|
|
447
|
+
"\n\n".join(compact_tool_results)[-1200:],
|
|
448
|
+
),
|
|
449
|
+
)
|
|
450
|
+
render_message("Antigravity ›", role="assistant_label")
|
|
451
|
+
render_message(escalated, role="assistant")
|
|
452
|
+
if aggregate_stats is not None:
|
|
453
|
+
aggregate_stats["tool_answers"] = (
|
|
454
|
+
aggregate_stats.get("tool_answers", 0) + 1
|
|
455
|
+
)
|
|
456
|
+
return (
|
|
457
|
+
escalated,
|
|
458
|
+
finalize_stats(aggregate_stats, request_started_ns),
|
|
459
|
+
False,
|
|
460
|
+
)
|
|
461
|
+
if should_recover:
|
|
462
|
+
recovery_attempted = True
|
|
463
|
+
working_messages.append(
|
|
464
|
+
{
|
|
465
|
+
"role": "user",
|
|
466
|
+
"content": (
|
|
467
|
+
"Твой предыдущий ответ пустой или содержит отказ, хотя "
|
|
468
|
+
"инструменты уже вернули полезные данные. Ответь по "
|
|
469
|
+
"найденным данным прямо сейчас. Если информации точно "
|
|
470
|
+
"нет, ответь одним словом: ESCALATE"
|
|
471
|
+
),
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
continue
|
|
475
|
+
return (
|
|
476
|
+
answer,
|
|
477
|
+
finalize_stats(aggregate_stats, request_started_ns),
|
|
478
|
+
False,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
round_tool_results = []
|
|
482
|
+
for call in tool_calls:
|
|
483
|
+
result = execute_tool_call(call)
|
|
484
|
+
round_tool_results.append((call.function.name, result))
|
|
485
|
+
tools_used = True
|
|
486
|
+
compact_tool_results.append(
|
|
487
|
+
f"{call.function.name}: {result[:2200]}"
|
|
488
|
+
)
|
|
489
|
+
working_messages.append(
|
|
490
|
+
{
|
|
491
|
+
"role": "tool",
|
|
492
|
+
"tool_name": call.function.name,
|
|
493
|
+
"content": result,
|
|
494
|
+
}
|
|
495
|
+
)
|
|
496
|
+
if round_tool_results and all(
|
|
497
|
+
name in DIRECT_PASSTHROUGH_TOOLS for name, _ in round_tool_results
|
|
498
|
+
):
|
|
499
|
+
answer = "\n\n".join(
|
|
500
|
+
format_direct_tool_result(name, result)
|
|
501
|
+
for name, result in round_tool_results
|
|
502
|
+
)
|
|
503
|
+
console.print()
|
|
504
|
+
label = (
|
|
505
|
+
"Antigravity ›"
|
|
506
|
+
if any(name == "ask_antigravity" for name, _ in round_tool_results)
|
|
507
|
+
else "Ответ ›"
|
|
508
|
+
)
|
|
509
|
+
render_message(label, role="assistant_label")
|
|
510
|
+
render_message(answer, role="assistant")
|
|
511
|
+
if aggregate_stats is not None:
|
|
512
|
+
aggregate_stats["tool_answers"] = (
|
|
513
|
+
aggregate_stats.get("tool_answers", 0) + len(round_tool_results)
|
|
514
|
+
)
|
|
515
|
+
return (
|
|
516
|
+
answer,
|
|
517
|
+
finalize_stats(aggregate_stats, request_started_ns),
|
|
518
|
+
False,
|
|
519
|
+
)
|
|
520
|
+
used_names = {name for name, _ in round_tool_results}
|
|
521
|
+
python_results = [
|
|
522
|
+
result
|
|
523
|
+
for name, result in round_tool_results
|
|
524
|
+
if name == "run_python_code"
|
|
525
|
+
]
|
|
526
|
+
python_check_unreliable = any(
|
|
527
|
+
"СТАТУС ПРОВЕРКИ: НЕОПРЕДЕЛЁН" in result
|
|
528
|
+
or not result.startswith("Код завершения: 0")
|
|
529
|
+
for result in python_results
|
|
530
|
+
)
|
|
531
|
+
if "search_web" in used_names or "run_python_code" in used_names or "memory" in used_names:
|
|
532
|
+
force_final_answer = True
|
|
533
|
+
followup_tool_functions = [calculate_datetime, calculate_expression] if "search_web" in used_names else None
|
|
534
|
+
next_instruction = (
|
|
535
|
+
"Теперь сформулируй итоговый, естественный и дружелюбный ответ для пользователя на основе полученных данных. "
|
|
536
|
+
"Если использовалась память — ответь человеческим языком (например: 'Твоего хомяка зовут Ракета!'), без служебных маркеров. "
|
|
537
|
+
"Если пользователь просил написать код — предоставь чистый код в блоке ```python ... ```. "
|
|
538
|
+
"Не вызывай другие инструменты."
|
|
539
|
+
)
|
|
540
|
+
elif tool_round == 0 and not python_check_unreliable:
|
|
541
|
+
force_final_answer = False
|
|
542
|
+
followup_tool_functions = None
|
|
543
|
+
next_instruction = (
|
|
544
|
+
"Проанализируй полученный результат. Если всё понятно — начни с прямого вывода и дай итоговый ответ."
|
|
545
|
+
)
|
|
546
|
+
else:
|
|
547
|
+
force_final_answer = True
|
|
548
|
+
followup_tool_functions = None
|
|
549
|
+
next_instruction = (
|
|
550
|
+
"Теперь ответь на исходный вопрос, используя результаты проверок выше. "
|
|
551
|
+
"Начни с прямого вывода. Не вызывай другие инструменты."
|
|
552
|
+
)
|
|
553
|
+
working_messages.append(
|
|
554
|
+
{
|
|
555
|
+
"role": "user",
|
|
556
|
+
"content": (
|
|
557
|
+
f"{next_instruction}\n"
|
|
558
|
+
f"Исходный вопрос: {last_user_text}"
|
|
559
|
+
),
|
|
560
|
+
}
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
answer = "Остановился: модель превысила лимит последовательных вызовов инструментов."
|
|
564
|
+
render_message(answer, role="notice")
|
|
565
|
+
return answer, finalize_stats(aggregate_stats, request_started_ns), False
|