135 lines
7.4 KiB
Python
135 lines
7.4 KiB
Python
import json
|
|
import difflib
|
|
import re
|
|
from modules.ai_verifier import ask_ollama
|
|
from modules.knowledge_base import load_knowledge_base
|
|
|
|
|
|
def find_python_fuzzy_matches(unexplained_df, raw_absent_df):
|
|
"""
|
|
Точный поиск совпадений ФИО силами Python (без галлюцинаций ИИ).
|
|
Сравнивает фамилию и полное имя с порогом сходства >= 0.80.
|
|
"""
|
|
if unexplained_df.empty or raw_absent_df is None or raw_absent_df.empty:
|
|
return []
|
|
|
|
# Собираем словарь чистых ФИО 1С -> сырые ФИО 1С
|
|
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="29.07.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 "Специфических правил компании пока нет."
|
|
|
|
# 1. Точный расчет неточных совпадений через Python (Исключает галлюцинации ИИ)
|
|
fio_mismatches_python = find_python_fuzzy_matches(absent_unexplained, raw_absent_df)
|
|
|
|
# 2. Список истинно неизвестных для ИИ
|
|
unexplained_list = []
|
|
if not absent_unexplained.empty:
|
|
for idx, r in absent_unexplained.iterrows():
|
|
fio = r.get('Сотрудник', r.get('fio_clean', ''))
|
|
dept = r.get('Подразделение', '—')
|
|
pos = r.get('Должность', '—')
|
|
unexplained_list.append(f"{fio} — {dept}, {pos}")
|
|
|
|
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
|
|
|
|
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 = "Ты — эксперт кадрового аудита. Будь точен, не придумывай непереданные данные."
|
|
return ask_ollama(prompt, system_prompt=sys_prompt) |