diff --git a/docs/development_roadmap.md b/docs/development_roadmap.md new file mode 100644 index 0000000..254704d --- /dev/null +++ b/docs/development_roadmap.md @@ -0,0 +1,22 @@ +# 🗺️ ДОРОЖНАЯ КАРТА И АРХИТЕКТУРНЫЙ РОАДМАП ПРОЕКТА SCUD ORION AI + +## 📌 Зафиксированные вехи и концептуальные архитектурные решения + +### 1. Архитектура скользящего контекста и отслеживания смены темы (Intent / Topic Drift Tracking) +* **Проблема:** При работе с длинными операциями (редактирование промптов, мастера табелей, снапшотов) оператор может отвлекаться на сторонние вопросы или уточнения. Жесткий сброс сессии уничтожает черновики, а вечное хранение засоряет память LLM. +* **Решение (Сферическое / Фундаментальное):** + 1. **Семантическая оценка моделью:** Модель через системный контекст определяет, относится ли вопрос оператора к активному черновику/инструменту или разговор ушел в сторону (`topic_shift = true`). + 2. **Детерминированный счетчик в бэкенде:** Бэкенд фиксирует шаги отвлечения (`idle_turns`). Пока $N < 3$, оператор может свободно общаться, не теряя висящий контекст. + 3. **Вежливый перехват и выбор (Guard):** На 3-м шаге отвлечения система отвечает на текущий вопрос оператора и мягко напоминает о незавершенной транзакции кнопками `[Применить / Сохранить]` или `[Отменить и сбросить]`. + +--- + +## 🚀 Будущие модули и запланированный функционал (Backlog) + +### 2. Интерактивный модуль планирования и дорожных карт (Checklist & Task Planner Engine) +* **Концепция:** Специализированный инструмент для LLM-агента, позволяющий вести динамические проектные чек-листы и планы с интерактивными чекбоксами прямо в диалоге и базе данных. +* **Ключевые возможности:** + * Составление многоуровневых планов с чекбоксами (`[ ]` / `[x]`). + * Фиксация промежуточных комментариев и статусов выполнения по каждому пункту. + * Синхронизация задач плана с SQLite-таблицей `tasks`. + * Экспорт планов в Markdown/Excel и отображение на дашборде. diff --git a/modules/web_api/llm/agent.py b/modules/web_api/llm/agent.py index 0fd7df5..252c943 100644 --- a/modules/web_api/llm/agent.py +++ b/modules/web_api/llm/agent.py @@ -5,27 +5,8 @@ PROJECT: SCUD Orion AI (Unified Architecture) MODULE: web_api / llm (Core Agent & Function Calling Dispatcher) ROLE: Главный оркестратор взаимодействия с Ollama LLM (Qwen 2.5), разбор вызовов инструментов (Function Calling), интеграция с декларативным реестром - действий SQLite (tool_action_registry) и управление сессионными кнопками. - -AI-CONTEXT-ANCHORS & INVARIANTS: - - ANCHOR[LOGGING_CONFIG]: Настройка StreamHandler в sys.stdout с propagate=False - для предотвращения дублирования строк в systemd journalctl. - - ANCHOR[DYNAMIC_CONTEXT]: Сборка системного контекста (календарь, сессия, промпт). - - ANCHOR[INFERENCE_OPTIONS]: Параметры инференса (repeat_penalty, ctx_size) для - предотвращения урезания длинных списков моделью Qwen 2.5. - - ANCHOR[TOOL_ROUTER]: Диспетчеризация функций SQLite (CRUD задач, снапшотов, KB). - - ANCHOR[PROMPT_MERGE_LOGIC]: Универсальный парсер точечного добавления и - удаления пунктов системного промпта в режиме предпросмотра (PROMPT_PREVIEW). - - ANCHOR[DATA_DRIVEN_ACTIONS]: Универсальная обработка инструментов на базе - таблицы tool_action_registry (мгновенный возврат ответов и динамических кнопок). - - ANCHOR[SECONDARY_PASS]: Вторичный вызов LLM для формирования текстового ответа - на основе полученного tool_result. - -DEPENDENCIES: - - modules/web_api/llm/db_tools.py (доступ к SQLite и реестру действий) - - modules/web_api/llm/schemas.py (TOOLS_SCHEMA) - - modules/web_api/llm/core/calendar_utils.py (get_dynamic_calendar_context) - - modules/web_api/llm/core/tool_injector.py (clean_raw_tool_tags, clean_output) + действий SQLite (tool_action_registry), детерминированный Fast-Path + для подтверждений, динамические кнопки и автоочистка эфемерных сообщений. =============================================================================== """ @@ -57,6 +38,7 @@ from .db_tools import ( db_get_current_server_time, db_save_chat_message, db_get_chat_history, + db_purge_ephemeral_messages, db_get_stats, db_get_anomalies, db_get_session_states, @@ -70,7 +52,7 @@ from .core.tool_injector import clean_raw_tool_tags, clean_output, inject_tools_ # --- [SECTION 2: LOGGING CONFIGURATION] --- # ANCHOR[LOGGING_CONFIG] logger = logging.getLogger("SCUD_AGENT") logger.setLevel(logging.INFO) -logger.propagate = False # Исключаем дублирование сообщений в родительском логгере Uvicorn +logger.propagate = False if not logger.handlers: handler = logging.StreamHandler(sys.stdout) @@ -94,10 +76,10 @@ def process_chat_message( ) -> Tuple[str, List[Dict[str, Any]], Optional[Dict[str, Any]]]: """ Главный конвейер обработки входящего сообщения: - 1. Сохранение сообщения пользователя. - 2. Формирование системного контекста и вызов Ollama. - 3. Выполнение вызванного Tool (если сгенерирован). - 4. Опрос Data-Driven реестра действий (tool_action_registry) или вторичный проход LLM. + 1. Fast-Path перехват подтверждений/отмен при активном session_state. + 2. Перехват завершения работы ('нет, закончить настройку') с автоочисткой эфемерных сообщений. + 3. Формирование системного контекста и вызов Ollama. + 4. Выполнение вызванного Tool и опрос Data-Driven реестра действий. 5. Возврат кортежа: (reply_text, chat_history, action_metadata). """ logger.info(f"Получено сообщение от user_id={user_id}, session_id={session_id}: {user_message}") @@ -107,34 +89,82 @@ def process_chat_message( if file_context: full_user_content = f"{user_message}\n\n[СОДЕРЖИМОЕ ПРИКРЕПЛЕННОГО ФАЙЛА]:\n{file_context}" + session_state = db_get_session_state(session_id) + user_msg_clean = user_message.lower().strip(" .!?:;") + + # --- [FAST-PATH 1: ПЕРЕХВАТ ЗАВЕРШЕНИЯ НАСТРОЙКИ С ОЧИСТКОЙ ЭФЕМЕРНОЙ ПАМЯТИ] --- + if user_msg_clean in ["нет, спасибо", "нет, закончить настройку", "закончить настройку", "завершить", "нет"]: + db_clear_session_state(session_id) + deleted_count = db_purge_ephemeral_messages(session_id) + logger.info(f"🧹 Завершена работа с инструментом. Удалено эфемерных сообщений: {deleted_count}") + + reply_text = "Хорошо. Настройка завершена, контекст диалога чист. Чем я могу помочь дальше?" + db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=0) + return reply_text, db_get_chat_history(session_id), None + + # --- [FAST-PATH 2: ПЕРЕХВАТ ПОДТВЕРЖДЕНИЯ / ОТМЕНЫ ПРЕВЬЮ ПРОМПТА] --- + if session_state and session_state.get("state_type") == "PROMPT_PREVIEW": + if user_msg_clean in ["подтверждаю", "подтвердить", "да", "сохранить", "применить", "ок", "хорошо"]: + db_add_system_prompt("main_agent", session_state.get("pending_data", "")) + db_clear_session_state(session_id) + + action_cfg = db_get_tool_action("db_confirm_prompt_preview") + reply_text = action_cfg["success_template"] if action_cfg else "✅ Системный промпт успешно сохранен и применен в базе данных." + if action_cfg and action_cfg.get("follow_up_question"): + reply_text += f"\n\n{action_cfg['follow_up_question']}" + + db_save_chat_message(session_id, "user", full_user_content, is_ephemeral=1) + db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=1) + return reply_text, db_get_chat_history(session_id), { + "type": action_cfg.get("action_type") if action_cfg else "FOLLOW_UP_ACTION", + "buttons": action_cfg.get("buttons", []) if action_cfg else [] + } + + elif user_msg_clean in ["отмена", "отменить", "отклонить", "назад", "стоп"]: + db_clear_session_state(session_id) + + action_cfg = db_get_tool_action("db_cancel_prompt_preview") + reply_text = action_cfg["success_template"] if action_cfg else "❌ Изменения системного промпта отменены." + if action_cfg and action_cfg.get("follow_up_question"): + reply_text += f"\n\n{action_cfg['follow_up_question']}" + + db_save_chat_message(session_id, "user", full_user_content, is_ephemeral=1) + db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=1) + return reply_text, db_get_chat_history(session_id), { + "type": action_cfg.get("action_type") if action_cfg else "FOLLOW_UP_ACTION", + "buttons": action_cfg.get("buttons", []) if action_cfg else [] + } + + # 3.2. Сохраняем входящее сообщение в историю диалога + is_user_ephemeral = 1 if session_state else 0 + db_save_chat_message(session_id, "user", full_user_content, is_ephemeral=is_user_ephemeral) + + # 3.3. Извлекаем полную актуальную историю для передачи в LLM db_history = db_get_chat_history(session_id, limit=20) - db_save_chat_message(session_id, "user", full_user_content) - # 3.2. Сборка системного контекста и правил # ANCHOR[DYNAMIC_CONTEXT] dynamic_prompt_text = db_get_active_system_prompt() calendar_context = get_dynamic_calendar_context() - session_state = db_get_session_state(session_id) - - preview_status_note = "" - if session_state and session_state.get("state_type") == "PROMPT_PREVIEW": - preview_status_note = "\n\n[АКТИВНО ПРЕВЬЮ ПРОМПТА: Ожидается подтверждение ('подтверждаю') или отмена ('отмена') пользователем]." - user_info = f"Пользователь ID={user_id}" if user_id != 0 else "Гость" system_prompt_content = ( - f"[ТЕКУЩИЙ АВТОРИЗОВАННЫЙ ПОЛЬЗОВАТЕЛЬ]\n" - f"Вы общаетесь с пользователем: {user_info}.\n\n" - f"[СИСТЕМНЫЙ КАЛЕНДАРЬ СЕРВЕРА]\n" - f"{calendar_context}\n\n" - f"[ПРАВИЛА И СТРОГИЕ ТРИГГЕРЫ ВЫЗОВА ИНСТРУМЕНТОВ]\n" - f"1. ТРИГГЕРЫ ПРОСМОТРА: Если запрос содержит фразы 'покажи системный промпт', 'покажи промпт', 'выведи промпт' — ТЫ ОБЯЗАН СГЕНЕРИРОВАТЬ ToolCall: db_get_system_prompt(). Категорически ЗАПРЕЩЕНО выводить текст промпта из памяти без вызова этой функции!\n" - f"2. ТРИГГЕРЫ ПРАВКИ: Если запрос содержит слова 'добавь пункт', 'удали пункт', 'измени промпт' — ТЫ ОБЯЗАН СГЕНЕРИРОВАТЬ ToolCall: db_preview_prompt_merge(prompt_text=...).\n" - f"3. ТРИГГЕРЫ ЗАДАЧ: При фразах 'покажи задачи', 'мои задачи', 'список дел' — СРАЗУ генерируй ToolCall: db_get_tasks().\n" - f"4. ЗАПРЕТ ТЕКСТА: Запрещено объяснять правила или писать названия функций текстом, если сработал триггер — просто вызывай функцию!\n\n" - f"ТЕКУЩИЙ АКТИВНЫЙ СИСТЕМНЫЙ ПРОМПТ:\n{dynamic_prompt_text}{preview_status_note}" + f"Ты — интеллектуальный ассистент SCUD Orion AI. " + f"Ты управляешь задачами, аналитикой СКУД и системными настройками с помощью инструментов (tools).\n\n" + f"[ОКРУЖЕНИЕ]\n" + f"- Пользователь: {user_info}\n" + f"- {calendar_context}\n\n" + f"[ПРАВИЛА ИСПОЛЬЗОВАНИЯ ИНСТРУМЕНТОВ]\n" + f"1. Для любых изменений системного промпта (добавить, удалить, изменить пункт) ВСЕГДА вызывай функцию db_preview_prompt_merge(prompt_text=...).\n" + f"2. Для просмотра системного промпта ВСЕГДА вызывай db_get_system_prompt().\n" + f"3. Для просмотра задач ВСЕГДА вызывай db_get_tasks().\n" + f"4. Никогда не симулируй выполнение функций в виде обычного текста. Если требуется действие — сразу вызывай соответствующий инструмент.\n\n" + f"[ПРИМЕРЫ ВЫЗОВА ИНСТРУМЕНТОВ]:\n" + f"- Пользователь: 'добавь пункт 3.4. Работать от сюда и до заката.' -> Вызов: db_preview_prompt_merge(prompt_text='3.4. Работать от сюда и до заката.')\n" + f"- Пользователь: 'удали пункт 3.4' -> Вызов: db_preview_prompt_merge(prompt_text='3.4')\n" + f"- Пользователь: 'покажи системный промпт' -> Вызов: db_get_system_prompt()\n" + f"- Пользователь: 'покажи мои задачи' -> Вызов: db_get_tasks()\n\n" + f"[ТЕКУЩИЙ АКТИВНЫЙ СИСТЕМНЫЙ ПРОМПТ]:\n{dynamic_prompt_text}" ) - # 3.3. Параметры инференса # ANCHOR[INFERENCE_OPTIONS] llm_options = { "num_predict": 8192, "num_ctx": 8192, @@ -174,8 +204,7 @@ def process_chat_message( tool_calls = msg.get("tool_calls", []) raw_text_content = msg.get("content", "") - # Фоллбэк проверка через tool_injector - tool_calls = inject_tools_if_needed(user_message, raw_text_content, tool_calls) + #tool_calls = inject_tools_if_needed(user_message, raw_text_content, tool_calls) if tool_calls: logger.info(f"Ответ от Ollama получен. Tool calls: True (кол-во: {len(tool_calls)})") @@ -187,7 +216,6 @@ def process_chat_message( logger.info(f"🚀 Вызов функции (Tool): {fn_name} с аргументами: {fn_args}") tool_result_content = "" - # --- 5.1. УНИВЕРСАЛЬНАЯ ПРОВЕРКА DATA-DRIVEN РЕЕСТРА (tool_action_registry) --- # ANCHOR[DATA_DRIVEN_ACTIONS] action_cfg = db_get_tool_action(fn_name) if fn_name == "db_confirm_prompt_preview": @@ -200,7 +228,8 @@ def process_chat_message( if action_cfg.get("follow_up_question"): reply_text += f"\n\n{action_cfg['follow_up_question']}" - db_save_chat_message(session_id, "assistant", reply_text) + is_eph = action_cfg.get("is_ephemeral", 1) + db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=is_eph) return reply_text, db_get_chat_history(session_id), { "type": action_cfg.get("action_type"), "buttons": action_cfg.get("buttons", []) @@ -208,7 +237,7 @@ def process_chat_message( tool_result_content = json.dumps({"status": "success"}, ensure_ascii=False) else: err_reply = "⚠️ Нет активного превью для подтверждения." - db_save_chat_message(session_id, "assistant", err_reply) + db_save_chat_message(session_id, "assistant", err_reply, is_ephemeral=1) return err_reply, db_get_chat_history(session_id), None elif fn_name == "db_cancel_prompt_preview": @@ -218,14 +247,14 @@ def process_chat_message( if action_cfg.get("follow_up_question"): reply_text += f"\n\n{action_cfg['follow_up_question']}" - db_save_chat_message(session_id, "assistant", reply_text) + is_eph = action_cfg.get("is_ephemeral", 1) + db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=is_eph) return reply_text, db_get_chat_history(session_id), { "type": action_cfg.get("action_type"), "buttons": action_cfg.get("buttons", []) } tool_result_content = json.dumps({"status": "cancelled"}, ensure_ascii=False) - # --- 5.2. СТАНДАРТНЫЙ РОУТИНГ ИНСТРУМЕНТОВ --- elif fn_name == "db_get_snapshots": snapshots_res = db_get_snapshots(session_id=session_id, date_str=fn_args.get("date_str"), original_user_message=user_message) tool_result_content = json.dumps(snapshots_res, ensure_ascii=False) @@ -254,7 +283,8 @@ def process_chat_message( reply_text = action_cfg["success_template"] if action_cfg.get("follow_up_question"): reply_text += f"\n\n{action_cfg['follow_up_question']}" - db_save_chat_message(session_id, "assistant", reply_text) + is_eph = action_cfg.get("is_ephemeral", 1) + db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=is_eph) return reply_text, db_get_chat_history(session_id), { "type": action_cfg.get("action_type"), "buttons": action_cfg.get("buttons", []) @@ -312,7 +342,7 @@ def process_chat_message( f"{proposed_text}\n\n" f"Для применения изменений подтвердите действие («подтверждаю») или отмените («отмена»)." ) - db_save_chat_message(session_id, "assistant", preview_reply) + db_save_chat_message(session_id, "assistant", preview_reply, is_ephemeral=1) return clean_raw_tool_tags(preview_reply), db_get_chat_history(session_id), { "type": "PROMPT_PREVIEW", "buttons": [ @@ -338,7 +368,8 @@ def process_chat_message( reply_text = action_cfg["success_template"] if action_cfg.get("follow_up_question"): reply_text += f"\n\n{action_cfg['follow_up_question']}" - db_save_chat_message(session_id, "assistant", reply_text) + is_eph = action_cfg.get("is_ephemeral", 1) + db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=is_eph) return reply_text, db_get_chat_history(session_id), { "type": action_cfg.get("action_type"), "buttons": action_cfg.get("buttons", []) @@ -354,14 +385,14 @@ def process_chat_message( sec_res_data = json.loads(sec_response.read().decode("utf-8")) raw_content = sec_res_data.get("message", {}).get("content", "").strip().replace("**", "") final_content = clean_raw_tool_tags(clean_output(raw_content)) - db_save_chat_message(session_id, "assistant", final_content) + db_save_chat_message(session_id, "assistant", final_content, is_ephemeral=0) return final_content, db_get_chat_history(session_id), None # Если вызовов функций не было raw_str = msg.get("content", "").strip().replace("**", "") content_str = clean_raw_tool_tags(clean_output(raw_str)) final_reply = content_str or "Запрос обработан." - db_save_chat_message(session_id, "assistant", final_reply) + db_save_chat_message(session_id, "assistant", final_reply, is_ephemeral=0) return final_reply, db_get_chat_history(session_id), None except Exception as ex: diff --git a/modules/web_api/llm/db/db_chat.py b/modules/web_api/llm/db/db_chat.py index 0a149aa..6e20e88 100644 --- a/modules/web_api/llm/db/db_chat.py +++ b/modules/web_api/llm/db/db_chat.py @@ -1,29 +1,56 @@ """ FILE: modules/web_api/llm/db/db_chat.py +ROLE: Управление историей сообщений и очисткой эфемерного контекста. """ from typing import List, Dict, Any from .connection import get_db_connection -def db_save_chat_message(session_id: str, role: str, content: str): - if not content: - return +def db_save_chat_message(session_id: str, role: str, content: str, is_ephemeral: int = 0) -> None: + """Сохраняет сообщение в БД (is_ephemeral=1 для временных служебных шагов, 0 для постоянных).""" conn = get_db_connection() cursor = conn.cursor() cursor.execute(""" - INSERT INTO chat_messages (session_id, role, content, created_at) - VALUES (?, ?, ?, CURRENT_TIMESTAMP) - """, (session_id, role, content)) + INSERT INTO chat_messages (session_id, role, content, is_ephemeral) + VALUES (?, ?, ?, ?) + """, (session_id, role, content, is_ephemeral)) conn.commit() conn.close() + def db_get_chat_history(session_id: str, limit: int = 20) -> List[Dict[str, Any]]: + """Получает последние сообщения истории диалога в хронологическом порядке.""" conn = get_db_connection() cursor = conn.cursor() cursor.execute(""" - SELECT role, content FROM chat_messages + SELECT role, content + FROM chat_messages WHERE session_id = ? - ORDER BY id DESC LIMIT ? + ORDER BY id DESC + LIMIT ? """, (session_id, limit)) rows = cursor.fetchall() conn.close() - return [{"role": r["role"], "content": r["content"]} for r in reversed(rows)] \ No newline at end of file + return [{"role": r["role"], "content": r["content"]} for r in reversed(rows)] + + +def db_purge_ephemeral_messages(session_id: str) -> int: + """ + Физически удаляет все временные служебные сообщения выбранной сессии + после завершения сценария работы с инструментом. + """ + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM chat_messages WHERE session_id = ? AND is_ephemeral = 1", (session_id,)) + deleted = cursor.rowcount + conn.commit() + conn.close() + return deleted + + +def db_clear_chat_history(session_id: str) -> None: + """Полная очистка всех сообщений сессии.""" + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM chat_messages WHERE session_id = ?", (session_id,)) + conn.commit() + conn.close() \ No newline at end of file diff --git a/modules/web_api/llm/db/db_prompts.py b/modules/web_api/llm/db/db_prompts.py index 9978bc4..e4de939 100644 --- a/modules/web_api/llm/db/db_prompts.py +++ b/modules/web_api/llm/db/db_prompts.py @@ -17,12 +17,12 @@ def db_get_active_system_prompt() -> str: return row["prompt_text"] if row else "Ты — ИИ-ассистент SCUD Orion AI." def db_get_tool_action(tool_name: str) -> Optional[Dict[str, Any]]: - """Получение шаблона и кнопок действия для инструмента из SQLite.""" + """Получение шаблона, кнопок и настроек эфемерности инструмента из SQLite.""" conn = get_db_connection() cursor = conn.cursor() cursor.execute(""" SELECT tool_name, category, bypass_llm, success_template, - follow_up_question, action_type, buttons_json + follow_up_question, action_type, buttons_json, is_ephemeral FROM tool_action_registry WHERE tool_name = ? AND is_active = 1 """, (tool_name,)) diff --git a/modules/web_api/llm/db_tools.py b/modules/web_api/llm/db_tools.py index df4f588..375a726 100644 --- a/modules/web_api/llm/db_tools.py +++ b/modules/web_api/llm/db_tools.py @@ -3,7 +3,7 @@ FILE: modules/web_api/llm/db_tools.py """ from datetime import datetime from .db.connection import DB_PATH, get_db_connection -from .db.db_chat import db_save_chat_message, db_get_chat_history +from .db.db_chat import db_save_chat_message, db_get_chat_history, db_purge_ephemeral_messages from .db.db_tasks import normalize_task_id, db_get_tasks, db_add_task, db_update_task_status, db_delete_task from .db.db_snapshots import db_get_snapshots, db_delete_snapshots from .db.db_prompts import ( diff --git a/modules/web_api/llm/schemas.py b/modules/web_api/llm/schemas.py index 9e8d3ee..b30a8dc 100644 --- a/modules/web_api/llm/schemas.py +++ b/modules/web_api/llm/schemas.py @@ -189,13 +189,13 @@ TOOLS_SCHEMA = [ "type": "function", "function": { "name": "db_preview_prompt_merge", - "description": "ВЫЗЫВАЙ ПРИ ЛЮБЫХ ИЗМЕНЕНИЯХ ПРОМПТА: добавление пункта ('добавь пункт...'), удаление пункта ('удали пункт 3.4', 'убери 3.4' или других номеров) или редактирование текста промпта. Передавай текст действия или номер удаляемого пункта в prompt_text.", + "description": "Сформировать предпросмотр изменения системного промпта. Вызывается при любых запросах на добавление ('добавь пункт...'), изменение или удаление пунктов системного промпта ('удали пункт...').", "parameters": { "type": "object", "properties": { "prompt_text": { "type": "string", - "description": "Текст нового пункта или команда/номер удаляемого пункта (например '3.4' или 'удали пункт 3.4')" + "description": "Полный текст добавляемого пункта или номер/описание удаляемого пункта." } }, "required": ["prompt_text"] diff --git a/scripts/db_cli.py b/scripts/db_cli.py index 2049738..781afd2 100644 --- a/scripts/db_cli.py +++ b/scripts/db_cli.py @@ -280,7 +280,6 @@ def print_session_states(): print("=" * 80 + "\n") -Python HELP_TEXT = """ CLI-утилита инспекции и управления SQLite базой данных СКУД (scud_orion_ai.db)