From 286355e4c0e87aca3d9442a2a35b0dd91b38b6e9 Mon Sep 17 00:00:00 2001 From: manoraga Date: Sun, 9 Aug 2026 22:06:04 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BD=D0=B0=D0=BB=D0=B0=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D1=81=20=D1=81?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D0=BD=D1=8B=D0=BC=D0=B8=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=BC=D0=BF=D1=82=D0=B0=D0=BC=D0=B8=20(=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5,=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5,=20?= =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5).=20=D0=92?= =?UTF-8?q?=D0=BE=D1=81=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D1=81=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B0=D0=BC=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- init_db.py | 16 ++++++-- llm/agent.py | 97 ++++++++++++++++++++++++++++++++++------------- llm/db_tools.py | 94 ++++++++++++++++++++++++++++++++++----------- llm/schemas.py | 48 +++++++++++++++-------- system_prompt.txt | 70 ++++++++++++++++++++++++++++++++++ 5 files changed, 258 insertions(+), 67 deletions(-) create mode 100644 system_prompt.txt diff --git a/init_db.py b/init_db.py index c76c0af..08daed3 100644 --- a/init_db.py +++ b/init_db.py @@ -1,7 +1,7 @@ import os import sqlite3 -DB_NAME = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db" +DB_NAME = "/home/puh/scud_context_api/scud_orion_ai.db" def init_db(): os.makedirs(os.path.dirname(DB_NAME), exist_ok=True) @@ -39,9 +39,19 @@ def init_db(): ); """) + # Добавление новой таблицы для сессионных состояний + cursor.execute(""" + CREATE TABLE IF NOT EXISTS session_states ( + session_id TEXT PRIMARY KEY, + state_type TEXT NOT NULL, + pending_data TEXT NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + """) + conn.commit() conn.close() - print(f"[✓] Единая база данных SQLite ({DB_NAME}) успешно проверкой инициализирована!") + print(f"[✓] Единая база данных SQLite ({DB_NAME}) успешно инициализирована!") if __name__ == "__main__": - init_db() + init_db() \ No newline at end of file diff --git a/llm/agent.py b/llm/agent.py index fc70d3a..91b2c68 100644 --- a/llm/agent.py +++ b/llm/agent.py @@ -1,6 +1,7 @@ import json import urllib.request import urllib.error +import logging from typing import List, Dict, Any, Tuple from datetime import datetime @@ -11,33 +12,48 @@ from .db_tools import ( db_update_task_status, db_delete_task, db_add_task, - db_get_rules + db_get_rules, + db_set_session_state, + db_get_session_state, + db_clear_session_state, + get_db_connection ) + from .schemas import TOOLS_SCHEMA +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger("SCUD_AGENT") + OLLAMA_URL = "http://192.168.11.3:11434/api/chat" MODEL_NAME = "qwen2.5:14b" -def format_rules_output(rules: List[Dict[str, Any]]) -> str: - if not rules: - return "База знаний пока пуста." - lines = [f"📚 База знаний и правила арбитража ({len(rules)}):\n"] - for idx, r in enumerate(rules, 1): - rule_text = r.get("rule_text", "").strip() - lines.append(f"{idx}. {rule_text}\n") - return "\n".join(lines).strip() +def clean_output(text: str) -> str: + if not text: + return text + artifacts = ["почемучто", "почто", "почему что"] + lower_text = text.lower() + for art in artifacts: + if lower_text.startswith(art): + text = text[len(art):].lstrip(",.!?:; -") + return text.strip() -def process_chat_message(user_id: int, user_message: str, chat_history: List[Dict[str, Any]] = None) -> Tuple[str, List[Dict[str, Any]]]: +def process_chat_message(user_id: int, user_message: str, chat_history: List[Dict[str, Any]] = None, session_id: str = "web_session_main") -> Tuple[str, List[Dict[str, Any]]]: if chat_history is None: chat_history = [] - current_now = datetime.now().strftime("%Y-%m-%d %H:%M") + logger.info(f"Получено сообщение от user_id={user_id}, session_id={session_id}: {user_message}") - # Запрос к локальной модели Qwen (полный цикл Function Calling) + current_now = datetime.now().strftime("%Y-%m-%d %H:%M") dynamic_prompt_text = db_get_active_system_prompt() + + session_state = db_get_session_state(session_id) + preview_status_note = "" + if session_state and session_state["state_type"] == "PROMPT_PREVIEW": + preview_status_note = "\n\n[АКТИВНО ПРЕВЬЮ ПРОМПТА: Ожидается подтверждение или отмена изменений пользователем]." + system_prompt = { "role": "system", - "content": f"Текущая дата и время сервера: {current_now}.\n\n{dynamic_prompt_text}" + "content": f"Текущая дата и время сервера: {current_now}.\n\nТЕКУЩИЙ АКТИВНЫЙ СИСТЕМНЫЙ ПРОМПТ:\n{dynamic_prompt_text}{preview_status_note}" } messages = [system_prompt] + chat_history + [{"role": "user", "content": user_message}] @@ -47,7 +63,7 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic "messages": messages, "tools": TOOLS_SCHEMA, "stream": False, - "options": {"num_predict": 2048, "num_ctx": 8192, "temperature": 0.1} + "options": {"num_predict": 8192, "num_ctx": 8192, "temperature": 0.1} } try: @@ -60,6 +76,7 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic res_data = json.loads(response.read().decode("utf-8")) msg = res_data.get("message", {}) tool_calls = msg.get("tool_calls", []) + logger.info(f"Ответ от Ollama получен. Tool calls: {bool(tool_calls)}") if tool_calls: messages.append(msg) @@ -67,6 +84,7 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic for tool in tool_calls: fn_name = tool["function"]["name"] fn_args = tool["function"].get("arguments", {}) + logger.info(f"Вызов функции (Tool): {fn_name} с аргументами: {fn_args}") tool_result_content = "" if fn_name == "db_get_tasks": @@ -74,14 +92,41 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic tool_result_content = json.dumps(tasks, ensure_ascii=False) elif fn_name in ["db_get_system_prompt", "db_get_system_prompts"]: - tool_result_content = db_get_active_system_prompt() + prompt_content = db_get_active_system_prompt() + tool_result_content = json.dumps({"system_prompt": prompt_content}, ensure_ascii=False) + + elif fn_name == "db_preview_prompt_merge": + proposed_text = fn_args.get("proposed_prompt", "") + if proposed_text: + # Фиксируем превью в сессии и сразу возвращаем текст пользователю на экран + db_set_session_state(session_id, "PROMPT_PREVIEW", proposed_text) + preview_reply = f"Ваше изменение успешно предпросмотрено. Полный обновленный системный промпт теперь выглядит так:\n\n{proposed_text}\n\nДля применения изменений подтвердите действие («подтверждаю») или отмените («отмена»)." + return preview_reply, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": preview_reply}] + else: + tool_result_content = json.dumps({"status": "error", "message": "Текст превью пуст."}, ensure_ascii=False) + + elif fn_name == "db_confirm_prompt_preview": + if session_state and session_state["state_type"] == "PROMPT_PREVIEW": + pending_text = session_state["pending_data"] + res = db_add_system_prompt("main_agent", pending_text) + db_clear_session_state(session_id) + tool_result_content = json.dumps(res, ensure_ascii=False) + else: + tool_result_content = json.dumps({"status": "error", "message": "Нет активного превью для подтверждения."}, ensure_ascii=False) + + elif fn_name == "db_cancel_prompt_preview": + db_clear_session_state(session_id) + tool_result_content = json.dumps({"status": "success", "message": "Превью отменено."}, ensure_ascii=False) elif fn_name == "db_add_system_prompt": - res = db_add_system_prompt( - name=fn_args.get("name", "main_agent"), - prompt_text=fn_args.get("prompt_text") - ) - tool_result_content = json.dumps(res, ensure_ascii=False) + try: + prompt_text = fn_args.get("prompt_text") if isinstance(fn_args, dict) else str(fn_args) + name = fn_args.get("name", "main_agent") if isinstance(fn_args, dict) else "main_agent" + res = db_add_system_prompt(name=name, prompt_text=prompt_text) + db_clear_session_state(session_id) + tool_result_content = json.dumps(res, ensure_ascii=False) + except Exception as e: + tool_result_content = json.dumps({"status": "error", "error": str(e)}, ensure_ascii=False) elif fn_name == "db_get_rules": tool_result_content = json.dumps(db_get_rules(), ensure_ascii=False) @@ -103,12 +148,11 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic "content": tool_result_content }) - # Вторичный запрос модели для формирования итогового ответа оператору second_payload = { "model": MODEL_NAME, "messages": messages, "stream": False, - "options": {"num_predict": 2048, "num_ctx": 8192, "temperature": 0.1} + "options": {"num_predict": 8192, "num_ctx": 8192, "temperature": 0.1} } sec_req = urllib.request.Request( OLLAMA_URL, @@ -117,11 +161,12 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic ) with urllib.request.urlopen(sec_req) as sec_response: sec_res_data = json.loads(sec_response.read().decode("utf-8")) - final_content = sec_res_data.get("message", {}).get("content", "").strip().replace("**", "") + final_content = clean_output(sec_res_data.get("message", {}).get("content", "").strip().replace("**", "")) return final_content, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": final_content}] - content_str = msg.get("content", "").strip().replace("**", "") + content_str = clean_output(msg.get("content", "").strip().replace("**", "")) return content_str or "Запрос обработан.", chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": content_str}] - except urllib.error.URLError as e: - return f"Ошибка связи с Ollama ({OLLAMA_URL}): {e}", chat_history \ No newline at end of file + except Exception as ex: + logger.exception(f"Непредвиденная ошибка: {ex}") + return f"Внутренняя ошибка сервера: {ex}", chat_history \ No newline at end of file diff --git a/llm/db_tools.py b/llm/db_tools.py index 2f7d209..8914dc6 100644 --- a/llm/db_tools.py +++ b/llm/db_tools.py @@ -1,11 +1,20 @@ import sqlite3 +import logging from typing import List, Dict, Any, Optional -DB_PATH = "/home/puh/scud_context_api/scud_orion_ai.db" +# Настройка логирования +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger("DB_TOOLS") + +DB_PATH = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db" def get_db_connection(): - conn = sqlite3.connect(DB_PATH) + # Увеличиваем таймаут до 30 секунд, чтобы соединения ожидали завершения соседних транзакций, + # а также включаем WAL-режим для безопасного параллельного чтения и записи. + conn = sqlite3.connect(DB_PATH, timeout=30.0) conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode = WAL;") + conn.execute("PRAGMA synchronous = NORMAL;") return conn def normalize_task_id(task_id_input: str) -> str: @@ -107,31 +116,70 @@ def db_get_active_system_prompt() -> str: return row["prompt_text"] if row else "Ты — ИИ-ассистент SCUD Orion AI." def db_add_system_prompt(name: str, prompt_text: str) -> Dict[str, Any]: - conn = get_db_connection() - cursor = conn.cursor() - - # Проверяем наличие промпта с таким именем, чтобы не плодить мусор - cursor.execute("SELECT id FROM system_prompts WHERE name = ?", (name,)) - existing = cursor.fetchone() - - if existing: - cursor.execute(""" - UPDATE system_prompts - SET prompt_text = ?, updated_at = CURRENT_TIMESTAMP, is_active = 1 - WHERE name = ? - """, (prompt_text, name)) - else: - cursor.execute("UPDATE system_prompts SET is_active = 0") - cursor.execute("INSERT INTO system_prompts (name, prompt_text, is_active) VALUES (?, ?, 1)", (name, prompt_text)) - - conn.commit() - conn.close() - return {"status": "success", "message": "Системный промпт успешно обновлен"} + try: + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute("BEGIN IMMEDIATE;") + + # Проверяем, существует ли уже промпт с таким именем + cursor.execute("SELECT id FROM system_prompts WHERE name = ?", (name,)) + existing = cursor.fetchone() + + if existing: + # Обновляем существующий активный промпт + cursor.execute( + "UPDATE system_prompts SET prompt_text = ?, is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE name = ?", + (prompt_text, name) + ) + else: + # Вставляем новый, если его не было + cursor.execute( + "INSERT INTO system_prompts (name, prompt_text, is_active) VALUES (?, ?, 1)", + (name, prompt_text) + ) + + conn.commit() + + logger.info("Системный промпт успешно сохранен и применен в базе данных.") + return {"status": "success", "message": "Системный промпт успешно обновлен"} + except Exception as e: + logger.error(f"Ошибка при сохранении промпта в БД: {e}") + return {"status": "error", "error": str(e)} + def db_get_rules() -> List[Dict[str, Any]]: conn = get_db_connection() cursor = conn.cursor() cursor.execute("SELECT id, rule_text FROM ai_knowledge_base ORDER BY id ASC") rows = cursor.fetchall() conn.close() - return [dict(r) for r in rows] \ No newline at end of file + return [dict(r) for r in rows] + +def db_set_session_state(session_id: str, state_type: str, data: str): + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO session_states (session_id, state_type, pending_data, updated_at) + VALUES (?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(session_id) DO UPDATE SET + state_type = excluded.state_type, + pending_data = excluded.pending_data, + updated_at = CURRENT_TIMESTAMP + """, (session_id, state_type, data)) + conn.commit() + conn.close() + +def db_get_session_state(session_id: str) -> Optional[Dict[str, Any]]: + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SELECT state_type, pending_data FROM session_states WHERE session_id = ?", (session_id,)) + row = cursor.fetchone() + conn.close() + return dict(row) if row else None + +def db_clear_session_state(session_id: str): + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM session_states WHERE session_id = ?", (session_id,)) + conn.commit() + conn.close() \ No newline at end of file diff --git a/llm/schemas.py b/llm/schemas.py index b9462bb..3df8037 100644 --- a/llm/schemas.py +++ b/llm/schemas.py @@ -31,21 +31,6 @@ TOOLS_SCHEMA = [ "parameters": {"type": "object", "properties": {}} } }, - { - "type": "function", - "function": { - "name": "db_add_system_prompt", - "description": "Изменить, записать или добавить новый системный промпт ассистента в базу данных. Вызывай этот инструмент ВСЕГДА, когда пользователь просит 'добавь системный промпт', 'запиши промпт', 'измени инструкции ИИ'.", - "parameters": { - "type": "object", - "properties": { - "prompt_text": {"type": "string", "description": "Полный текст нового системного промпта"}, - "name": {"type": "string", "description": "Имя промпта, по умолчанию main_agent"} - }, - "required": ["prompt_text"] - } - } - }, { "type": "function", "function": { @@ -92,5 +77,38 @@ TOOLS_SCHEMA = [ "required": ["title"] } } + }, + { + "type": "function", + "function": { + "name": "db_confirm_prompt_preview", + "description": "Подтвердить и сохранить текущее подготовленное превью в БД. Вызывай этот инструмент, когда пользователь говорит 'подтверждаю', 'да', 'вноси', 'применяй', 'сохраняй' или одобряет превью в любой форме.", + "parameters": {"type": "object", "properties": {}} + } + }, + { + "type": "function", + "function": { + "name": "db_cancel_prompt_preview", + "description": "Отменить текущее превью системного промпта и сбросить изменения. Вызывай, когда пользователь явно отказывается от изменений.", + "parameters": {"type": "object", "properties": {}} + } + }, + { + "type": "function", + "function": { + "name": "db_preview_prompt_merge", + "description": "ОБЯЗАТЕЛЬНЫЙ ИНСТРУМЕНТ для ЛЮБЫХ изменений системного промпта (добавление пунктов, удаление, форматирование, отступы). Вызывай его ВСЕГДА, когда пользователь просит изменить промпт. В параметре proposed_prompt передавай ИТОГОВЫЙ полный текст со всеми разделами целиком.", + "parameters": { + "type": "object", + "properties": { + "proposed_prompt": { + "type": "string", + "description": "Полный текст системного промпта, содержащий все разделы от 1 до 3 с учетом внесенных изменений." + } + }, + "required": ["proposed_prompt"] + } + } } ] diff --git a/system_prompt.txt b/system_prompt.txt new file mode 100644 index 0000000..95aa929 --- /dev/null +++ b/system_prompt.txt @@ -0,0 +1,70 @@ +python3 -c " +import sqlite3 + +db_path = '/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db' +full_prompt = '''Ты — интеллектуальный ИИ-ассистент и архитектурный координатор проекта SCUD Orion AI (система учета рабочего времени и контроля доступа). + +1. РОЛЬ И ЗАДАЧИ АССИСТЕНТА + 1.1. Управление бэклогом задач проекта (создание, просмотр, изменение статуса, удаление через инструменты db_get_tasks, db_add_task, db_update_task_status, db_delete_task). + 1.2. Консультация по архитектуре, правилам и арбитражу кадровых данных/СКУД из базы знаний (ai_knowledge_base через db_get_rules). + 1.3. Информирование оператора о возможностях системы, доступных командах и инструментах управления. + 1.4. Поддержка диалога с разработчиками, системными администраторами и операторами системы. + +2. ПРАВИЛА АНАЛИЗА И ВЫЗОВА ИНСТРУМЕНТОВ + 2.1. Если пользователь просит изменить или добавить правило в системный промпт, ты обязан взять текущий текст промпта (полученный через db_get_system_prompt), внедрить в него изменения, сохранить всю структуру целиком от начала до конца и вызвать инструмент db_preview_prompt_merge. + 2.2. При запросах на удаление или смену статуса по коротким номерам (например, \"1 и 2\") сопоставляй их с TASK-001, TASK-002 и сразу вызывай соответствующие инструменты без лишних вопросов. + 2.3. Если запрос пользователя неоднозначен, размыт или не содержит достаточно данных — задай краткий, вежливый и конкретный уточняющий вопрос. + +3. ПРАВИЛА ФОРМАТИРОВАНИЯ И СТИЛЯ + 3.1. АБСОЛЮТНЫЙ ЗАПРЕТ: Никогда не начинай ответ со склеек, опечаток или слов-паразитов вроде \"Почемучто\". Ответ должен начинаться строго с результата действия или текста ответа. + 3.2. По умолчанию отвечай строго в чистом текстовом формате (plain text). Не используй Markdown-спецсимволы (двойные звездочки **, решетки ###), если прямо не попросили о форматировании. + 3.3. Сохраняй инженерный, лаконичный и профессиональный стиль.''' + 3.4. ЦЕЛОСТНОСТЬ ДОКУМЕНТА: Запрещено сокращать, усекать или опускать любые разделы промпта при вызове инструментов обновления. Всегда передавай структуру полностью от Раздела 1 до Раздела 3 включительно. + +conn = sqlite3.connect(db_path) +cursor = conn.cursor() +cursor.execute('DELETE FROM system_prompts WHERE name = ?', ('main_agent',)) +cursor.execute('INSERT INTO system_prompts (name, prompt_text, is_active) VALUES (?, ?, 1)', ('main_agent', full_prompt)) +conn.commit() +conn.close() +print('[✓] Полный промпт успешно восстановлен в БД.') +" + +python3 -c ' +import sqlite3 +from passlib.context import CryptContext + +db_path = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db" +conn = sqlite3.connect(db_path) +cursor = conn.cursor() + +# 1. Создаем таблицу пользователей, если ее не было +cursor.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + is_admin INTEGER DEFAULT 0, + full_name TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); +""") + +# 2. Создаем хэш пароля (например, для пароля "admin" или вашего текущего) +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +# Укажите желаемый пароль для пользователя puh (сейчас поставим "admin", можете изменить) +pwd_hash = pwd_context.hash("M@n0raga") + +# 3. Вставляем или обновляем пользователя puh +cursor.execute(""" + INSERT INTO users (username, password_hash, is_admin, full_name) + VALUES (?, ?, 1, ?) + ON CONFLICT(username) DO UPDATE SET + password_hash = excluded.password_hash, + is_admin = 1; +""", ("puh", pwd_hash, "Пушков Александр Александрович")) + +conn.commit() +conn.close() +print("[✓] Таблица users создана, пользователь puh успешно добавлен в рабочую БД!") +' \ No newline at end of file