diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/app/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/core/__init__.py b/app/core/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/app/core/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/core/ollama_client.py b/app/core/ollama_client.py deleted file mode 100644 index 24465e0..0000000 --- a/app/core/ollama_client.py +++ /dev/null @@ -1,32 +0,0 @@ -import json -import urllib.request -import urllib.error - -OLLAMA_URL = "http://192.168.11.3:11434/api/chat" -MODEL_NAME = "qwen2.5:14b" - -def send_ollama_request(messages: list, tools: list) -> dict: - """Отправляет HTTP-запрос к локальной модели Ollama.""" - payload = { - "model": MODEL_NAME, - "messages": messages, - "tools": tools, - "stream": False, - "options": { - "num_predict": 2048, - "num_ctx": 8192, - "temperature": 0.1 - } - } - - req = urllib.request.Request( - OLLAMA_URL, - data=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) - - try: - with urllib.request.urlopen(req) as response: - return json.loads(response.read().decode("utf-8")) - except urllib.error.URLError as e: - raise ConnectionError(f"Ошибка связи с Ollama ({OLLAMA_URL}): {e}") diff --git a/app/db/__init__.py b/app/db/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/app/db/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/db/database.py b/app/db/database.py deleted file mode 100644 index 0b349de..0000000 --- a/app/db/database.py +++ /dev/null @@ -1,152 +0,0 @@ -import sqlite3 - -DB_NAME = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db" - -def get_connection(): - return sqlite3.connect(DB_NAME) - -def db_get_active_system_prompt() -> str: - """Извлекает активный системный промпт из базы данных SQLite.""" - try: - conn = get_connection() - cursor = conn.cursor() - cursor.execute("SELECT prompt_text FROM system_prompts WHERE name = 'main_agent' AND is_active = 1 LIMIT 1") - row = cursor.fetchone() - conn.close() - if row and row[0]: - return row[0].replace("**", "") - except Exception as e: - print(f"Ошибка чтения системного промпта из БД: {e}") - - return "Ты — интеллектуальный ИИ-ассистент и архитектурный координатор проекта SCUD Orion AI." - -def db_add_system_prompt(name: str = "main_agent", prompt_text: str = "") -> dict: - """Обновляет или добавляет активный системный промпт в БД.""" - clean_text = prompt_text.replace("**", "") - conn = get_connection() - cursor = conn.cursor() - cursor.execute(""" - INSERT INTO system_prompts (name, prompt_text, is_active) - VALUES (?, ?, 1) - ON CONFLICT(name) DO UPDATE SET prompt_text=excluded.prompt_text, updated_at=CURRENT_TIMESTAMP - """, (name, clean_text)) - conn.commit() - conn.close() - return {"status": "success", "message": f"Системный промпт '{name}' успешно обновлен в базе!"} - -def db_get_rules() -> list: - """Извлекает правила из таблицы ai_knowledge_base.""" - conn = get_connection() - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - cursor.execute("SELECT id, rule_text, added_by FROM ai_knowledge_base ORDER BY id") - rows = [dict(r) for r in cursor.fetchall()] - conn.close() - return rows - -def db_add_rule(rule_text: str, added_by: str = "Operator") -> dict: - """Добавляет новое правило или инструкцию в таблицу ai_knowledge_base.""" - clean_text = rule_text.replace("**", "") - conn = get_connection() - cursor = conn.cursor() - cursor.execute( - "INSERT INTO ai_knowledge_base (rule_text, added_by) VALUES (?, ?)", - (clean_text, added_by) - ) - rule_id = cursor.lastrowid - conn.commit() - conn.close() - return {"status": "success", "message": f"Правило #{rule_id} успешно добавлено в базу знаний!", "rule_id": rule_id} - -def db_get_tasks(status: str = None) -> list: - """Извлекает список задач из реестра БД.""" - conn = get_connection() - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - if status: - cursor.execute("SELECT * FROM tasks WHERE status = ? ORDER BY task_id", (status.upper(),)) - else: - cursor.execute("SELECT * FROM tasks ORDER BY task_id") - rows = [dict(r) for r in cursor.fetchall()] - conn.close() - return rows - -def db_update_task_status(task_id: str, status: str = None, due_date: str = None) -> dict: - """Обновляет статус и/или срок выполнения задачи.""" - conn = get_connection() - cursor = conn.cursor() - updates = [] - params = [] - - if status: - status_upper = status.upper() - updates.append("status = ?") - params.append(status_upper) - if status_upper == "COMPLETED": - updates.append("completed_at = CURRENT_TIMESTAMP") - else: - updates.append("completed_at = NULL") - - if due_date: - updates.append("due_date = ?") - params.append(due_date) - - if not updates: - conn.close() - return {"error": "Не указаны параметры для обновления"} - - params.append(task_id.upper()) - query = f"UPDATE tasks SET {', '.join(updates)} WHERE task_id = ?" - cursor.execute(query, tuple(params)) - - if cursor.rowcount == 0: - conn.close() - return {"error": f"Задача {task_id} не найдена"} - - conn.commit() - conn.close() - return {"status": "success", "message": f"Задача {task_id} обновлена"} - -def db_delete_task(task_id: str) -> dict: - """Удаляет задачу из базы данных по её task_id.""" - conn = get_connection() - cursor = conn.cursor() - cursor.execute("DELETE FROM tasks WHERE task_id = ?", (task_id.upper(),)) - deleted_count = cursor.rowcount - conn.commit() - conn.close() - - if deleted_count == 0: - return {"error": f"Задача {task_id} не найдена"} - return {"status": "success", "message": f"Задача {task_id} удалена"} - -def db_add_task(module: str = "general", title: str = "", priority: str = "MEDIUM", due_date: str = None, task_id: str = None) -> dict: - """Добавляет новую задачу в реестр с авто-генерацией TASK-ID.""" - conn = get_connection() - cursor = conn.cursor() - - if not task_id: - cursor.execute("SELECT task_id FROM tasks WHERE task_id LIKE 'TASK-%'") - rows = cursor.fetchall() - max_num = 0 - for r in rows: - try: - num = int(r[0].replace("TASK-", "")) - if num > max_num: - max_num = num - except ValueError: - pass - task_id = f"TASK-{max_num + 1:02d}" - - try: - cursor.execute( - "INSERT INTO tasks (task_id, module, title, status, priority, due_date) VALUES (?, ?, ?, 'BACKLOG', ?, ?)", - (task_id.upper(), module, title, priority.upper(), due_date) - ) - conn.commit() - res = {"status": "success", "message": f"Задача {task_id} создана", "task_id": task_id} - except sqlite3.IntegrityError: - res = {"error": f"Задача с ID {task_id} уже существует"} - finally: - conn.close() - return res diff --git a/app/orchestrator.py b/app/orchestrator.py deleted file mode 100644 index dcf4396..0000000 --- a/app/orchestrator.py +++ /dev/null @@ -1,101 +0,0 @@ -import json -from typing import List, Dict, Any, Tuple -from datetime import datetime - -from app.db import database -from app.tools.definitions import TOOLS_SCHEMA -from app.tools.handlers import handle_tool_call, get_system_capabilities_text -from app.core.ollama_client import send_ollama_request - -def process_chat_message(user_message: str, chat_history: List[Dict[str, Any]] = None) -> Tuple[str, List[Dict[str, Any]]]: - if chat_history is None: - chat_history = [] - - current_now = datetime.now().strftime("%Y-%m-%d %H:%M") - user_clean = user_message.replace("\xa0", " ").strip() - user_lower = user_clean.lower() - - # 1. Жесткий перехват: Помощь и варианты команд (чистый Plain Text без Markdown) - if any(phrase in user_lower for phrase in ["помощь", "возможности", "что ты умеешь", "помощь в общении", "варианты команд"]): - formatted_text = get_system_capabilities_text() - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - # 2. Жесткий перехват: Системный промпт из БД - if any(phrase in user_lower for phrase in ["системный промпт", "покажи промпт", "промпт системы", "промпт из базы"]): - prompt_text = database.db_get_active_system_prompt() - formatted_text = f"⚙️ Актуальный системный промпт ИИ (из базы SQLite):\n\n{prompt_text}" - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - # 3. Жесткий перехват: База знаний из БД - if any(phrase in user_lower for phrase in ["базу знаний", "база знаний", "покажи правила", "инструкции ии"]): - rules = database.db_get_rules() - if not rules: - formatted_text = "База знаний пуста." - else: - formatted_text = "🧠 База знаний ИИ (ai_knowledge_base):\n\n" - for r in rules: - formatted_text += f"{r.get('id')}. {r.get('rule_text')}\n\n" - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - # Динамическая подгрузка системного промпта из БД SQLite - dynamic_prompt_text = database.db_get_active_system_prompt() - system_prompt = { - "role": "system", - "content": f"Текущая дата и время сервера: {current_now}.\n\n{dynamic_prompt_text}" - } - - messages = [system_prompt] + chat_history + [{"role": "user", "content": user_message}] - - try: - res_data = send_ollama_request(messages=messages, tools=TOOLS_SCHEMA) - msg = res_data.get("message", {}) - - tool_calls = msg.get("tool_calls", []) - content_str = msg.get("content", "").strip().replace("**", "").replace("```", "").replace("###", "") - - # Перехват текстового JSON с именем функции - if not tool_calls and "name" in content_str and "db_" in content_str: - try: - start_idx = content_str.find("{") - end_idx = content_str.rfind("}") + 1 - if start_idx != -1 and end_idx != -1: - parsed = json.loads(content_str[start_idx:end_idx]) - if "name" in parsed: - tool_calls = [{"function": parsed}] - except Exception: - pass - - if tool_calls: - for tool in tool_calls: - fn_name = tool["function"]["name"] - fn_args = tool["function"].get("arguments", {}) - - formatted_text = handle_tool_call(fn_name, fn_args) - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": content_str} - ] - return content_str, updated_history - - except Exception as e: - return f"Ошибка обработки запроса: {e}", chat_history \ No newline at end of file diff --git a/app/tools/__init__.py b/app/tools/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/app/tools/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/tools/definitions.py b/app/tools/definitions.py deleted file mode 100644 index 34f4a8d..0000000 --- a/app/tools/definitions.py +++ /dev/null @@ -1,111 +0,0 @@ -TOOLS_SCHEMA = [ - { - "type": "function", - "function": { - "name": "db_get_tasks", - "description": "ПОЛУЧИТЬ СПИСОК ЗАДАЧ ПРОЕКТА. Вызывай ТОЛЬКО когда пользователь просит показать задачи, бэклог или список дел.", - "parameters": { - "type": "object", - "properties": { - "status": { - "type": "string", - "description": "Фильтр статуса: BACKLOG, IN_PROGRESS или COMPLETED." - } - } - } - } - }, - { - "type": "function", - "function": { - "name": "db_get_rules", - "description": "ПОЛУЧИТЬ БАЗУ ЗНАНИЙ ИИ И ПРАВИЛА АРБИТРАЖА (ai_knowledge_base). Вызывай когда пользователь просит показать базу знаний, правила, инструкции или промпты.", - "parameters": {"type": "object", "properties": {}} - } - }, - { - "type": "function", - "function": { - "name": "db_add_rule", - "description": "Добавить новое правило, инструкцию или факт в базу знаний (ai_knowledge_base). Вызывай при командах 'добавь в базу знаний', 'запомни правило', 'запиши инструкцию'.", - "parameters": { - "type": "object", - "properties": { - "rule_text": {"type": "string", "description": "Текст правила, инструкции или факта для сохранения"}, - "added_by": {"type": "string", "description": "Автор или источник правила, по умолчанию Operator"} - }, - "required": ["rule_text"] - } - } - }, - { - "type": "function", - "function": { - "name": "db_get_system_prompt", - "description": "ПОЛУЧИТЬ ТЕКУЩИЙ СИСТЕМНЫЙ ПРОМПТ ИИ (system_prompts). Вызывай когда пользователь просит показать системный промпт, инструкции ассистента или промпт из базы.", - "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": { - "name": "db_update_task_status", - "description": "Изменить статус и/или срок выполнения задачи в реестре.", - "parameters": { - "type": "object", - "properties": { - "task_id": {"type": "string", "description": "Идентификатор задачи, например TASK-17"}, - "status": {"type": "string", "description": "Новый статус: COMPLETED, IN_PROGRESS или BACKLOG"}, - "due_date": {"type": "string", "description": "Срок выполнения задачи"} - }, - "required": ["task_id"] - } - } - }, - { - "type": "function", - "function": { - "name": "db_delete_task", - "description": "Удалить задачу из реестра по её task_id (например, TASK-18).", - "parameters": { - "type": "object", - "properties": { - "task_id": {"type": "string", "description": "Идентификатор задачи для удаления, например TASK-18"} - }, - "required": ["task_id"] - } - } - }, - { - "type": "function", - "function": { - "name": "db_add_task", - "description": "Добавить новую задачу в бэклог проекта.", - "parameters": { - "type": "object", - "properties": { - "title": {"type": "string", "description": "Краткое описание задачи"}, - "priority": {"type": "string", "description": "Приоритет: HIGH, MEDIUM, LOW"}, - "module": {"type": "string", "description": "Модуль проекта, например general или services/scud_export"}, - "due_date": {"type": "string", "description": "Срок выполнения задачи, например '2026-08-07 12:00'"} - }, - "required": ["title"] - } - } - } -] diff --git a/app/tools/handlers.py b/app/tools/handlers.py deleted file mode 100644 index 259043a..0000000 --- a/app/tools/handlers.py +++ /dev/null @@ -1,101 +0,0 @@ -from app.db import database - -def get_system_capabilities_text() -> str: - """Формирует список возможностей и примеров команд для текущей стадии проекта.""" - return ( - "🛠 Доступные команды и возможности системы SCUD Orion AI:\n\n" - "1. Управление задачами (Бэклог):\n" - " • «Покажи последние задачи» — вывод списка задач проекта.\n" - " • «Добавь задачу: Настроить интеграцию 1С с REST API» — создание новой задачи.\n" - " • «Измени статус задачи TASK-01 на COMPLETED» — обновление состояния.\n" - " • «Удали задачу TASK-02» — удаление задачи из реестра.\n\n" - "2. База знаний и правила (ai_knowledge_base):\n" - " • «Покажи базу знаний» (или «покажи правила») — вывод текущих правил проекта.\n" - " • «Добавь в базу знаний: Офисные сотрудники отдела разработки работают по гибкому графику с 09:00 до 11:00» — запись нового правила.\n\n" - "3. Системные промпты (system_prompts):\n" - " • «Покажи системный промпт» — просмотр активных инструкций ИИ.\n" - " • «Обнови системный промпт: Ты — ассистент SCUD Orion AI. Отвечай кратко и строго в формате Plain Text.» — смена роли ИИ на лету.\n\n" - "Вы можете написать любой из этих запросов в чат в свободной форме." - ) - -def handle_tool_call(fn_name: str, fn_args: dict) -> str: - """Диспетчеризация вызова функций и форматирование текстового ответа.""" - - if fn_name == "db_get_tasks": - tasks = database.db_get_tasks(status=fn_args.get("status")) - if not tasks: - return "Список задач пуст." - tasks_sorted = sorted(tasks, key=lambda x: x.get('task_id', ''), reverse=True) - formatted_text = "📋 Последние задачи проекта:\n\n" - for t in tasks_sorted: - due = f" (до {t['due_date']})" if t.get('due_date') else "" - priority = t.get('priority') or 'MEDIUM' - status_str = t.get('status') or 'BACKLOG' - module_str = t.get('module') or 'general' - formatted_text += f"{t.get('task_id')} — {t.get('title')} [{status_str}]\n" - formatted_text += f" • Приоритет: {priority} | Модуль: {module_str}{due}\n\n" - return formatted_text - - elif fn_name == "db_get_rules": - rules = database.db_get_rules() - if not rules: - return "База знаний пуста." - formatted_text = "🧠 База знаний ИИ (ai_knowledge_base):\n\n" - for r in rules: - formatted_text += f"{r.get('id')}. {r.get('rule_text')}\n\n" - return formatted_text - - elif fn_name == "db_add_rule": - res = database.db_add_rule( - rule_text=fn_args.get("rule_text", ""), - added_by=fn_args.get("added_by", "Operator") - ) - return res.get("message", "Правило добавлено в базу знаний.") - - elif fn_name == "db_get_system_prompt": - prompt_text = database.db_get_active_system_prompt() - return f"⚙️ Актуальный системный промпт ИИ (из базы SQLite):\n\n{prompt_text}" - - elif fn_name == "db_add_system_prompt": - res = database.db_add_system_prompt( - name=fn_args.get("name", "main_agent"), - prompt_text=fn_args.get("prompt_text", "") - ) - return res.get("message", "Промпт обновлен.") - - elif fn_name == "db_add_task": - res = database.db_add_task( - module=fn_args.get("module", "general"), - title=fn_args.get("title"), - priority=fn_args.get("priority", "MEDIUM"), - due_date=fn_args.get("due_date") - ) - if "error" in res: - return f"Ошибка добавления задачи: {res['error']}" - assigned_id = res.get("task_id", "TASK") - due_str = fn_args.get('due_date') or 'Без срока' - return ( - f"Задача {assigned_id} успешно создана!\n\n" - f"• Название: {fn_args.get('title')}\n" - f"• Приоритет: {fn_args.get('priority', 'MEDIUM')}\n" - f"• Срок: {due_str}\n" - f"• Модуль: {fn_args.get('module', 'general')}" - ) - - elif fn_name == "db_update_task_status": - res = database.db_update_task_status( - task_id=fn_args.get("task_id"), - status=fn_args.get("status"), - due_date=fn_args.get("due_date") - ) - if "error" in res: - return f"Ошибка обновления задачи: {res['error']}" - return f"Статус задачи {fn_args.get('task_id')} успешно обновлен!" - - elif fn_name == "db_delete_task": - res = database.db_delete_task(task_id=fn_args.get("task_id")) - if "error" in res: - return f"Ошибка удаления задачи: {res['error']}" - return f"Задача {fn_args.get('task_id')} успешно удалена из базы!" - - return f"Функция {fn_name} выполнена." \ No newline at end of file diff --git a/core/database.py b/core/database.py index 2edc040..a57063e 100644 --- a/core/database.py +++ b/core/database.py @@ -98,7 +98,6 @@ def init_db(): sync_knowledge_base_to_db() - def has_scud_logs_for_date(date_str): """Проверяет, есть ли в базе данные СКУД за указанную дату.""" with get_connection() as conn: @@ -106,6 +105,16 @@ def has_scud_logs_for_date(date_str): cursor.execute("SELECT 1 FROM scud_logs WHERE log_date = ? LIMIT 1", (date_str,)) return cursor.fetchone() is not None +def has_yesterday_final_snapshot(date_str): + """Проверяет, зафиксирован ли уже ИТОГОВЫЙ вчерашний снапшот с индексом Y/22:00.""" + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute( + "SELECT 1 FROM scud_logs WHERE log_date = ? AND (snapshot_id LIKE 'Y%' OR snapshot_time LIKE '%22:00:00') LIMIT 1", + (date_str,) + ) + return cursor.fetchone() is not None + def get_or_create_snapshot_id(snapshot_time, date_str=None, is_yesterday=False): """ @@ -393,4 +402,44 @@ def delete_snapshots_by_date(date_str: str): deleted_count = cursor.rowcount conn.commit() print(f"[✓] Удалены все снапшоты за дату [{date_str}]. Удалено строк: {deleted_count}") - return deleted_count \ No newline at end of file + return deleted_count +def init_department_synonyms_db(): + """Создает таблицу синонимов отделов в БД SQLite.""" + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS department_synonyms ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + short_name TEXT UNIQUE NOT NULL, + full_name TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + """) + # По умолчанию добавляем ОВК -> Отдел внутреннего контроля + cursor.execute(""" + INSERT OR IGNORE INTO department_synonyms (short_name, full_name) + VALUES ('овк', 'отдел внутреннего контроля') + """) + conn.commit() + + +def get_department_synonyms_dict(): + """Возвращает словарь всех изученных синонимов {short_name: full_name}.""" + init_department_synonyms_db() + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT LOWER(short_name), LOWER(full_name) FROM department_synonyms") + return {row[0]: row[1] for row in cursor.fetchall()} + + +def add_department_synonym_to_db(short_name, full_name): + """Сохраняет новую пару синонимов отдела в базу SQLite.""" + init_department_synonyms_db() + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute( + "INSERT OR REPLACE INTO department_synonyms (short_name, full_name) VALUES (?, ?)", + (short_name.strip().lower(), full_name.strip().lower()) + ) + conn.commit() + print(f"[✓] В базу SQLite добавлен новый синоним отдела: '{short_name}' ⟷ '{full_name}'") diff --git a/data/scud_orion_ai.db b/data/scud_orion_ai.db index 90f2085..8c3871c 100644 Binary files a/data/scud_orion_ai.db and b/data/scud_orion_ai.db differ diff --git a/frontend/__init__.py b/frontend/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index c7508ba..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,342 +0,0 @@ -cat << 'EOF' > /home/puh/scud_orion_ai_v2/frontend/index.html - - - - - - SCUD Orion AI — Context Manager - - - - - - - -
-
-
- -
-
-

SCUD Orion AI

- Context & Task Manager -
-
- -
- - API Online - - -
-
- - -
-
- -
-

- ИИ-Ассистент -

-

- Для вывода возможных команд управления локальными системами, спросите об этом в произвольном формате или нажмите на кнопку ниже. -

- -
-
- - -
-
-
- -
- -
- - -
- -
-
-
- - - - - - - - - -EOF - -# Копируем файл в корень для совместимости с веб-сервером -cp /home/puh/scud_orion_ai_v2/frontend/index.html /home/puh/scud_orion_ai_v2/index.html \ No newline at end of file diff --git a/init_db.py b/init_db.py deleted file mode 100644 index e279610..0000000 --- a/init_db.py +++ /dev/null @@ -1,78 +0,0 @@ -import sqlite3 -import os -from datetime import datetime - -DB_NAME = "context_memory.db" - -def init_db(): - conn = sqlite3.connect(DB_NAME) - cursor = conn.cursor() - - # 1. Таблица трекинга задач - cursor.execute(""" - CREATE TABLE IF NOT EXISTS tasks ( - task_id TEXT PRIMARY KEY, - module TEXT NOT NULL, - title TEXT NOT NULL, - status TEXT NOT NULL, - priority TEXT NOT NULL, - completed_at TEXT - ) - """) - - # 2. Таблица архитектурных правил и контекста - cursor.execute(""" - CREATE TABLE IF NOT EXISTS architecture_memory ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - category TEXT NOT NULL, - rule_text TEXT NOT NULL, - is_active INTEGER DEFAULT 1, - created_at TEXT DEFAULT CURRENT_TIMESTAMP - ) - """) - - # Первичное заполнение реестра задач [TASK-01] .. [TASK-17] - tasks_data = [ - ("TASK-01", "services/data_loader", "Исправление KeyError по ФИО в Excel экспорте 1С (переход на .iloc)", "COMPLETED", "HIGH", "2026-08-03"), - ("TASK-02", "services/zup_extractor", "Перевод подключения 1С:ЗУП на продакшн MS SQL сервер ACCOUNT-01 + гибридный фоллбэк", "COMPLETED", "HIGH", "2026-08-04"), - ("TASK-03", "services/scud_export", "Инцидент Равина В.Э.: Фиксация 100% приоритета сырых логов pLogData СКУД над расчетом УРВ Ориона", "COMPLETED", "HIGH", "2026-08-04"), - ("TASK-04", "core/database", "Реализация схемы SQLite WAL, сохранение снапшотов и маппинг колонок (department/position)", "COMPLETED", "HIGH", "2026-08-05"), - ("TASK-05", "core/database", "Исправление бага дублирования номеров снапшотов (переход на вычисление MAX() по суффиксу)", "COMPLETED", "HIGH", "2026-08-05"), - ("TASK-06", "scripts/fix_snapshots", "Создание и выполнение скрипта сквозной переиндексации снапшотов с префиксом Y", "COMPLETED", "HIGH", "2026-08-05"), - ("TASK-07", "scripts/db_cli", "Реализация CLI-инспектора БД и выравнивание номеров срезов для идеальной верстки в терминале", "COMPLETED", "HIGH", "2026-08-05"), - ("TASK-08", "main.py / logic", "Двойная проверка исключений по department_scud и 1С, удержание 100% приоритета кадровых документов", "COMPLETED", "HIGH", "2026-08-05"), - ("TASK-09", "services/text_reporter", "Лаконичный вывод неразмеченных сотрудников с короткими аббревиатурами отделов СКУД (РУК, ОИЗ, ПУ и др.)", "COMPLETED", "HIGH", "2026-08-05"), - ("TASK-10", "services/scud_export", "Внутридневной контроль: Извлечение полных цепочек проходов pLogData (входы/выходы за смену)", "BACKLOG", "HIGH", None), - ("TASK-11", "services/data_loader", "Внутридневной контроль: Расчет количества и суммарной длительности отлучек/перекуров сверх обеда", "BACKLOG", "HIGH", None), - ("TASK-12", "services/text_reporter", "Внутридневной контроль: Вывод списка системных нарушителей внутреннего распорядка в Markdown-отчет", "BACKLOG", "MEDIUM", None), - ("TASK-13", "automation", "Автоматизация сбора срезов по расписанию (Cron / Systemd Timers на 12:00, 17:00, 19:00)", "BACKLOG", "HIGH", None), - ("TASK-14", "services/notifications", "Интеграция Telegram-бота для алертов Администратору при массовых сбоях турникетов (>5%)", "BACKLOG", "MEDIUM", None), - ("TASK-15", "api / fastapi", "Создание REST API на FastAPI (/api/v1/snapshots, /api/v1/scud/logs, /api/v1/anomalies)", "BACKLOG", "HIGH", None), - ("TASK-16", "frontend / chat", "Веб-интерфейс с ИИ-чатом на базе Ollama (Qwen 2.5 14B) с поддержкой Function Calling к REST API", "BACKLOG", "HIGH", None), - ("TASK-17", "core/memory", "Создание модуля персистентной памяти проекта в SQLite и CLI-инструмента для фиксации архитектурных решений", "IN_PROGRESS", "HIGH", None), - ] - - cursor.executemany(""" - INSERT OR REPLACE INTO tasks (task_id, module, title, status, priority, completed_at) - VALUES (?, ?, ?, ?, ?, ?) - """, tasks_data) - - # Базовые правила архитектуры - rules_data = [ - ("logic_rule", "Сырые логи pLogData СКУД имееют 100% приоритет над расчетом УРВ Ориона."), - ("logic_rule", "Официальные кадровые документы 1С (отпуска, командировки, больничные) имеют абсолютный приоритет над отсутствием в СКУД."), - ("scud_override", "Запрещено переименовывать ФИО сотрудников при 100% совпадении строк между 1С и СКУД в AI-верификаторе."), - ("formatting", "Температура указывается исключительно в градусах Цельсия, скорости в км/ч.") - ] - - cursor.executemany(""" - INSERT INTO architecture_memory (category, rule_text) - VALUES (?, ?) - """, rules_data) - - conn.commit() - conn.close() - print("[✓] База данных контекста и трекинга задач успешно создана и заполнена!") - -if __name__ == "__main__": - init_db() \ No newline at end of file diff --git a/llm_agent.py b/llm_agent.py deleted file mode 100644 index 867274b..0000000 --- a/llm_agent.py +++ /dev/null @@ -1,474 +0,0 @@ -import sqlite3 -import json -import urllib.request -import urllib.error -from typing import List, Dict, Any, Tuple -from datetime import datetime - -# Абсолютный путь к базе данных проекта scud_orion_ai_v2 -DB_NAME = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db" -OLLAMA_URL = "http://192.168.11.3:11434/api/chat" -MODEL_NAME = "qwen2.5:14b" - - -# === БЛОК РАБОТЫ С БАЗОЙ ДАННЫХ === - -def db_get_active_system_prompt() -> str: - """Извлекает активный системный промпт из базы данных SQLite.""" - try: - conn = sqlite3.connect(DB_NAME) - cursor = conn.cursor() - cursor.execute("SELECT prompt_text FROM system_prompts WHERE name = 'main_agent' AND is_active = 1 LIMIT 1") - row = cursor.fetchone() - conn.close() - if row and row[0]: - # Чистим от случайных звездочек - return row[0].replace("**", "") - except Exception as e: - print(f"Ошибка чтения системного промпта из БД: {e}") - - return "Ты — интеллектуальный ИИ-ассистент и архитектурный координатор проекта SCUD Orion AI." - - -def db_add_system_prompt(name: str = "main_agent", prompt_text: str = "") -> dict: - """Обновляет или добавляет активный системный промпт в БД.""" - clean_text = prompt_text.replace("**", "") - conn = sqlite3.connect(DB_NAME) - cursor = conn.cursor() - cursor.execute(""" - INSERT INTO system_prompts (name, prompt_text, is_active) - VALUES (?, ?, 1) - ON CONFLICT(name) DO UPDATE SET prompt_text=excluded.prompt_text, updated_at=CURRENT_TIMESTAMP - """, (name, clean_text)) - conn.commit() - conn.close() - return {"status": "success", "message": f"Системный промпт '{name}' успешно обновлен в базе!"} - - -def db_get_tasks(status: str = None) -> list: - """Извлекает список задач из реестра БД.""" - conn = sqlite3.connect(DB_NAME) - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - if status: - cursor.execute("SELECT * FROM tasks WHERE status = ? ORDER BY task_id", (status.upper(),)) - else: - cursor.execute("SELECT * FROM tasks ORDER BY task_id") - rows = [dict(r) for r in cursor.fetchall()] - conn.close() - return rows - - -def db_update_task_status(task_id: str, status: str = None, due_date: str = None) -> dict: - """Обновляет статус и/или срок выполнения задачи.""" - conn = sqlite3.connect(DB_NAME) - cursor = conn.cursor() - updates = [] - params = [] - - if status: - status_upper = status.upper() - updates.append("status = ?") - params.append(status_upper) - if status_upper == "COMPLETED": - updates.append("completed_at = CURRENT_TIMESTAMP") - else: - updates.append("completed_at = NULL") - - if due_date: - updates.append("due_date = ?") - params.append(due_date) - - if not updates: - conn.close() - return {"error": "Не указаны параметры для обновления"} - - params.append(task_id.upper()) - query = f"UPDATE tasks SET {', '.join(updates)} WHERE task_id = ?" - cursor.execute(query, tuple(params)) - - if cursor.rowcount == 0: - conn.close() - return {"error": f"Задача {task_id} не найдена"} - - conn.commit() - conn.close() - return {"status": "success", "message": f"Задача {task_id} обновлена"} - - -def db_delete_task(task_id: str) -> dict: - """Удаляет задачу из базы данных по её task_id.""" - conn = sqlite3.connect(DB_NAME) - cursor = conn.cursor() - cursor.execute("DELETE FROM tasks WHERE task_id = ?", (task_id.upper(),)) - deleted_count = cursor.rowcount - conn.commit() - conn.close() - - if deleted_count == 0: - return {"error": f"Задача {task_id} не найдена"} - return {"status": "success", "message": f"Задача {task_id} удалена"} - - -def db_add_task(module: str = "general", title: str = "", priority: str = "MEDIUM", due_date: str = None, task_id: str = None) -> dict: - """Добавляет новую задачу в реестр с авто-генерацией TASK-ID.""" - conn = sqlite3.connect(DB_NAME) - cursor = conn.cursor() - - if not task_id: - cursor.execute("SELECT task_id FROM tasks WHERE task_id LIKE 'TASK-%'") - rows = cursor.fetchall() - max_num = 0 - for r in rows: - try: - num = int(r[0].replace("TASK-", "")) - if num > max_num: - max_num = num - except ValueError: - pass - task_id = f"TASK-{max_num + 1:02d}" - - try: - cursor.execute( - "INSERT INTO tasks (task_id, module, title, status, priority, due_date) VALUES (?, ?, ?, 'BACKLOG', ?, ?)", - (task_id.upper(), module, title, priority.upper(), due_date) - ) - conn.commit() - res = {"status": "success", "message": f"Задача {task_id} создана", "task_id": task_id} - except sqlite3.IntegrityError: - res = {"error": f"Задача с ID {task_id} уже существует"} - finally: - conn.close() - return res - - -def db_get_rules() -> list: - """Извлекает правила из таблицы ai_knowledge_base.""" - conn = sqlite3.connect(DB_NAME) - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - cursor.execute("SELECT id, rule_text, added_by FROM ai_knowledge_base ORDER BY id") - rows = [dict(r) for r in cursor.fetchall()] - conn.close() - return rows - - -# === СХЕМА ИНСТРУМЕНТОВ ДЛЯ OLLAMA === - -TOOLS_SCHEMA = [ - { - "type": "function", - "function": { - "name": "db_get_tasks", - "description": "ПОЛУЧИТЬ СПИСОК ЗАДАЧ ПРОЕКТА. Вызывай ТОЛЬКО когда пользователь просит показать задачи, бэклог или список дел.", - "parameters": { - "type": "object", - "properties": { - "status": { - "type": "string", - "description": "Фильтр статуса: BACKLOG, IN_PROGRESS или COMPLETED." - } - } - } - } - }, - { - "type": "function", - "function": { - "name": "db_get_rules", - "description": "ПОЛУЧИТЬ БАЗУ ЗНАНИЙ ИИ И ПРАВИЛА АРБИТРАЖА (ai_knowledge_base). Вызывай когда пользователь просит показать базу знаний, правила, инструкции или промпты.", - "parameters": {"type": "object", "properties": {}} - } - }, - { - "type": "function", - "function": { - "name": "db_get_system_prompt", - "description": "ПОЛУЧИТЬ ТЕКУЩИЙ СИСТЕМНЫЙ ПРОМПТ ИИ (system_prompts). Вызывай когда пользователь просит показать системный промпт, инструкции ассистента или промпт из базы.", - "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": { - "name": "db_update_task_status", - "description": "Изменить статус и/или срок выполнения задачи в реестре.", - "parameters": { - "type": "object", - "properties": { - "task_id": {"type": "string", "description": "Идентификатор задачи, например TASK-17"}, - "status": {"type": "string", "description": "Новый статус: COMPLETED, IN_PROGRESS или BACKLOG"}, - "due_date": {"type": "string", "description": "Срок выполнения задачи"} - }, - "required": ["task_id"] - } - } - }, - { - "type": "function", - "function": { - "name": "db_delete_task", - "description": "Удалить задачу из реестра по её task_id (например, TASK-18).", - "parameters": { - "type": "object", - "properties": { - "task_id": {"type": "string", "description": "Идентификатор задачи для удаления, например TASK-18"} - }, - "required": ["task_id"] - } - } - }, - { - "type": "function", - "function": { - "name": "db_add_task", - "description": "Добавить новую задачу в бэклог проекта.", - "parameters": { - "type": "object", - "properties": { - "title": {"type": "string", "description": "Краткое описание задачи"}, - "priority": {"type": "string", "description": "Приоритет: HIGH, MEDIUM, LOW"}, - "module": {"type": "string", "description": "Модуль проекта, например general или services/scud_export"}, - "due_date": {"type": "string", "description": "Срок выполнения задачи, например '2026-08-07 12:00'"} - }, - "required": ["title"] - } - } - } -] - - -# === ОСНОВНАЯ ФУНКЦИЯ ОБРАБОТКИ СООБЩЕНИЙ === - -def process_chat_message(user_message: str, chat_history: List[Dict[str, Any]] = None) -> Tuple[str, List[Dict[str, Any]]]: - if chat_history is None: - chat_history = [] - - current_now = datetime.now().strftime("%Y-%m-%d %H:%M") - user_lower = user_message.lower().strip() - - # Жесткие перехваты для 100% чистого вывода без участия генератора Ollama (без звездочек!) - if any(phrase in user_lower for phrase in ["системный промпт", "покажи промпт", "промпт системы", "промпт из базы"]): - prompt_text = db_get_active_system_prompt() - formatted_text = f"⚙️ Актуальный системный промпт ИИ (из базы SQLite):\n\n{prompt_text}" - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - if any(phrase in user_lower for phrase in ["базу знаний", "база знаний", "покажи правила", "инструкции ии"]): - rules = db_get_rules() - if not rules: - formatted_text = "База знаний пуста." - else: - formatted_text = "🧠 База знаний ИИ (ai_knowledge_base):\n\n" - for r in rules: - formatted_text += f"{r.get('id')}. {r.get('rule_text')}\n\n" - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - # Динамическая подгрузка системного промпта из БД SQLite - dynamic_prompt_text = db_get_active_system_prompt() - - system_prompt = { - "role": "system", - "content": f"Текущая дата и время сервера: {current_now}.\n\n{dynamic_prompt_text}" - } - - messages = [system_prompt] + chat_history + [{"role": "user", "content": user_message}] - - payload = { - "model": MODEL_NAME, - "messages": messages, - "tools": TOOLS_SCHEMA, - "stream": False, - "options": { - "num_predict": 2048, - "num_ctx": 8192, - "temperature": 0.1 - } - } - - req = urllib.request.Request( - OLLAMA_URL, - data=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) - - try: - with urllib.request.urlopen(req) as response: - res_data = json.loads(response.read().decode("utf-8")) - msg = res_data.get("message", {}) - - tool_calls = msg.get("tool_calls", []) - content_str = msg.get("content", "").strip().replace("**", "") - - # Перехват вызова функции из текста - if not tool_calls and "name" in content_str and "db_" in content_str: - try: - start_idx = content_str.find("{") - end_idx = content_str.rfind("}") + 1 - if start_idx != -1 and end_idx != -1: - parsed = json.loads(content_str[start_idx:end_idx]) - if "name" in parsed: - tool_calls = [{"function": parsed}] - except Exception: - pass - - if tool_calls: - for tool in tool_calls: - fn_name = tool["function"]["name"] - fn_args = tool["function"].get("arguments", {}) - - # 1. Вывод задач - if fn_name == "db_get_tasks": - tasks = db_get_tasks(status=fn_args.get("status")) - if not tasks: - formatted_text = "Список задач пуст." - else: - tasks_sorted = sorted(tasks, key=lambda x: x.get('task_id', ''), reverse=True) - formatted_text = "📋 Последние задачи проекта:\n\n" - for t in tasks_sorted: - due = f" (до {t['due_date']})" if t.get('due_date') else "" - priority = t.get('priority') or 'MEDIUM' - status_str = t.get('status') or 'BACKLOG' - module_str = t.get('module') or 'general' - - formatted_text += f"{t.get('task_id')} — {t.get('title')} [{status_str}]\n" - formatted_text += f" • Приоритет: {priority} | Модуль: {module_str}{due}\n\n" - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - # 2. Вывод базы знаний - elif fn_name == "db_get_rules": - rules = db_get_rules() - if not rules: - formatted_text = "База знаний пуста." - else: - formatted_text = "🧠 База знаний ИИ (ai_knowledge_base):\n\n" - for r in rules: - formatted_text += f"{r.get('id')}. {r.get('rule_text')}\n\n" - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - # 3. Вывод системного промпта - elif fn_name == "db_get_system_prompt": - prompt_text = db_get_active_system_prompt() - formatted_text = f"⚙️ Актуальный системный промпт ИИ (из базы SQLite):\n\n{prompt_text}" - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - # 4. Добавление/обновление системного промпта - 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", "") - ) - formatted_text = res.get("message", "Промпт обновлен.") - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - # 5. Добавление новой задачи - elif fn_name == "db_add_task": - res = db_add_task( - module=fn_args.get("module", "general"), - title=fn_args.get("title"), - priority=fn_args.get("priority", "MEDIUM"), - due_date=fn_args.get("due_date") - ) - - if "error" in res: - formatted_text = f"Ошибка добавления задачи: {res['error']}" - else: - assigned_id = res.get("task_id", "TASK") - due_str = fn_args.get('due_date') or 'Без срока' - formatted_text = ( - f"Задача {assigned_id} успешно создана!\n\n" - f"• Название: {fn_args.get('title')}\n" - f"• Приоритет: {fn_args.get('priority', 'MEDIUM')}\n" - f"• Срок: {due_str}\n" - f"• Модуль: {fn_args.get('module', 'general')}" - ) - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - # 6. Обновление статуса задачи - elif fn_name == "db_update_task_status": - res = db_update_task_status( - task_id=fn_args.get("task_id"), - status=fn_args.get("status"), - due_date=fn_args.get("due_date") - ) - - if "error" in res: - formatted_text = f"Ошибка обновления задачи: {res['error']}" - else: - formatted_text = f"Статус задачи {fn_args.get('task_id')} успешно обновлен!" - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - # 7. Удаление задачи - elif fn_name == "db_delete_task": - res = db_delete_task(task_id=fn_args.get("task_id")) - if "error" in res: - formatted_text = f"Ошибка удаления задачи: {res['error']}" - else: - formatted_text = f"Задача {fn_args.get('task_id')} успешно удалена из базы!" - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": content_str} - ] - return content_str, updated_history - - except urllib.error.URLError as e: - return f"Ошибка связи с Ollama ({OLLAMA_URL}): {e}", chat_history \ No newline at end of file diff --git a/main.py b/main.py index 4babb73..e409130 100644 --- a/main.py +++ b/main.py @@ -28,6 +28,41 @@ from core.database import ( has_scud_logs_for_date ) +from services.ai_verifier import resolve_department_exception_ai + +def apply_exceptions_from_json(df, exceptions_cfg): + """ + Размечает флаг is_excluded=True на основе правил из exceptions.json и БД SQLite. + Сверяет одновременно 1С, СКУД и использует интеллектуальный поиск отделов. + """ + if df is None or df.empty or not exceptions_cfg: + if df is not None: + df['is_excluded'] = False + return df + + deps = exceptions_cfg.get("departments", []) + exact_pos = [p.lower() for p in exceptions_cfg.get("positions", [])] + pos_kw = [k.lower() for k in exceptions_cfg.get("position_keywords", [])] + exc_fios = [normalize_fio(f) for f in exceptions_cfg.get("fio", [])] + + df['is_excluded'] = False + + for idx, row in df.iterrows(): + fio_clean = row.get('fio_clean', '') + dep_1c = row.get('Подразделение', '') + dep_scud = row.get('department_scud', row.get('department', '')) + pos = str(row.get('Должность', '')).lower() + + is_fio_exc = fio_clean in exc_fios + is_pos_exc = (pos in exact_pos) or any(k in pos for k in pos_kw) if pos else False + + # Двухуровневая интеллектуальная проверка отделов (1С + СКУД + ИИ-Кеш БД) + is_dep_exc = resolve_department_exception_ai(dep_1c, dep_scud, deps) + + if is_fio_exc or is_dep_exc or is_pos_exc: + df.at[idx, 'is_excluded'] = True + + return df def load_exceptions_config(): """Загружает файл exceptions.json из корня проекта.""" diff --git a/migrate_structure.py b/migrate_structure.py deleted file mode 100644 index 42500d5..0000000 --- a/migrate_structure.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -import subprocess - -def run_git_move(): - print("=== Безопасная миграция файлов в структуру проекта ===") - - # Словарь: имя файла -> целевая папка - file_mapping = { - "database.py": "core/database.py", - "scud_export.py": "services/scud_export.py", - "share_copier.py": "services/share_copier.py", - "data_loader.py": "services/data_loader.py", - "data_validator.py": "services/data_validator.py", - "zup_extractor.py": "services/zup_extractor.py", - "ai_verifier.py": "services/ai_verifier.py", - "excel_exporter.py": "services/excel_exporter.py", - "text_reporter.py": "services/text_reporter.py", - "feedback_loop.py": "services/feedback_loop.py", - "knowledge_base.py": "services/knowledge_base.py", - "db_cli.py": "scripts/db_cli.py" - } - - # Убедимся, что папки существуют и содержат __init__.py - for folder in ["core", "services", "api", "frontend", "scripts"]: - os.makedirs(folder, exist_ok=True) - init_file = os.path.join(folder, "__init__.py") - if not os.path.exists(init_file): - open(init_file, 'w').close() - - for filename, dest in file_mapping.items(): - if os.path.exists(filename): - src = filename - elif os.path.exists(dest): - print(f"[ℹ️ Уже на месте] {dest}") - continue - else: - found = None - for root, dirs, files in os.walk("."): - if filename in files and "venv" not in root: - found = os.path.join(root, filename) - break - src = found - - if src and os.path.exists(src): - if src == dest: - continue - dest_dir = os.path.dirname(dest) - os.makedirs(dest_dir, exist_ok=True) - try: - subprocess.run(["git", "mv", src, dest], check=True) - print(f"[git mv] Успешно перемещен: {src} -> {dest}") - except subprocess.CalledProcessError as e: - print(f"[⚠️ Ошибка] Не удалось переместить через git mv {src}: {e}") - else: - print(f"[❌ Не найден] Файл {filename} не обнаружен в репозитории.") - - print("\n=== Миграция завершена. Выполните 'git status' для проверки ===") - -if __name__ == "__main__": - run_git_move() \ No newline at end of file diff --git a/ref.py b/ref.py deleted file mode 100644 index 7359194..0000000 --- a/ref.py +++ /dev/null @@ -1,525 +0,0 @@ -#!/usr/bin/env python3 -import os - -# Базовый путь к проекту -BASE_DIR = "/home/puh/scud_orion_ai_v2" - -# 1. Данные для файла app/db/database.py -DB_PY = '''import sqlite3 - -DB_NAME = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db" - -def get_connection(): - return sqlite3.connect(DB_NAME) - -def db_get_active_system_prompt() -> str: - """Извлекает активный системный промпт из базы данных SQLite.""" - try: - conn = get_connection() - cursor = conn.cursor() - cursor.execute("SELECT prompt_text FROM system_prompts WHERE name = 'main_agent' AND is_active = 1 LIMIT 1") - row = cursor.fetchone() - conn.close() - if row and row[0]: - return row[0].replace("**", "") - except Exception as e: - print(f"Ошибка чтения системного промпта из БД: {e}") - - return "Ты — интеллектуальный ИИ-ассистент и архитектурный координатор проекта SCUD Orion AI." - -def db_add_system_prompt(name: str = "main_agent", prompt_text: str = "") -> dict: - """Обновляет или добавляет активный системный промпт в БД.""" - clean_text = prompt_text.replace("**", "") - conn = get_connection() - cursor = conn.cursor() - cursor.execute(""" - INSERT INTO system_prompts (name, prompt_text, is_active) - VALUES (?, ?, 1) - ON CONFLICT(name) DO UPDATE SET prompt_text=excluded.prompt_text, updated_at=CURRENT_TIMESTAMP - """, (name, clean_text)) - conn.commit() - conn.close() - return {"status": "success", "message": f"Системный промпт '{name}' успешно обновлен в базе!"} - -def db_get_rules() -> list: - """Извлекает правила из таблицы ai_knowledge_base.""" - conn = get_connection() - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - cursor.execute("SELECT id, rule_text, added_by FROM ai_knowledge_base ORDER BY id") - rows = [dict(r) for r in cursor.fetchall()] - conn.close() - return rows - -def db_add_rule(rule_text: str, added_by: str = "Operator") -> dict: - """Добавляет новое правило или инструкцию в таблицу ai_knowledge_base.""" - clean_text = rule_text.replace("**", "") - conn = get_connection() - cursor = conn.cursor() - cursor.execute( - "INSERT INTO ai_knowledge_base (rule_text, added_by) VALUES (?, ?)", - (clean_text, added_by) - ) - rule_id = cursor.lastrowid - conn.commit() - conn.close() - return {"status": "success", "message": f"Правило #{rule_id} успешно добавлено в базу знаний!", "rule_id": rule_id} - -def db_get_tasks(status: str = None) -> list: - """Извлекает список задач из реестра БД.""" - conn = get_connection() - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - if status: - cursor.execute("SELECT * FROM tasks WHERE status = ? ORDER BY task_id", (status.upper(),)) - else: - cursor.execute("SELECT * FROM tasks ORDER BY task_id") - rows = [dict(r) for r in cursor.fetchall()] - conn.close() - return rows - -def db_update_task_status(task_id: str, status: str = None, due_date: str = None) -> dict: - """Обновляет статус и/или срок выполнения задачи.""" - conn = get_connection() - cursor = conn.cursor() - updates = [] - params = [] - - if status: - status_upper = status.upper() - updates.append("status = ?") - params.append(status_upper) - if status_upper == "COMPLETED": - updates.append("completed_at = CURRENT_TIMESTAMP") - else: - updates.append("completed_at = NULL") - - if due_date: - updates.append("due_date = ?") - params.append(due_date) - - if not updates: - conn.close() - return {"error": "Не указаны параметры для обновления"} - - params.append(task_id.upper()) - query = f"UPDATE tasks SET {', '.join(updates)} WHERE task_id = ?" - cursor.execute(query, tuple(params)) - - if cursor.rowcount == 0: - conn.close() - return {"error": f"Задача {task_id} не найдена"} - - conn.commit() - conn.close() - return {"status": "success", "message": f"Задача {task_id} обновлена"} - -def db_delete_task(task_id: str) -> dict: - """Удаляет задачу из базы данных по её task_id.""" - conn = get_connection() - cursor = conn.cursor() - cursor.execute("DELETE FROM tasks WHERE task_id = ?", (task_id.upper(),)) - deleted_count = cursor.rowcount - conn.commit() - conn.close() - - if deleted_count == 0: - return {"error": f"Задача {task_id} не найдена"} - return {"status": "success", "message": f"Задача {task_id} удалена"} - -def db_add_task(module: str = "general", title: str = "", priority: str = "MEDIUM", due_date: str = None, task_id: str = None) -> dict: - """Добавляет новую задачу в реестр с авто-генерацией TASK-ID.""" - conn = get_connection() - cursor = conn.cursor() - - if not task_id: - cursor.execute("SELECT task_id FROM tasks WHERE task_id LIKE 'TASK-%'") - rows = cursor.fetchall() - max_num = 0 - for r in rows: - try: - num = int(r[0].replace("TASK-", "")) - if num > max_num: - max_num = num - except ValueError: - pass - task_id = f"TASK-{max_num + 1:02d}" - - try: - cursor.execute( - "INSERT INTO tasks (task_id, module, title, status, priority, due_date) VALUES (?, ?, ?, 'BACKLOG', ?, ?)", - (task_id.upper(), module, title, priority.upper(), due_date) - ) - conn.commit() - res = {"status": "success", "message": f"Задача {task_id} создана", "task_id": task_id} - except sqlite3.IntegrityError: - res = {"error": f"Задача с ID {task_id} уже существует"} - finally: - conn.close() - return res -''' - -# 2. Данные для файла app/tools/definitions.py -TOOLS_DEFINITIONS_PY = '''TOOLS_SCHEMA = [ - { - "type": "function", - "function": { - "name": "db_get_tasks", - "description": "ПОЛУЧИТЬ СПИСОК ЗАДАЧ ПРОЕКТА. Вызывай ТОЛЬКО когда пользователь просит показать задачи, бэклог или список дел.", - "parameters": { - "type": "object", - "properties": { - "status": { - "type": "string", - "description": "Фильтр статуса: BACKLOG, IN_PROGRESS или COMPLETED." - } - } - } - } - }, - { - "type": "function", - "function": { - "name": "db_get_rules", - "description": "ПОЛУЧИТЬ БАЗУ ЗНАНИЙ ИИ И ПРАВИЛА АРБИТРАЖА (ai_knowledge_base). Вызывай когда пользователь просит показать базу знаний, правила, инструкции или промпты.", - "parameters": {"type": "object", "properties": {}} - } - }, - { - "type": "function", - "function": { - "name": "db_add_rule", - "description": "Добавить новое правило, инструкцию или факт в базу знаний (ai_knowledge_base). Вызывай при командах 'добавь в базу знаний', 'запомни правило', 'запиши инструкцию'.", - "parameters": { - "type": "object", - "properties": { - "rule_text": {"type": "string", "description": "Текст правила, инструкции или факта для сохранения"}, - "added_by": {"type": "string", "description": "Автор или источник правила, по умолчанию Operator"} - }, - "required": ["rule_text"] - } - } - }, - { - "type": "function", - "function": { - "name": "db_get_system_prompt", - "description": "ПОЛУЧИТЬ ТЕКУЩИЙ СИСТЕМНЫЙ ПРОМПТ ИИ (system_prompts). Вызывай когда пользователь просит показать системный промпт, инструкции ассистента или промпт из базы.", - "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": { - "name": "db_update_task_status", - "description": "Изменить статус и/или срок выполнения задачи в реестре.", - "parameters": { - "type": "object", - "properties": { - "task_id": {"type": "string", "description": "Идентификатор задачи, например TASK-17"}, - "status": {"type": "string", "description": "Новый статус: COMPLETED, IN_PROGRESS или BACKLOG"}, - "due_date": {"type": "string", "description": "Срок выполнения задачи"} - }, - "required": ["task_id"] - } - } - }, - { - "type": "function", - "function": { - "name": "db_delete_task", - "description": "Удалить задачу из реестра по её task_id (например, TASK-18).", - "parameters": { - "type": "object", - "properties": { - "task_id": {"type": "string", "description": "Идентификатор задачи для удаления, например TASK-18"} - }, - "required": ["task_id"] - } - } - }, - { - "type": "function", - "function": { - "name": "db_add_task", - "description": "Добавить новую задачу в бэклог проекта.", - "parameters": { - "type": "object", - "properties": { - "title": {"type": "string", "description": "Краткое описание задачи"}, - "priority": {"type": "string", "description": "Приоритет: HIGH, MEDIUM, LOW"}, - "module": {"type": "string", "description": "Модуль проекта, например general или services/scud_export"}, - "due_date": {"type": "string", "description": "Срок выполнения задачи, например '2026-08-07 12:00'"} - }, - "required": ["title"] - } - } - } -] -''' - -# 3. Данные для файла app/tools/handlers.py -TOOLS_HANDLERS_PY = '''from app.db import database - -def handle_tool_call(fn_name: str, fn_args: dict) -> str: - """Диспетчеризация вызова функций и форматирование текстового ответа.""" - - if fn_name == "db_get_tasks": - tasks = database.db_get_tasks(status=fn_args.get("status")) - if not tasks: - return "Список задач пуст." - tasks_sorted = sorted(tasks, key=lambda x: x.get('task_id', ''), reverse=True) - formatted_text = "📋 Последние задачи проекта:\n\n" - for t in tasks_sorted: - due = f" (до {t['due_date']})" if t.get('due_date') else "" - priority = t.get('priority') or 'MEDIUM' - status_str = t.get('status') or 'BACKLOG' - module_str = t.get('module') or 'general' - formatted_text += f"{t.get('task_id')} — {t.get('title')} [{status_str}]\n" - formatted_text += f" • Приоритет: {priority} | Модуль: {module_str}{due}\n\n" - return formatted_text - - elif fn_name == "db_get_rules": - rules = database.db_get_rules() - if not rules: - return "База знаний пуста." - formatted_text = "🧠 База знаний ИИ (ai_knowledge_base):\n\n" - for r in rules: - formatted_text += f"{r.get('id')}. {r.get('rule_text')}\n\n" - return formatted_text - - elif fn_name == "db_add_rule": - res = database.db_add_rule( - rule_text=fn_args.get("rule_text", ""), - added_by=fn_args.get("added_by", "Operator") - ) - return res.get("message", "Правило добавлено в базу знаний.") - - elif fn_name == "db_get_system_prompt": - prompt_text = database.db_get_active_system_prompt() - return f"⚙️ Актуальный системный промпт ИИ (из базы SQLite):\n\n{prompt_text}" - - elif fn_name == "db_add_system_prompt": - res = database.db_add_system_prompt( - name=fn_args.get("name", "main_agent"), - prompt_text=fn_args.get("prompt_text", "") - ) - return res.get("message", "Промпт обновлен.") - - elif fn_name == "db_add_task": - res = database.db_add_task( - module=fn_args.get("module", "general"), - title=fn_args.get("title"), - priority=fn_args.get("priority", "MEDIUM"), - due_date=fn_args.get("due_date") - ) - if "error" in res: - return f"Ошибка добавления задачи: {res['error']}" - assigned_id = res.get("task_id", "TASK") - due_str = fn_args.get('due_date') or 'Без срока' - return ( - f"Задача {assigned_id} успешно создана!\n\n" - f"• Название: {fn_args.get('title')}\n" - f"• Приоритет: {fn_args.get('priority', 'MEDIUM')}\n" - f"• Срок: {due_str}\n" - f"• Модуль: {fn_args.get('module', 'general')}" - ) - - elif fn_name == "db_update_task_status": - res = database.db_update_task_status( - task_id=fn_args.get("task_id"), - status=fn_args.get("status"), - due_date=fn_args.get("due_date") - ) - if "error" in res: - return f"Ошибка обновления задачи: {res['error']}" - return f"Статус задачи {fn_args.get('task_id')} успешно обновлен!" - - elif fn_name == "db_delete_task": - res = database.db_delete_task(task_id=fn_args.get("task_id")) - if "error" in res: - return f"Ошибка удаления задачи: {res['error']}" - return f"Задача {fn_args.get('task_id')} успешно удалена из базы!" - - return f"Функция {fn_name} выполнена." -''' - -# 4. Данные для файла app/core/ollama_client.py -OLLAMA_CLIENT_PY = '''import json -import urllib.request -import urllib.error - -OLLAMA_URL = "http://192.168.11.3:11434/api/chat" -MODEL_NAME = "qwen2.5:14b" - -def send_ollama_request(messages: list, tools: list) -> dict: - """Отправляет HTTP-запрос к локальной модели Ollama.""" - payload = { - "model": MODEL_NAME, - "messages": messages, - "tools": tools, - "stream": False, - "options": { - "num_predict": 2048, - "num_ctx": 8192, - "temperature": 0.1 - } - } - - req = urllib.request.Request( - OLLAMA_URL, - data=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) - - try: - with urllib.request.urlopen(req) as response: - return json.loads(response.read().decode("utf-8")) - except urllib.error.URLError as e: - raise ConnectionError(f"Ошибка связи с Ollama ({OLLAMA_URL}): {e}") -''' - -# 5. Данные для файла app/orchestrator.py -ORCHESTRATOR_PY = '''import json -from typing import List, Dict, Any, Tuple -from datetime import datetime - -from app.db import database -from app.tools.definitions import TOOLS_SCHEMA -from app.tools.handlers import handle_tool_call -from app.core.ollama_client import send_ollama_request - -def process_chat_message(user_message: str, chat_history: List[Dict[str, Any]] = None) -> Tuple[str, List[Dict[str, Any]]]: - if chat_history is None: - chat_history = [] - - current_now = datetime.now().strftime("%Y-%m-%d %H:%M") - user_lower = user_message.lower().strip() - - # Быстрые жесткие перехваты - if any(phrase in user_lower for phrase in ["системный промпт", "покажи промпт", "промпт системы", "промпт из базы"]): - prompt_text = database.db_get_active_system_prompt() - formatted_text = f"⚙️ Актуальный системный промпт ИИ (из базы SQLite):\n\n{prompt_text}" - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - if any(phrase in user_lower for phrase in ["базу знаний", "база знаний", "покажи правила", "инструкции ии"]): - rules = database.db_get_rules() - if not rules: - formatted_text = "База знаний пуста." - else: - formatted_text = "🧠 База знаний ИИ (ai_knowledge_base):\n\n" - for r in rules: - formatted_text += f"{r.get('id')}. {r.get('rule_text')}\n\n" - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - # Загрузка актуального промпта - dynamic_prompt_text = database.db_get_active_system_prompt() - system_prompt = { - "role": "system", - "content": f"Текущая дата и время сервера: {current_now}.\n\n{dynamic_prompt_text}" - } - - messages = [system_prompt] + chat_history + [{"role": "user", "content": user_message}] - - try: - res_data = send_ollama_request(messages=messages, tools=TOOLS_SCHEMA) - msg = res_data.get("message", {}) - - tool_calls = msg.get("tool_calls", []) - content_str = msg.get("content", "").strip().replace("**", "") - - # Перехват текстового JSON с именем функции - if not tool_calls and "name" in content_str and "db_" in content_str: - try: - start_idx = content_str.find("{") - end_idx = content_str.rfind("}") + 1 - if start_idx != -1 and end_idx != -1: - parsed = json.loads(content_str[start_idx:end_idx]) - if "name" in parsed: - tool_calls = [{"function": parsed}] - except Exception: - pass - - if tool_calls: - for tool in tool_calls: - fn_name = tool["function"]["name"] - fn_args = tool["function"].get("arguments", {}) - - formatted_text = handle_tool_call(fn_name, fn_args) - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": formatted_text} - ] - return formatted_text, updated_history - - updated_history = chat_history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": content_str} - ] - return content_str, updated_history - - except Exception as e: - return f"Ошибка обработки запроса: {e}", chat_history -''' - -# 6. Обновленный главный модуль llm_agent.py -LLM_AGENT_PY = '''""" -SCUD Orion AI — Главная точка входа для общения с LLM-ассистентом. -Перенаправляет вызовы в центральный оркестратор app.orchestrator. -""" - -from app.orchestrator import process_chat_message - -__all__ = ["process_chat_message"] -''' - -def build_structure(): - files = { - os.path.join(BASE_DIR, "app", "__init__.py"): "", - os.path.join(BASE_DIR, "app", "db", "__init__.py"): "", - os.path.join(BASE_DIR, "app", "db", "database.py"): DB_PY, - os.path.join(BASE_DIR, "app", "tools", "__init__.py"): "", - os.path.join(BASE_DIR, "app", "tools", "definitions.py"): TOOLS_DEFINITIONS_PY, - os.path.join(BASE_DIR, "app", "tools", "handlers.py"): TOOLS_HANDLERS_PY, - os.path.join(BASE_DIR, "app", "core", "__init__.py"): "", - os.path.join(BASE_DIR, "app", "core", "ollama_client.py"): OLLAMA_CLIENT_PY, - os.path.join(BASE_DIR, "app", "orchestrator.py"): ORCHESTRATOR_PY, - os.path.join(BASE_DIR, "llm_agent.py"): LLM_AGENT_PY, - } - - print("🚀 Начинаем рефакторинг проекта...") - for filepath, content in files.items(): - os.makedirs(os.path.dirname(filepath), exist_ok=True) - with open(filepath, "w", encoding="utf-8") as f: - f.write(content.strip() + "\n") - print(f" [✓] Создан/обновлен: {filepath}") - - print("\n✅ Рефакторинг завершен успешно!") - -if __name__ == "__main__": - build_structure() \ No newline at end of file diff --git a/scripts/migrate_db.py b/scripts/migrate_db.py deleted file mode 100644 index bad7120..0000000 --- a/scripts/migrate_db.py +++ /dev/null @@ -1,47 +0,0 @@ -import sqlite3 -import os - -DB_PATH = 'data/scud_orion_ai.db' - - -def migrate(): - if not os.path.exists(DB_PATH): - print(f"База данных {DB_PATH} не найдена. Создание будет выполнено при первом запуске.") - return - - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - cursor.execute("PRAGMA table_info(scud_logs);") - columns = [row[1] for row in cursor.fetchall()] - - if 'first_activity' not in columns: - cursor.execute("ALTER TABLE scud_logs ADD COLUMN first_activity TEXT DEFAULT '—';") - print("[✓] Добавлена колонка 'first_activity'") - - if 'anomaly_flag' not in columns: - cursor.execute("ALTER TABLE scud_logs ADD COLUMN anomaly_flag TEXT DEFAULT 'NONE';") - print("[✓] Добавлена колонка 'anomaly_flag'") - - if 'snapshot_time' not in columns: - cursor.execute("ALTER TABLE scud_logs ADD COLUMN snapshot_time TEXT DEFAULT NULL;") - print("[✓] Добавлена колонка 'snapshot_time'") - - if 'snapshot_id' not in columns: - cursor.execute("ALTER TABLE scud_logs ADD COLUMN snapshot_id TEXT DEFAULT NULL;") - print("[✓] Добавлена колонка 'snapshot_id'") - - # Проставляем номера для ранее сохраненных срезов - cursor.execute("SELECT DISTINCT snapshot_time FROM scud_logs WHERE snapshot_time IS NOT NULL ORDER BY snapshot_time ASC") - snaps = cursor.fetchall() - for idx, (s_time,) in enumerate(snaps, 1): - s_id = f"{idx:07d}" - cursor.execute("UPDATE scud_logs SET snapshot_id = ? WHERE snapshot_time = ?", (s_id, s_time)) - - conn.commit() - conn.close() - print("[✓] Миграция схемы БД успешно завершена!") - - -if __name__ == "__main__": - migrate() \ No newline at end of file diff --git a/services/ai_verifier.py b/services/ai_verifier.py index cd37cf8..b33b361 100644 --- a/services/ai_verifier.py +++ b/services/ai_verifier.py @@ -4,7 +4,65 @@ import requests import pandas as pd from config import OLLAMA_URL, OLLAMA_MODEL from core.database import get_all_rules_from_db +from core.database import get_department_synonyms_dict, add_department_synonym_to_db +def resolve_department_exception_ai(dept_1c, dept_scud, exception_departments): + """ + Универсальный сопоставитель отделов. + Проверяет 1С, СКУД, локальную базу синонимов SQLite и задействует ИИ для сложных случайных аббревиатур. + """ + if not exception_departments: + return False + + d_1c = str(dept_1c).strip().lower() if dept_1c else "" + d_scud = str(dept_scud).strip().lower() if dept_scud else "" + exc_list = [d.strip().lower() for d in exception_departments] + + # 1. Прямая проверка: если точное имя или подстрока уже совпали в 1С или СКУД + for exc in exc_list: + if exc and (exc in d_1c or exc in d_scud or d_1c in exc or d_scud in exc): + return True + + # 2. Проверка по сохраненной Базе Знаний синонимов из SQLite + synonyms = get_department_synonyms_dict() + for exc in exc_list: + # Если в БД зафиксировано: 'овк' -> 'отдел внутреннего контроля' + full_from_db = synonyms.get(exc, "") + if full_from_db and (full_from_db in d_1c or full_from_db in d_scud): + return True + + # 3. Умный ИИ-арбитраж (если отдел спорный и еще не сохранен в БД) + if d_1c or d_scud: + dept_to_check = d_1c if d_1c else d_scud + prompt = f""" +Ты — кадровый аналитик. +Проверь, является ли отдел сотрудника "{dept_to_check}" тем же самым подразделением, что и один из отделов-исключений: {exception_departments}? + +Примеры: +- "Отдел внутреннего контроля" — это "ОВК" (Да) +- "Отдел технического обеспечения" — это "ОТО" (Да) + +Ответь СТРОГО в формате JSON: +{{ + "is_match": true/false, + "matched_exception": "Название из списка исключений", + "explanation": "краткое объяснение" +}} +""" + try: + raw_res = ask_ollama(prompt, system_prompt="Отвечай только валидным JSON.") + match = re.search(r'\{.*\}', raw_res, re.DOTALL) + if match: + data = json.loads(match.group(0)) + if data.get("is_match"): + matched_exc = data.get("matched_exception", "").lower() + # Запоминаем открытую ИИ связь в SQLite навсегда! + add_department_synonym_to_db(matched_exc, dept_to_check) + return True + except Exception as e: + print(f"[⚠️] Ошибка ИИ-арбитража отделов: {e}") + + return False def ask_ollama(prompt, system_prompt=None): """ diff --git a/services/feedback_loop.py b/services/feedback_loop.py index 785fa73..c2b3463 100644 --- a/services/feedback_loop.py +++ b/services/feedback_loop.py @@ -40,17 +40,14 @@ def review_ai_decisions(report_text, suspicious_cases): elif user_input_clean in ['н', 'нет', 'no', 'n', '-']: print(" [ℹ️] Решение отклонено оператором.") elif user_input: - # Если оператор ввел действительно новое текстовое правило - new_rule = f"Правило по {fio_target}: {user_input}" - - add_rule_to_db(new_rule, added_by="Human_Admin") - if save_rule: - try: - save_rule(new_rule) - except Exception: - pass + # Если оператор обучает систему синониму отделов + if "отдел" in user_input_clean or "овк" in user_input_clean: + from core.database import add_department_synonym_to_db + add_department_synonym_to_db(user_input.strip(), "отдел внутреннего контроля") - print(f" [✓] Новое правило записано в Базу Знаний: {new_rule}") + new_rule = f"Правило: {user_input}" + add_rule_to_db(new_rule, added_by="Human_Admin") + print(f" [✓] Новое правило записано в Базу Знаний SQLite: {new_rule}") else: print(" [ℹ️] Запись пропущена.") diff --git a/services/scud_export.py b/services/scud_export.py index c1848ca..d5e74f3 100644 --- a/services/scud_export.py +++ b/services/scud_export.py @@ -15,7 +15,7 @@ import pyodbc from openpyxl.utils import get_column_letter from config import SCUD_DIR, clean_scud_fio_light, load_exceptions -from core.database import save_scud_to_db, has_scud_logs_for_date +from core.database import save_scud_to_db, has_scud_logs_for_date, has_yesterday_final_snapshot warnings.filterwarnings("ignore", message="pandas only supports SQLAlchemy connectable") @@ -227,8 +227,8 @@ def run_export(input_date: str | None = None, save_xlsx: bool = True, debug: boo # ⚡️ ПРОВЕРКА НАЛИЧИЯ ВЧЕРАШНЕГО ДНЯ В SQLITE: # Если это вчерашний день и его снапшот уже зафиксирован в SQLite — пропускаем тяжелый запрос к MS SQL - if is_yesterday and has_scud_logs_for_date(processing_date_str): - log(f"[ℹ️] Вчерашний день ({processing_date_str}) уже зафиксирован в SQLite. Пропускаем запрос к MS SQL.") + if is_yesterday and has_yesterday_final_snapshot(processing_date_str): + log(f"[ℹ️] Вчерашний день ({processing_date_str}) уже зафиксирован финишным снапшотом Y. Пропускаем запрос к MS SQL.") continue if is_yesterday: diff --git a/update_imports.py b/update_imports.py deleted file mode 100644 index fbbf5c8..0000000 --- a/update_imports.py +++ /dev/null @@ -1,41 +0,0 @@ -import os - -def update_file_imports(filepath): - with open(filepath, 'r', encoding='utf-8') as f: - content = f.read() - - # Заменяем старые пути на новые - new_content = content - new_content = new_content.replace("from modules.database", "from core.database") - new_content = new_content.replace("import modules.database", "import core.database") - - # Все остальные модули уехали в services/ - service_modules = [ - "ai_verifier", "data_loader", "data_validator", "excel_exporter", - "feedback_loop", "knowledge_base", "scud_export", "share_copier", - "text_reporter", "zup_extractor" - ] - - for mod in service_modules: - new_content = new_content.replace(f"from modules.{mod}", f"from services.{mod}") - new_content = new_content.replace(f"import modules.{mod}", f"import services.{mod}") - # Замена для прямых импортов между модулями внутри папки services/ - new_content = new_content.replace(f"from {mod}", f"from services.{mod}") - - if content != new_content: - with open(filepath, 'w', encoding='utf-8') as f: - f.write(new_content) - print(f"[Обновлено] Импорты в файле: {filepath}") - -def run_update(): - print("=== Автоматическое обновление импортов ===") - for root, dirs, files in os.walk("."): - if "venv" in root or ".git" in root: - continue - for file in files: - if file.endswith(".py") and file not in ["migrate_structure.py", "update_imports.py"]: - update_file_imports(os.path.join(root, file)) - print("=== Обновление импортов успешно завершено ===") - -if __name__ == "__main__": - run_update() \ No newline at end of file