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): 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"): 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 "Специфических правил компании пока нет." total_staff = len(merged_df) # 1. Пришедшие в офис present_office_cnt = len(merged_df[merged_df['Пришел'] == True]) # 2. Не пришедшие удаленщики reason_series = merged_df['Вид_отсутствия'].astype(str).str.lower() is_remote_reason = reason_series.str.contains('удален|дистанцион', regex=True, na=False) remote_home_cnt = len(merged_df[(merged_df['Пришел'] == False) & is_remote_reason]) # 3. Официально отсутствующие (без удаленки) explained_cnt = len(merged_df[(merged_df['Пришел'] == False) & (merged_df['Вид_отсутствия'].notna()) & (~is_remote_reason) & (~merged_df['Вид_отсутствия'].astype(str).str.startswith('Исключение'))]) unexplained_cnt = len(absent_unexplained) anomalies_cnt = len(anomalies_list) if anomalies_list else 0 fio_mismatches_python = find_python_fuzzy_matches(absent_unexplained, raw_absent_df) anomalies_formatted = [] if anomalies_list: for idx, a in enumerate(anomalies_list, 1): anomalies_formatted.append(f"{idx}. {a.get('fio', 'Сотрудник')}: {a.get('description', '')}") anomalies_text_block = "\n".join(anomalies_formatted) if anomalies_formatted else "Аномалий не обнаружено." 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}") prompt = f""" Ты — старший аудитор кадровой безопасности и контроллинга СКУД. Сформируй итоговую сводку кадрового контроля на {date_str}. 📌 БАЗА ЗНАНИЙ И ПРАВИЛА ПРЕДПРИЯТИЯ: {custom_rules_str} 🚨 ВХОДНЫЕ МЕТРИКИ: - Всего сотрудников: {total_staff} - Итого на работе (в офисе): {present_office_cnt} - В том числе на удаленной работе: {remote_home_cnt} - Официально отсутствуют: {explained_cnt} - Неизвестно (истинно неотмеченные): {unexplained_cnt} - Выявлено аномалий/конфликтов реестров: {anomalies_cnt} --- 🚨 ПОДТВЕРЖДЁННЫЕ АНОМАЛИИ ({anomalies_cnt} шт): {anomalies_text_block} --- ⚠️ ПОДТВЕРЖДЁННЫЕ 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. КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО выводить JSON! В блоке "Выявленные ИИ аномалии" выведи читаемый нумерованный список строк ровно так, как передано в разделе "ПОДТВЕРЖДЁННЫЕ АНОМАЛИИ". Если аномалий нет, напиши: "Аномалий не обнаружено." 2. В разделе "Подозрения на ошибки сопоставления ФИО" используй ТОЛЬКО массив `ПОДТВЕРЖДЁННЫЕ PYTHON ОШИБКИ СОПОСТАВЛЕНИЯ ФИО`. Если он пуст, напиши: "Ошибок сопоставления ФИО и неточностей в 1С не обнаружено." 3. В разделе "Неизвестные случаи" выведи НУМЕРОВАННЫЙ СПИСОК всех {unexplained_cnt} человек ровно в том виде, в котором они переданы выше. 4. В разделе "Рекомендации" дай 2-3 конкретные системные рекомендации для кадровой службы без перечисления конкретных ФИО. СТРОГИЙ ШАБЛОН ОТВЕТА: **Сводка контроллинга СКУД и 1С:ЗУП на {date_str}** - Всего офисных сотрудников: **{total_staff}** - Итого на работе (в офисе): **{present_office_cnt}** - В том числе на удаленной работе: **{remote_home_cnt}** - Официально отсутствуют: **{explained_cnt}** - Неизвестно (истинно неотмеченные): **{unexplained_cnt}** чел. - Выявлено аномалий/конфликтов реестров: **{anomalies_cnt}** шт. #### 🚨 Выявленные ИИ аномалии и конфликты источников ({anomalies_cnt}): (Выведи нумерованный текстовый список аномалий или 'Аномалий не обнаружено.') #### ⚠️ Подозрения на ошибки сопоставления ФИО и несоответствия 1С: (Выведи данные ИСКЛЮЧИТЕЛЬНО из массива ошибок ФИО. Если пуст — 'Ошибок сопоставления ФИО и неточностей в 1С не обнаружено.') #### Неизвестные случаи: {unexplained_cnt} (Выведи нумерованный список всех {unexplained_cnt} человек) ### Точечные рекомендации: (2-3 системные рекомендации) """ sys_prompt = "Ты — русскоязычный кадровый аудитор. Пиши СТРОГО на русском языке обычным форматированным текстом. Категорически запрещено выводить фигурные скобки JSON или технический код." return ask_ollama(prompt, system_prompt=sys_prompt)