import json import re 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): """ Универсальная функция отправки запросов к локальной модели Ollama (Qwen 2.5). """ payload = { "model": OLLAMA_MODEL, "prompt": f"{system_prompt}\n\n{prompt}" if system_prompt else prompt, "stream": False } try: response = requests.post(OLLAMA_URL, json=payload, timeout=120) if response.status_code == 200: data = response.json() if "response" in data: return data["response"].strip() elif "message" in data and "content" in data["message"]: return data["message"]["content"].strip() else: return f"⚠️ Неизвестная структура ответа Ollama: {data}" else: return f"⚠️ Ошибка Ollama (Код {response.status_code}): {response.text}" except requests.exceptions.ConnectionError: return "⚠️ Не удалось подключиться к Ollama. Проверьте, запущен ли сервис (ollama run qwen2.5:14b)." except Exception as e: return f"⚠️ Ошибка при обращении к Ollama: {e}" def get_system_rules_context(): """ Загружает динамические системные правила компании напрямую из Базы Данных SQLite. """ rules = get_all_rules_from_db() if not rules: return "" rules_text = "\n".join([f"- {r}" for r in rules]) return f"\nОБЯЗАТЕЛЬНЫЕ ГЛОБАЛЬНЫЕ ПРАВИЛА И ПРИОРИТЕТЫ КОМПАНИИ (ИЗ SQLITE БД):\n{rules_text}\n" def analyze_scud_mass_failure_ai(df_scud): """ Оценивает процент аномалий 'ANOMALY_NO_IN_HAS_ACTIVITY' в выгрузке. Если процент аномалий превышает 5% от смены или 10 человек, вызывает ИИ для генерации критического алерта. """ if df_scud is None or df_scud.empty: return None total_records = len(df_scud) anomaly_rows = df_scud[df_scud.get('anomaly_flag', 'NONE') == 'ANOMALY_NO_IN_HAS_ACTIVITY'] anomaly_count = len(anomaly_rows) if total_records == 0: return None anomaly_percent = round((anomaly_count / total_records) * 100, 1) if anomaly_percent > 5.0 or anomaly_count >= 10: rules_context = get_system_rules_context() prompt = f""" {rules_context} ВНИМАНИЕ! Проведён анализ смены СКУД: - Всего записей за смену: {total_records} - Выявлено сотрудников без утреннего входа, но с зафиксированной дневной активностью: {anomaly_count} ({anomaly_percent}% от смены) Сформируй понятное предупреждение для Администратора СКУД и Руководителя. Объясни, что это критический массовый сбой турникетов/контроллеров входа на КПП, и порекомендуй действия. """ alert_text = ask_ollama(prompt, system_prompt="Ты — ИИ-аналитик контроллинга СКУД. Отвечай кратко и строго по делу.") return { "is_mass_failure": True, "anomaly_count": anomaly_count, "anomaly_percent": anomaly_percent, "alert_text": alert_text } return { "is_mass_failure": False, "anomaly_count": anomaly_count, "anomaly_percent": anomaly_percent, "alert_text": "Массовых сбоев оборудования не зафиксировано." } def ai_verify_scud_against_staff(unrecognized_scud_fios, staff_fios): if not unrecognized_scud_fios or not staff_fios: return {} # Исключаем точные совпадения staff_fios_set = set(staff_fios) real_unrecognized = [f for f in unrecognized_scud_fios if f not in staff_fios_set] if not real_unrecognized: return {} rules_context = get_system_rules_context() prompt = f""" Ты — кадровый аудитор безопасности СКУД. {rules_context} В СКУД записаны неопознанные ФИО: {json.dumps(real_unrecognized, ensure_ascii=False)} В официальном Штатном расписании 1С записаны ЭТАЛОНЫ: {json.dumps(staff_fios, ensure_ascii=False)} СТРОГИЕ ПРАВИЛА: 1. Запись из Штат 1С — это 100% ПРАВИЛЬНЫЙ эталон. 2. В СКУД допущена опечатка. 3. Любое присутствие сотрудника по СКУД при наличии в 1С документа отсутствия (кроме командировок) является гарантированной аномалией. 4. В поле "warning" опиши обнаруженную опечатку. 5. Запрещено выдумывать опечатки и объединять разных людей/однофамильцев! ОТВЕЧАЙ ТОЛЬКО ИСКЛЮЧИТЕЛЬНО В ФОРМАТЕ ВАЛИДНОГО JSON: {{ "verified_matches": [ {{ "scud_fio": "ФИО из СКУД", "staff_fio": "эталон ФИО из Штат 1С", "warning": "Описание опечатки в СКУД" }} ] }} """ raw_response = ask_ollama( prompt, system_prompt="Ты — строгий JSON API генератор. Отвечай только валидным JSON объектом без пояснительного текста." ) mapping = {} if not raw_response: return mapping # Фаза 1: Попытка прямого разбора с санитарной очисткой try: match = re.search(r'\{.*\}', raw_response, re.DOTALL) if match: json_str = match.group(0) # Убираем висячие запятые: {"a": 1,} -> {"a": 1} json_str = re.sub(r',\s*([\}\]])', r'\1', json_str) # Заменяем одинарные кавычки в ключах/значениях на двойные при необходимости json_str = re.sub(r"(?<=\{|\,)\s*'([^']+)'\s*:", r'"\1":', json_str) data = json.loads(json_str) for item in data.get("verified_matches", []): scud_f = item.get("scud_fio") staff_f = item.get("staff_fio") warn = item.get("warning", "Точное совпадение (без опечаток)") if scud_f and staff_f and staff_f in staff_fios_set: mapping[scud_f] = {"staff_fio": staff_f, "warning": warn} return mapping except Exception: pass # Фаза 2: Резервный Regex-парсер (если JSON синтаксически сломан, но пары ключ-значение есть) try: pattern = r'["\']scud_fio["\']\s*:\s*["\']([^"\']+)["\'].*?["\']staff_fio["\']\s*:\s*["\']([^"\']+)["\']' matches = re.findall(pattern, raw_response, re.DOTALL) for scud_f, staff_f in matches: scud_clean = scud_f.strip() staff_clean = staff_f.strip() if staff_clean in staff_fios_set: mapping[scud_clean] = {"staff_fio": staff_clean, "warning": "Восстановлено парсером опечаток"} except Exception as e: print(f"[!] Ошибка резервного парсинга опечаток: {e}") return mapping def ai_verify_department_exceptions(unexplained_df, exception_departments): """ Локальный ИИ проверяет, не являются ли неотмеченные отделы синонимами отделов-исключений. """ if unexplained_df.empty or not exception_departments: return [] dept_list = unexplained_df['Подразделение'].dropna().unique().tolist() rules_context = get_system_rules_context() prompt = f""" {rules_context} Ты — кадровый аудитор. Список отделов-исключений компании: {json.dumps(exception_departments, ensure_ascii=False)} Список отделов сотрудников, попавших в неизвестные: {json.dumps(dept_list, ensure_ascii=False)} Определи, какие из отделов сотрудников являются ПОЛНЫМИ НАЗВАНИЯМИ или СИНУНИМАМИ отделов-исключений (например: 'Отдел внутреннего контроля' — это 'ОВК'). Выдай ответ строго в формате JSON: {{ "matched_departments": ["Название отдела 1", "Название отдела 2"] }} """ raw_response = ask_ollama(prompt, system_prompt="Выдавай только валидный JSON.") try: match = re.search(r'\{.*\}', raw_response, re.DOTALL) if match: data = json.loads(match.group(0)) return data.get("matched_departments", []) except Exception as e: print(f"[⚠️] Ошибка ИИ-арбитража отделов: {e}") return []