import json import difflib import re from services.ai_verifier import ask_ollama from services.knowledge_base import load_knowledge_base def find_python_fuzzy_matches(unexplained_df, raw_absent_df): """ Точный поиск совпадений ФИО силами Python (без галлюцинаций ИИ). Сравнивает фамилию и полное имя с порогом сходства >= 0.75. """ if unexplained_df.empty or raw_absent_df is None or raw_absent_df.empty: return [] absent_dict = {} for idx, r in raw_absent_df.iterrows(): raw_fio = str(r.get('ФИО', '')) clean_fio = str(r.get('fio_clean', '')) reason = str(r.get('Вид_отсутствия', '')) if clean_fio and clean_fio not in ['Ао "Ленморниипроект"', 'Сотрудник', 'Nan', 'None']: absent_dict[clean_fio] = {'raw_fio': raw_fio, 'reason': reason} matches = [] unexplained_fios = unexplained_df['fio_clean'].unique() if 'fio_clean' in unexplained_df.columns else [] for fio in unexplained_fios: fio_parts = fio.split() if not fio_parts: continue surname = fio_parts[0].lower() for abs_clean, abs_info in absent_dict.items(): abs_parts = abs_clean.split() if not abs_parts: continue abs_surname = abs_parts[0].lower() if surname == abs_surname: ratio = difflib.SequenceMatcher(None, fio.lower(), abs_clean.lower()).ratio() if ratio >= 0.75: matches.append({ "target_fio": fio, "found_in_1c_raw": abs_info['raw_fio'], "reason_1c": abs_info['reason'] }) break return matches def generate_markdown_report(merged_df, absent_explained, absent_unexplained, scud_present_but_absent_in_1c, anomalies_list=None, raw_scud_df=None, raw_staff_df=None, raw_absent_df=None, date_str="05.08.2026"): # 1. Загрузка Базы Знаний kb = load_knowledge_base() custom_rules = kb.get("rules", []) custom_rules_str = "\n".join([f"- {r}" for r in custom_rules]) if custom_rules else "Специфических правил компании пока нет." # 2. Предварительный расчет ключевых метрик (РЕШАЕТ NameError: total_staff) total_staff = len(merged_df) is_working_mask = (merged_df['Пришел'] == True) | ( merged_df['Вид_отсутствия'].astype(str).str.lower().str.contains('командировк|удален|дистанцион|разъездн', regex=True, na=False) ) present_cnt = len(merged_df[is_working_mask]) explained_cnt = len(absent_explained) unexplained_cnt = len(absent_unexplained) anomalies_cnt = len(anomalies_list) if anomalies_list else 0 # 3. Точный расчет неточных совпадений через Python fio_mismatches_python = find_python_fuzzy_matches(absent_unexplained, raw_absent_df) # 4. Формирование списка неизвестных с приоритетом аббревиатуры СКУД unexplained_list = [] if not absent_unexplained.empty: for idx, r in absent_unexplained.iterrows(): fio = r.get('Сотрудник', r.get('fio_clean', '')) dept_scud = str(r.get('department_scud', '')).strip() dept_1c = str(r.get('Подразделение', '')).strip() dept = dept_scud if (dept_scud and dept_scud.lower() != 'nan' and dept_scud != '—') else dept_1c if not dept: dept = '—' pos = r.get('Должность', '—') unexplained_list.append(f"{fio} — {dept}, {pos}") # 5. Сборка промпта для Ollama prompt = f""" Ты — старший аудитор кадровой безопасности и контроллинга СКУД. Сформируй итоговую сводку кадрового контроля на {date_str}. 📌 БАЗА ЗНАНИЙ И ПРАВИЛА ПРЕДПРИЯТИЯ: {custom_rules_str} 🚨 ВХОДНЫЕ МЕТРИКИ: - Всего сотрудников: {total_staff} - Работают (офис / удаленка / командировки): {present_cnt} - Официально отсутствуют: {explained_cnt} - Неизвестно (истинно неотмеченные): {unexplained_cnt} - Выявлено аномалий/конфликтов реестров: {anomalies_cnt} --- 🚨 ПОДТВЕРЖДЁННЫЕ АНОМАЛИИ ({anomalies_cnt} шт): {json.dumps(anomalies_list, ensure_ascii=False, indent=2)} --- ⚠️ ПОДТВЕРЖДЁННЫЕ PYTHON ОШИБКИ СОПОСТАВЛЕНИЯ ФИО В 1С ({len(fio_mismatches_python)} шт): {json.dumps(fio_mismatches_python, ensure_ascii=False, indent=2)} --- 📊 СПИСОК НЕИЗВЕСТНЫХ СЛУЧАЕВ ({unexplained_cnt} чел): {json.dumps(unexplained_list, ensure_ascii=False, indent=2)} СТРОГИЕ ИНСТРУКЦИИ ДЛЯ ИИ: 1. КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО придумывать несуществующие совпадения ФИО или приписывать суффиксы "(осн.)". Используй ТОЛЬКО массив `ПОДТВЕРЖДЁННЫЕ PYTHON ОШИБКИ СОПОСТАВЛЕНИЯ ФИО`. Если этот массив пуст, напиши в этом разделе: "Ошибок сопоставления ФИО и неточностей в 1С не обнаружено." 2. В разделе "Неизвестные случаи" выведи НУМЕРОВАННЫЙ СПИСОК всех {unexplained_cnt} человек ровно в том виде, в котором они переданы выше. 3. В разделе "Рекомендации" дай 2-3 конкретные системные рекомендации для кадровой службы. НЕ ПЕРЕЧИСЛЯЙ конкретные ФИО в тексте рекомендаций. СТРОГИЙ ШАБЛОН ОТВЕТА: **Сводка контроллинга СКУД и 1С:ЗУП на {date_str}** - Всего офисных сотрудников: **{total_staff}** - Работают (офис / удаленка / командировки): **{present_cnt}** - Официально отсутствуют: **{explained_cnt}** - Неизвестно (истинно неотмеченные): **{unexplained_cnt}** чел. - Выявлено аномалий/конфликтов реестров: **{anomalies_cnt}** шт. #### 🚨 Выявленные ИИ аномалии и конфликты источников ({anomalies_cnt}): (Описание аномалий из anomalies_list. Если их нет — "Аномалий не обнаружено.") #### ⚠️ Подозрения на ошибки сопоставления ФИО и несоответствия 1С: (Выведи данные ИСКЛЮЧИТЕЛЬНО из массива fio_mismatches_python. Если он пуст — "Ошибок сопоставления ФИО и неточностей в 1С не обнаружено.") #### Неизвестные случаи: {unexplained_cnt} (Выведи нумерованный список всех {unexplained_cnt} человек) ### Точечные рекомендации: (2-3 системные рекомендации без указания ФИО сотрудников) """ sys_prompt = "Ты — русскоязычный кадровый аудитор. Пиши СТРОГО на русском языке по предоставленному Markdown-шаблону. Категорически запрещено выводить JSON или китайские символы." return ask_ollama(prompt, system_prompt=sys_prompt)