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/app.py
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Главный интерфейс приложения W1x, слэш-команды и цикл диалога."""
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
from .config import (
|
|
10
|
+
APP_VERSION,
|
|
11
|
+
CHAT_DIR,
|
|
12
|
+
CURRENT_CHAT_PATH,
|
|
13
|
+
FACTS_PATH,
|
|
14
|
+
MAX_SAVED_MESSAGES,
|
|
15
|
+
DEFAULT_SYSTEM_PROMPT,
|
|
16
|
+
EXIT_COMMANDS,
|
|
17
|
+
ACTIVE_SESSION_DATA,
|
|
18
|
+
)
|
|
19
|
+
from .terminal import (
|
|
20
|
+
console,
|
|
21
|
+
render_message,
|
|
22
|
+
read_input,
|
|
23
|
+
copy_to_clipboard,
|
|
24
|
+
show_terminal_emoji_hint,
|
|
25
|
+
)
|
|
26
|
+
from .puller import GLOBAL_PULL_QUEUE, first_time_setup_wizard, pull_model
|
|
27
|
+
from .models import (
|
|
28
|
+
choose_model,
|
|
29
|
+
installed_models,
|
|
30
|
+
preload_model,
|
|
31
|
+
start_background_preload,
|
|
32
|
+
wait_for_model_ready,
|
|
33
|
+
reset_all_models,
|
|
34
|
+
unload_all_models,
|
|
35
|
+
interactive_config_menu,
|
|
36
|
+
print_models,
|
|
37
|
+
get_last_model,
|
|
38
|
+
)
|
|
39
|
+
from .memory import (
|
|
40
|
+
load_facts,
|
|
41
|
+
remember_fact,
|
|
42
|
+
recall_facts,
|
|
43
|
+
forget_facts,
|
|
44
|
+
extract_remember_request,
|
|
45
|
+
extract_forget_request,
|
|
46
|
+
looks_like_memory_question,
|
|
47
|
+
migrate_explicit_memory,
|
|
48
|
+
)
|
|
49
|
+
from .tools import (
|
|
50
|
+
set_file_workspace,
|
|
51
|
+
run_agy_for_user,
|
|
52
|
+
should_auto_escalate,
|
|
53
|
+
)
|
|
54
|
+
from .testpy import (
|
|
55
|
+
extract_latest_python_code,
|
|
56
|
+
run_interactive_python_test,
|
|
57
|
+
)
|
|
58
|
+
from .engine import (
|
|
59
|
+
generate_answer,
|
|
60
|
+
direct_tool_stats,
|
|
61
|
+
empty_totals,
|
|
62
|
+
add_to_totals,
|
|
63
|
+
print_stats,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def new_context(system_prompt):
|
|
68
|
+
return [{"role": "system", "content": system_prompt}]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def save_chat(messages, requested_name=""):
|
|
72
|
+
CHAT_DIR.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
if requested_name:
|
|
74
|
+
safe_name = Path(requested_name).name
|
|
75
|
+
if not safe_name.lower().endswith(".md"):
|
|
76
|
+
safe_name += ".md"
|
|
77
|
+
else:
|
|
78
|
+
safe_name = datetime.now().strftime("chat-%Y-%m-%d-%H%M%S.md")
|
|
79
|
+
path = CHAT_DIR / safe_name
|
|
80
|
+
|
|
81
|
+
lines = ["# Ollama chat", ""]
|
|
82
|
+
for message in messages:
|
|
83
|
+
if message["role"] == "system":
|
|
84
|
+
continue
|
|
85
|
+
title = "Вы" if message["role"] == "user" else "Ассистент"
|
|
86
|
+
lines.extend([f"## {title}", "", message["content"], ""])
|
|
87
|
+
path.write_text("\n".join(lines), encoding="utf-8")
|
|
88
|
+
return path
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def save_current_chat(system_prompt, messages):
|
|
92
|
+
"""Автоматически сохраняет текущий диалог локально."""
|
|
93
|
+
saved_messages = [
|
|
94
|
+
{
|
|
95
|
+
"role": message["role"],
|
|
96
|
+
"content": message["content"],
|
|
97
|
+
}
|
|
98
|
+
for message in messages
|
|
99
|
+
if message.get("role") in {"user", "assistant"}
|
|
100
|
+
and isinstance(message.get("content"), str)
|
|
101
|
+
][-MAX_SAVED_MESSAGES:]
|
|
102
|
+
|
|
103
|
+
state = {
|
|
104
|
+
"version": 1,
|
|
105
|
+
"system_prompt": system_prompt,
|
|
106
|
+
"custom_system_prompt": system_prompt != DEFAULT_SYSTEM_PROMPT,
|
|
107
|
+
"messages": saved_messages,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
CHAT_DIR.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
temporary_path = CURRENT_CHAT_PATH.with_suffix(".tmp")
|
|
113
|
+
temporary_path.write_text(
|
|
114
|
+
json.dumps(state, ensure_ascii=False, indent=2),
|
|
115
|
+
encoding="utf-8",
|
|
116
|
+
)
|
|
117
|
+
temporary_path.replace(CURRENT_CHAT_PATH)
|
|
118
|
+
except OSError as error:
|
|
119
|
+
console.print(f"[yellow]Не удалось сохранить память: {error}[/yellow]")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def load_current_chat():
|
|
123
|
+
"""Восстанавливает последний диалог или создаёт новый."""
|
|
124
|
+
if not CURRENT_CHAT_PATH.exists():
|
|
125
|
+
return DEFAULT_SYSTEM_PROMPT, new_context(DEFAULT_SYSTEM_PROMPT)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
state = json.loads(CURRENT_CHAT_PATH.read_text(encoding="utf-8"))
|
|
129
|
+
saved_system_prompt = state.get("system_prompt") or ""
|
|
130
|
+
has_custom_prompt = state.get("custom_system_prompt") is True
|
|
131
|
+
system_prompt = (
|
|
132
|
+
saved_system_prompt
|
|
133
|
+
if has_custom_prompt and saved_system_prompt
|
|
134
|
+
else DEFAULT_SYSTEM_PROMPT
|
|
135
|
+
)
|
|
136
|
+
saved_messages = []
|
|
137
|
+
|
|
138
|
+
discard_legacy_style = (
|
|
139
|
+
"custom_system_prompt" not in state
|
|
140
|
+
and saved_system_prompt
|
|
141
|
+
and saved_system_prompt != DEFAULT_SYSTEM_PROMPT
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
for message in ([] if discard_legacy_style else state.get("messages", [])):
|
|
145
|
+
role = message.get("role")
|
|
146
|
+
content = message.get("content")
|
|
147
|
+
if role in {"user", "assistant"} and isinstance(content, str):
|
|
148
|
+
saved_messages.append({"role": role, "content": content})
|
|
149
|
+
|
|
150
|
+
if discard_legacy_style:
|
|
151
|
+
console.print("[dim]Старый экспериментальный стиль сброшен.[/dim]")
|
|
152
|
+
|
|
153
|
+
return system_prompt, [
|
|
154
|
+
{"role": "system", "content": system_prompt},
|
|
155
|
+
*saved_messages[-MAX_SAVED_MESSAGES:],
|
|
156
|
+
]
|
|
157
|
+
except (OSError, ValueError, TypeError) as error:
|
|
158
|
+
console.print(f"[yellow]Не удалось восстановить память: {error}[/yellow]")
|
|
159
|
+
return DEFAULT_SYSTEM_PROMPT, new_context(DEFAULT_SYSTEM_PROMPT)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def print_status(model, think_mode, stats_enabled, messages):
|
|
163
|
+
from . import config
|
|
164
|
+
think_text = "выключен" if not think_mode else str(think_mode)
|
|
165
|
+
turns = sum(message["role"] == "user" for message in messages)
|
|
166
|
+
console.print(
|
|
167
|
+
Panel(
|
|
168
|
+
f"Модель: [bold]{model}[/bold]\n"
|
|
169
|
+
f"ИИ: [bold]W1x[/bold] • разработчик: [bold]W1bbex[/bold]\n"
|
|
170
|
+
f"Сборка: [bold]{APP_VERSION}[/bold]\n"
|
|
171
|
+
f"Thinking: [bold]{think_text}[/bold]\n"
|
|
172
|
+
"Инструменты: [bold]поиск, время, калькулятор, Python, память, "
|
|
173
|
+
"файлы, Antigravity[/bold]\n"
|
|
174
|
+
f"Папка проекта: [bold]{config.ACTIVE_FILE_ROOT or 'не выбрана'}[/bold]\n"
|
|
175
|
+
f"Статистика: [bold]{'включена' if stats_enabled else 'выключена'}[/bold]\n"
|
|
176
|
+
f"Сообщений пользователя: [bold]{turns}[/bold]",
|
|
177
|
+
title="Состояние",
|
|
178
|
+
border_style="cyan",
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def print_header(model):
|
|
184
|
+
console.print(
|
|
185
|
+
Panel(
|
|
186
|
+
f"[bold green]{model}[/bold green]\n"
|
|
187
|
+
f"[dim]W1x CLI • v{APP_VERSION}[/dim]\n"
|
|
188
|
+
"[dim]Enter — отправить • Shift+Enter — новая строка • "
|
|
189
|
+
"/help — команды[/dim]",
|
|
190
|
+
title="W1x",
|
|
191
|
+
border_style="green",
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def print_help():
|
|
197
|
+
console.print(
|
|
198
|
+
"""
|
|
199
|
+
[bold]Ввод[/bold]
|
|
200
|
+
Enter отправить сообщение
|
|
201
|
+
Shift+Enter новая строка
|
|
202
|
+
Ctrl+Enter новая строка
|
|
203
|
+
Alt+Enter запасной вариант новой строки
|
|
204
|
+
Ctrl+S вернуть прошлый вопрос в строку ввода и стереть из памяти
|
|
205
|
+
Ctrl+Q выгрузить все модели и выйти
|
|
206
|
+
Ctrl+Backspace удалить слово слева
|
|
207
|
+
Ctrl+Delete удалить слово справа
|
|
208
|
+
Ctrl+A выделить весь текст; Delete/Backspace — очистить
|
|
209
|
+
Ctrl+X вырезать выделенный текст (или весь ввод) в буфер
|
|
210
|
+
Ctrl+C копировать выделение / остановить генерацию
|
|
211
|
+
Ctrl+V вставить текст из буфера обмена
|
|
212
|
+
/ком... серое продолжение и список; Tab/Enter — подставить
|
|
213
|
+
|
|
214
|
+
[bold]Команды[/bold]
|
|
215
|
+
[cyan]/model[/cyan] сменить модель (стрелки ↑/↓ или Enter)
|
|
216
|
+
[cyan]/pull [модели...][/cyan] скачать модели из библиотеки Ollama
|
|
217
|
+
[cyan]/config[/cyan] настройки контекста, VRAM и модели
|
|
218
|
+
[cyan]/models[/cyan] список установленных моделей
|
|
219
|
+
[cyan]/new[/cyan], [cyan]/clear[/cyan] начать новый диалог
|
|
220
|
+
[cyan]/undo[/cyan] удалить последний вопрос и ответ
|
|
221
|
+
[cyan]/retry[/cyan] заново ответить на последний вопрос
|
|
222
|
+
[cyan]/think[/cyan] включить/выключить размышления
|
|
223
|
+
[cyan]/think low|medium|high[/cyan] выбрать уровень размышлений
|
|
224
|
+
[cyan]/tools[/cyan] доступные инструменты
|
|
225
|
+
[cyan]/agy задача[/cyan] передать задачу Antigravity напрямую
|
|
226
|
+
[cyan]/workspace путь[/cyan] выбрать папку проекта для безопасного чтения
|
|
227
|
+
[cyan]/stats[/cyan] статистика прошлого и следующих ответов
|
|
228
|
+
[cyan]/status[/cyan] текущие настройки
|
|
229
|
+
[cyan]/system[/cyan] показать системную инструкцию
|
|
230
|
+
[cyan]/system текст[/cyan] заменить системную инструкцию
|
|
231
|
+
[cyan]/copy[/cyan] скопировать последний ответ
|
|
232
|
+
[cyan]/testpy[/cyan] протестировать последний Python-код в отдельном окне
|
|
233
|
+
[cyan]/save[/cyan] [имя] сохранить диалог в Markdown
|
|
234
|
+
[cyan]/memory[/cyan] показать состояние постоянной памяти
|
|
235
|
+
[cyan]/forget текст[/cyan] удалить подходящий факт из памяти
|
|
236
|
+
[cyan]/cls[/cyan] очистить экран
|
|
237
|
+
[cyan]/help[/cyan] эта справка
|
|
238
|
+
[cyan]q[/cyan], [cyan]/exit[/cyan] выгрузить модели и выйти
|
|
239
|
+
"""
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def shutdown():
|
|
244
|
+
console.print()
|
|
245
|
+
reset_all_models()
|
|
246
|
+
console.print("[dim]Все модели выгружены.[/dim]")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def looks_like_launch_command(text):
|
|
250
|
+
normalized = text.strip().lower().replace('"', "")
|
|
251
|
+
return (
|
|
252
|
+
".py" in normalized
|
|
253
|
+
and (
|
|
254
|
+
"python.exe" in normalized
|
|
255
|
+
or normalized.startswith("python ")
|
|
256
|
+
or normalized.startswith("py ")
|
|
257
|
+
or normalized.startswith("& ")
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def main():
|
|
263
|
+
import sys
|
|
264
|
+
from . import config
|
|
265
|
+
|
|
266
|
+
cli_args = sys.argv[1:]
|
|
267
|
+
if cli_args:
|
|
268
|
+
first_arg = cli_args[0].lower()
|
|
269
|
+
if first_arg in ("-q", "--unload", "/unload", "unload"):
|
|
270
|
+
unloaded = unload_all_models()
|
|
271
|
+
msg = ", ".join(unloaded) if unloaded else "нет активных моделей"
|
|
272
|
+
console.print(f"[green]✓ Все модели выгружены из VRAM ({msg}).[/green]")
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
if first_arg in ("pull", "download", "--pull", "скачать"):
|
|
276
|
+
models_to_pull = cli_args[1:]
|
|
277
|
+
if not models_to_pull:
|
|
278
|
+
first_time_setup_wizard()
|
|
279
|
+
return
|
|
280
|
+
GLOBAL_PULL_QUEUE.add(models_to_pull)
|
|
281
|
+
GLOBAL_PULL_QUEUE.run_all()
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
if first_arg in ("--config", "-c", "config", "/config", "/settings"):
|
|
285
|
+
interactive_config_menu()
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
models = installed_models()
|
|
289
|
+
if not models:
|
|
290
|
+
console.print("[yellow]W1x: первый запуск. Выберите стартовую модель для загрузки:[/yellow]")
|
|
291
|
+
downloaded = first_time_setup_wizard()
|
|
292
|
+
if not downloaded:
|
|
293
|
+
return
|
|
294
|
+
models = installed_models()
|
|
295
|
+
|
|
296
|
+
target_model = get_last_model(models)
|
|
297
|
+
question_parts = []
|
|
298
|
+
i = 0
|
|
299
|
+
while i < len(cli_args):
|
|
300
|
+
arg = cli_args[i]
|
|
301
|
+
if arg in ("-m", "--model") and i + 1 < len(cli_args):
|
|
302
|
+
custom_m = cli_args[i + 1]
|
|
303
|
+
if custom_m in models:
|
|
304
|
+
target_model = custom_m
|
|
305
|
+
i += 2
|
|
306
|
+
else:
|
|
307
|
+
question_parts.append(arg)
|
|
308
|
+
i += 1
|
|
309
|
+
|
|
310
|
+
question = " ".join(question_parts).strip()
|
|
311
|
+
if not question:
|
|
312
|
+
question = "Привет!"
|
|
313
|
+
|
|
314
|
+
system_prompt = config.DEFAULT_SYSTEM_PROMPT
|
|
315
|
+
messages = [
|
|
316
|
+
{"role": "system", "content": system_prompt},
|
|
317
|
+
{"role": "user", "content": question},
|
|
318
|
+
]
|
|
319
|
+
try:
|
|
320
|
+
wait_for_model_ready(target_model)
|
|
321
|
+
generate_answer(target_model, messages, think_mode=False)
|
|
322
|
+
except KeyboardInterrupt:
|
|
323
|
+
console.print("\n[yellow]Прервано пользователем.[/yellow]")
|
|
324
|
+
except Exception as error:
|
|
325
|
+
console.print(f"[red]Ошибка: {error}[/red]")
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
show_terminal_emoji_hint()
|
|
329
|
+
model = choose_model()
|
|
330
|
+
if model is None:
|
|
331
|
+
shutdown()
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
# Запускаем прогрев в фоне: поле ввода открывается мгновенно, не блокируя пользователя!
|
|
335
|
+
start_background_preload(model)
|
|
336
|
+
|
|
337
|
+
system_prompt, messages = load_current_chat()
|
|
338
|
+
ACTIVE_SESSION_DATA["messages"] = messages
|
|
339
|
+
ACTIVE_SESSION_DATA["system_prompt"] = system_prompt
|
|
340
|
+
migrated_facts = migrate_explicit_memory(messages)
|
|
341
|
+
save_current_chat(system_prompt, messages)
|
|
342
|
+
think_mode = False
|
|
343
|
+
stats_enabled = False
|
|
344
|
+
last_stats = None
|
|
345
|
+
totals = empty_totals()
|
|
346
|
+
last_response = next(
|
|
347
|
+
(
|
|
348
|
+
item["content"]
|
|
349
|
+
for item in reversed(messages)
|
|
350
|
+
if item["role"] == "assistant"
|
|
351
|
+
),
|
|
352
|
+
"",
|
|
353
|
+
)
|
|
354
|
+
print_header(model)
|
|
355
|
+
restored_turns = sum(item["role"] == "user" for item in messages)
|
|
356
|
+
if restored_turns:
|
|
357
|
+
console.print(
|
|
358
|
+
f"[dim]Память восстановлена: {restored_turns} "
|
|
359
|
+
f"сообщений пользователя.[/dim]"
|
|
360
|
+
)
|
|
361
|
+
if migrated_facts:
|
|
362
|
+
console.print(
|
|
363
|
+
f"[dim]В постоянную память перенесено фактов: {migrated_facts}.[/dim]"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
while True:
|
|
367
|
+
try:
|
|
368
|
+
prompt = read_input().strip()
|
|
369
|
+
except KeyboardInterrupt:
|
|
370
|
+
console.print("\n[dim]Для выхода введи q или /exit.[/dim]")
|
|
371
|
+
continue
|
|
372
|
+
except EOFError:
|
|
373
|
+
shutdown()
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
if not prompt:
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
command_line = prompt.split(maxsplit=1)
|
|
380
|
+
command = command_line[0].lower()
|
|
381
|
+
argument = command_line[1].strip() if len(command_line) > 1 else ""
|
|
382
|
+
|
|
383
|
+
if prompt.lower() in EXIT_COMMANDS:
|
|
384
|
+
shutdown()
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
if looks_like_launch_command(prompt):
|
|
388
|
+
console.print(
|
|
389
|
+
"[yellow]Это команда запуска Python. Сейчас ты внутри чата: "
|
|
390
|
+
"введи q, а затем выполни команду в CMD или PowerShell.[/yellow]"
|
|
391
|
+
)
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
if command in {"/help", "/?"}:
|
|
395
|
+
print_help()
|
|
396
|
+
continue
|
|
397
|
+
|
|
398
|
+
if command in {"/config", "/settings", "/настройки"}:
|
|
399
|
+
interactive_config_menu()
|
|
400
|
+
continue
|
|
401
|
+
|
|
402
|
+
if command in {"/unload", "/free"}:
|
|
403
|
+
reset_all_models(show_message=True)
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
if command == "/models":
|
|
407
|
+
print_models(installed_models())
|
|
408
|
+
continue
|
|
409
|
+
|
|
410
|
+
if command in {"/pull", "/download", "/скачать"}:
|
|
411
|
+
models_to_pull = argument.split()
|
|
412
|
+
if not models_to_pull:
|
|
413
|
+
downloaded = first_time_setup_wizard()
|
|
414
|
+
if downloaded:
|
|
415
|
+
model = downloaded
|
|
416
|
+
start_background_preload(model)
|
|
417
|
+
print_header(model)
|
|
418
|
+
else:
|
|
419
|
+
GLOBAL_PULL_QUEUE.add(models_to_pull)
|
|
420
|
+
GLOBAL_PULL_QUEUE.run_all()
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
if command == "/model":
|
|
424
|
+
selected = choose_model(current=model)
|
|
425
|
+
if selected is None or selected == model:
|
|
426
|
+
continue
|
|
427
|
+
reset_all_models()
|
|
428
|
+
model = selected
|
|
429
|
+
start_background_preload(model)
|
|
430
|
+
print_header(model)
|
|
431
|
+
continue
|
|
432
|
+
|
|
433
|
+
if command in {"/new", "/clear"}:
|
|
434
|
+
messages = new_context(system_prompt)
|
|
435
|
+
ACTIVE_SESSION_DATA["messages"] = messages
|
|
436
|
+
last_stats = None
|
|
437
|
+
last_response = ""
|
|
438
|
+
totals = empty_totals()
|
|
439
|
+
save_current_chat(system_prompt, messages)
|
|
440
|
+
console.print("[green]Новый диалог начат.[/green]")
|
|
441
|
+
continue
|
|
442
|
+
|
|
443
|
+
if command == "/cls":
|
|
444
|
+
console.clear()
|
|
445
|
+
print_header(model)
|
|
446
|
+
continue
|
|
447
|
+
|
|
448
|
+
if command == "/stats":
|
|
449
|
+
stats_enabled = not stats_enabled
|
|
450
|
+
state = "включена" if stats_enabled else "выключена"
|
|
451
|
+
console.print(f"[green]Статистика {state}.[/green]")
|
|
452
|
+
if stats_enabled:
|
|
453
|
+
print_stats(last_stats, totals)
|
|
454
|
+
continue
|
|
455
|
+
|
|
456
|
+
if command == "/tools":
|
|
457
|
+
console.print(
|
|
458
|
+
Panel(
|
|
459
|
+
"[bold]search_web[/bold] — поиск свежей информации\n"
|
|
460
|
+
"[bold]calculate_datetime[/bold] — дата, время и точные смещения\n"
|
|
461
|
+
"[bold]calculate_expression[/bold] — точная арифметика\n"
|
|
462
|
+
"[bold]run_python_code[/bold] — проверка Python, только после подтверждения\n"
|
|
463
|
+
"[bold]remember/recall/forget[/bold] — постоянная память фактов\n"
|
|
464
|
+
"[bold]list/read_project[/bold] — безопасное чтение выбранной папки\n"
|
|
465
|
+
"[bold]ask_antigravity[/bold] — старшая модель для сложных задач\n\n"
|
|
466
|
+
"[dim]Просто сформулируй задачу: модель сама выберет инструмент.[/dim]",
|
|
467
|
+
title="Инструменты",
|
|
468
|
+
border_style="magenta",
|
|
469
|
+
)
|
|
470
|
+
)
|
|
471
|
+
continue
|
|
472
|
+
|
|
473
|
+
if command == "/workspace":
|
|
474
|
+
if not argument:
|
|
475
|
+
console.print(
|
|
476
|
+
f"[dim]Текущая папка: {config.ACTIVE_FILE_ROOT or 'не выбрана'}[/dim]\n"
|
|
477
|
+
"Используй: [cyan]/workspace C:\\путь\\к\\проекту[/cyan] "
|
|
478
|
+
"или [cyan]/workspace off[/cyan]"
|
|
479
|
+
)
|
|
480
|
+
continue
|
|
481
|
+
if argument.casefold() in {"off", "none", "выкл"}:
|
|
482
|
+
config.ACTIVE_FILE_ROOT = None
|
|
483
|
+
console.print("[green]Доступ к папке проекта отключён.[/green]")
|
|
484
|
+
continue
|
|
485
|
+
try:
|
|
486
|
+
selected_root = set_file_workspace(argument.strip('"'))
|
|
487
|
+
console.print(
|
|
488
|
+
f"[green]Папка проекта выбрана только для чтения:[/green] {selected_root}"
|
|
489
|
+
)
|
|
490
|
+
except Exception as error:
|
|
491
|
+
console.print(f"[red]Не удалось выбрать папку: {error}[/red]")
|
|
492
|
+
continue
|
|
493
|
+
|
|
494
|
+
if command == "/agy":
|
|
495
|
+
if not argument:
|
|
496
|
+
console.print("[dim]Используй: /agy задача для старшей модели[/dim]")
|
|
497
|
+
continue
|
|
498
|
+
started_ns = time.perf_counter_ns()
|
|
499
|
+
try:
|
|
500
|
+
result = run_agy_for_user(argument, messages)
|
|
501
|
+
except KeyboardInterrupt:
|
|
502
|
+
console.print("\n[yellow]Antigravity остановлена пользователем.[/yellow]")
|
|
503
|
+
continue
|
|
504
|
+
messages.append({"role": "user", "content": argument})
|
|
505
|
+
messages.append({"role": "assistant", "content": result})
|
|
506
|
+
save_current_chat(system_prompt, messages)
|
|
507
|
+
last_response = result
|
|
508
|
+
last_stats = direct_tool_stats("Antigravity", started_ns)
|
|
509
|
+
add_to_totals(totals, last_stats)
|
|
510
|
+
if stats_enabled:
|
|
511
|
+
print_stats(last_stats, totals)
|
|
512
|
+
continue
|
|
513
|
+
|
|
514
|
+
if command == "/think":
|
|
515
|
+
value = argument.lower()
|
|
516
|
+
if not value:
|
|
517
|
+
think_mode = False if think_mode else True
|
|
518
|
+
elif value in {"off", "false", "0"}:
|
|
519
|
+
think_mode = False
|
|
520
|
+
elif value in {"on", "true", "1"}:
|
|
521
|
+
think_mode = True
|
|
522
|
+
elif value in {"low", "medium", "high"}:
|
|
523
|
+
think_mode = value
|
|
524
|
+
else:
|
|
525
|
+
console.print("[red]Используй: /think off|on|low|medium|high[/red]")
|
|
526
|
+
continue
|
|
527
|
+
label = "выключен" if not think_mode else str(think_mode)
|
|
528
|
+
console.print(f"[green]Thinking: {label}.[/green]")
|
|
529
|
+
continue
|
|
530
|
+
|
|
531
|
+
if command == "/status":
|
|
532
|
+
print_status(model, think_mode, stats_enabled, messages)
|
|
533
|
+
continue
|
|
534
|
+
|
|
535
|
+
if command == "/memory":
|
|
536
|
+
turns = sum(item["role"] == "user" for item in messages)
|
|
537
|
+
facts = load_facts()
|
|
538
|
+
fact_preview = "\n".join(
|
|
539
|
+
f"• {item['fact']}" for item in facts[-10:]
|
|
540
|
+
) or "Постоянных фактов пока нет."
|
|
541
|
+
console.print(
|
|
542
|
+
Panel(
|
|
543
|
+
f"Сохранено сообщений пользователя: [bold]{turns}[/bold]\n"
|
|
544
|
+
f"Лимит истории: [bold]{MAX_SAVED_MESSAGES} сообщений[/bold]\n"
|
|
545
|
+
f"Постоянных фактов: [bold]{len(facts)}[/bold]\n"
|
|
546
|
+
f"Файл диалога: {CURRENT_CHAT_PATH}\n"
|
|
547
|
+
f"Файл фактов: {FACTS_PATH}\n\n"
|
|
548
|
+
f"{fact_preview}\n\n"
|
|
549
|
+
"[dim]/new очищает диалог, но не постоянные факты; "
|
|
550
|
+
"/forget текст удаляет факт[/dim]",
|
|
551
|
+
title="Постоянная память",
|
|
552
|
+
border_style="cyan",
|
|
553
|
+
)
|
|
554
|
+
)
|
|
555
|
+
continue
|
|
556
|
+
|
|
557
|
+
if command == "/forget":
|
|
558
|
+
result = forget_facts(argument)
|
|
559
|
+
render_message(result, role="notice" if result.startswith("Ошибка") else "assistant")
|
|
560
|
+
continue
|
|
561
|
+
|
|
562
|
+
if command == "/system":
|
|
563
|
+
if not argument:
|
|
564
|
+
console.print(Panel(system_prompt, title="System prompt"))
|
|
565
|
+
else:
|
|
566
|
+
system_prompt = argument
|
|
567
|
+
messages[0] = {"role": "system", "content": system_prompt}
|
|
568
|
+
ACTIVE_SESSION_DATA["system_prompt"] = system_prompt
|
|
569
|
+
save_current_chat(system_prompt, messages)
|
|
570
|
+
console.print("[green]Системная инструкция обновлена.[/green]")
|
|
571
|
+
continue
|
|
572
|
+
|
|
573
|
+
if command == "/copy":
|
|
574
|
+
if not last_response:
|
|
575
|
+
console.print("[dim]Ответа для копирования пока нет.[/dim]")
|
|
576
|
+
continue
|
|
577
|
+
try:
|
|
578
|
+
copy_to_clipboard(last_response)
|
|
579
|
+
console.print("[green]Последний ответ скопирован.[/green]")
|
|
580
|
+
except Exception as error:
|
|
581
|
+
console.print(f"[red]Не удалось скопировать: {error}[/red]")
|
|
582
|
+
continue
|
|
583
|
+
|
|
584
|
+
if command == "/testpy":
|
|
585
|
+
code_to_test = argument or extract_latest_python_code(messages)
|
|
586
|
+
if not code_to_test:
|
|
587
|
+
console.print("[yellow]В диалоге не найдено исполняемого Python-кода для тестирования.[/yellow]")
|
|
588
|
+
console.print("[dim]Подсказка: вы можете передать код напрямую: /testpy print('hello')[/dim]")
|
|
589
|
+
continue
|
|
590
|
+
|
|
591
|
+
lines_count = len(code_to_test.splitlines())
|
|
592
|
+
first_line = code_to_test.splitlines()[0][:60]
|
|
593
|
+
console.print(f"[dim]Тестирую Python-код ({lines_count} строк: '{first_line}')...[/dim]")
|
|
594
|
+
|
|
595
|
+
exit_code, log_content = run_interactive_python_test(code_to_test)
|
|
596
|
+
if exit_code == 0:
|
|
597
|
+
console.print("[green]✓ Тестирование завершено (код 0). Результат сохранён в диалог.[/green]")
|
|
598
|
+
else:
|
|
599
|
+
console.print(f"[yellow]⚠ Тестирование завершилось с кодом {exit_code}. Результат сохранён в диалог.[/yellow]")
|
|
600
|
+
|
|
601
|
+
session_snippet = log_content[:6000] if log_content else "Скрипт выполнился без вывода в консоль"
|
|
602
|
+
messages.append({
|
|
603
|
+
"role": "user",
|
|
604
|
+
"content": (
|
|
605
|
+
f"[Системный отчёт: пользователь запустил /testpy в отдельной консоли для проверки последнего кода]\n"
|
|
606
|
+
f"Код завершения: {exit_code}\n"
|
|
607
|
+
f"Лог взаимодействия в консоли:\n```\n{session_snippet}\n```\n"
|
|
608
|
+
f"(Если в логе ошибка или неверный результат — разбери причину и предложи исправление; если всё прошло успешно — подтверди)."
|
|
609
|
+
)
|
|
610
|
+
})
|
|
611
|
+
messages.append({
|
|
612
|
+
"role": "assistant",
|
|
613
|
+
"content": f"Лог тестирования получен (код {exit_code}). Если нужно исправить логику или ошибки ввода — напиши!"
|
|
614
|
+
})
|
|
615
|
+
save_current_chat(system_prompt, messages)
|
|
616
|
+
last_response = messages[-1]["content"]
|
|
617
|
+
continue
|
|
618
|
+
|
|
619
|
+
if command == "/save":
|
|
620
|
+
try:
|
|
621
|
+
path = save_chat(messages, argument)
|
|
622
|
+
console.print(f"[green]Диалог сохранён:[/green] {path}")
|
|
623
|
+
except Exception as error:
|
|
624
|
+
console.print(f"[red]Не удалось сохранить: {error}[/red]")
|
|
625
|
+
continue
|
|
626
|
+
|
|
627
|
+
if command == "/undo":
|
|
628
|
+
removed = False
|
|
629
|
+
if messages and messages[-1]["role"] == "assistant":
|
|
630
|
+
messages.pop()
|
|
631
|
+
removed = True
|
|
632
|
+
if messages and messages[-1]["role"] == "user":
|
|
633
|
+
messages.pop()
|
|
634
|
+
removed = True
|
|
635
|
+
last_response = next(
|
|
636
|
+
(
|
|
637
|
+
item["content"]
|
|
638
|
+
for item in reversed(messages)
|
|
639
|
+
if item["role"] == "assistant"
|
|
640
|
+
),
|
|
641
|
+
"",
|
|
642
|
+
)
|
|
643
|
+
console.print(
|
|
644
|
+
"[green]Последний ход удалён.[/green]"
|
|
645
|
+
if removed
|
|
646
|
+
else "[dim]Удалять нечего.[/dim]"
|
|
647
|
+
)
|
|
648
|
+
if removed:
|
|
649
|
+
save_current_chat(system_prompt, messages)
|
|
650
|
+
continue
|
|
651
|
+
|
|
652
|
+
fact_to_remember = extract_remember_request(prompt)
|
|
653
|
+
fact_to_forget = extract_forget_request(prompt)
|
|
654
|
+
if fact_to_remember or fact_to_forget:
|
|
655
|
+
started_ns = time.perf_counter_ns()
|
|
656
|
+
result = (
|
|
657
|
+
remember_fact(fact_to_remember)
|
|
658
|
+
if fact_to_remember
|
|
659
|
+
else forget_facts(fact_to_forget)
|
|
660
|
+
)
|
|
661
|
+
console.print()
|
|
662
|
+
render_message("Память ›", role="assistant_label")
|
|
663
|
+
render_message(result, role="assistant")
|
|
664
|
+
messages.append({"role": "user", "content": prompt})
|
|
665
|
+
messages.append({"role": "assistant", "content": result})
|
|
666
|
+
save_current_chat(system_prompt, messages)
|
|
667
|
+
last_response = result
|
|
668
|
+
last_stats = direct_tool_stats("memory", started_ns)
|
|
669
|
+
add_to_totals(totals, last_stats)
|
|
670
|
+
if stats_enabled:
|
|
671
|
+
print_stats(last_stats, totals)
|
|
672
|
+
continue
|
|
673
|
+
|
|
674
|
+
if looks_like_memory_question(prompt):
|
|
675
|
+
memory_answer = recall_facts(prompt)
|
|
676
|
+
if "нет подходящего факта" not in memory_answer and not memory_answer.endswith("пуста."):
|
|
677
|
+
started_ns = time.perf_counter_ns()
|
|
678
|
+
console.print()
|
|
679
|
+
render_message("Память ›", role="assistant_label")
|
|
680
|
+
render_message(memory_answer, role="assistant")
|
|
681
|
+
messages.append({"role": "user", "content": prompt})
|
|
682
|
+
messages.append({"role": "assistant", "content": memory_answer})
|
|
683
|
+
save_current_chat(system_prompt, messages)
|
|
684
|
+
last_response = memory_answer
|
|
685
|
+
last_stats = direct_tool_stats("memory", started_ns)
|
|
686
|
+
add_to_totals(totals, last_stats)
|
|
687
|
+
if stats_enabled:
|
|
688
|
+
print_stats(last_stats, totals)
|
|
689
|
+
continue
|
|
690
|
+
|
|
691
|
+
if command != "/retry" and should_auto_escalate(prompt):
|
|
692
|
+
started_ns = time.perf_counter_ns()
|
|
693
|
+
try:
|
|
694
|
+
result = run_agy_for_user(prompt, messages)
|
|
695
|
+
except KeyboardInterrupt:
|
|
696
|
+
console.print("\n[yellow]Antigravity остановлена пользователем.[/yellow]")
|
|
697
|
+
continue
|
|
698
|
+
messages.append({"role": "user", "content": prompt})
|
|
699
|
+
messages.append({"role": "assistant", "content": result})
|
|
700
|
+
save_current_chat(system_prompt, messages)
|
|
701
|
+
last_response = result
|
|
702
|
+
last_stats = direct_tool_stats("Antigravity", started_ns)
|
|
703
|
+
add_to_totals(totals, last_stats)
|
|
704
|
+
if stats_enabled:
|
|
705
|
+
print_stats(last_stats, totals)
|
|
706
|
+
continue
|
|
707
|
+
|
|
708
|
+
retrying = command == "/retry"
|
|
709
|
+
if retrying:
|
|
710
|
+
if messages and messages[-1]["role"] == "assistant":
|
|
711
|
+
messages.pop()
|
|
712
|
+
if not messages or messages[-1]["role"] != "user":
|
|
713
|
+
console.print("[dim]Нет вопроса для повторного ответа.[/dim]")
|
|
714
|
+
continue
|
|
715
|
+
else:
|
|
716
|
+
messages.append({"role": "user", "content": prompt})
|
|
717
|
+
|
|
718
|
+
try:
|
|
719
|
+
wait_for_model_ready(model)
|
|
720
|
+
response, response_stats, interrupted = generate_answer(
|
|
721
|
+
model,
|
|
722
|
+
messages,
|
|
723
|
+
think_mode,
|
|
724
|
+
)
|
|
725
|
+
except Exception as error:
|
|
726
|
+
console.print(
|
|
727
|
+
f"\n[bold red]Ошибка Ollama:[/bold red] "
|
|
728
|
+
f"{type(error).__name__}: {error}"
|
|
729
|
+
)
|
|
730
|
+
continue
|
|
731
|
+
|
|
732
|
+
if interrupted:
|
|
733
|
+
if messages and messages[-1].get("role") == "user":
|
|
734
|
+
messages.pop()
|
|
735
|
+
save_current_chat(system_prompt, messages)
|
|
736
|
+
console.print("[yellow]Генерация остановлена. Запрос удалён из памяти модели (будто не писали).[/yellow]")
|
|
737
|
+
console.print("[dim]💡 Нажми [cyan]Ctrl+S[/cyan], чтобы вернуть отменённый запрос в строку ввода.[/dim]")
|
|
738
|
+
continue
|
|
739
|
+
|
|
740
|
+
if not response:
|
|
741
|
+
console.print("[yellow]Модель не вернула текст.[/yellow]")
|
|
742
|
+
continue
|
|
743
|
+
|
|
744
|
+
messages.append({"role": "assistant", "content": response})
|
|
745
|
+
save_current_chat(system_prompt, messages)
|
|
746
|
+
last_response = response
|
|
747
|
+
last_stats = response_stats
|
|
748
|
+
add_to_totals(totals, response_stats)
|
|
749
|
+
|
|
750
|
+
if stats_enabled:
|
|
751
|
+
print_stats(last_stats, totals)
|
|
752
|
+
|
|
753
|
+
if "```" in response and any(kw in response for kw in ("```python", "```py", "def ", "import ")):
|
|
754
|
+
console.print("[dim]💡 Протестировать код в отдельном окне: [cyan]/testpy[/cyan][/dim]")
|