промежуточный положительный результат. Отчеты коррктно формируются. Перед автоматической загрузкой файлов 1С.

This commit is contained in:
2026-07-30 12:42:41 +03:00
parent 2fc417f3a7
commit 9347c42d22
10 changed files with 477 additions and 237 deletions
+83 -38
View File
@@ -1,34 +1,72 @@
import json
import difflib
import re
from modules.ai_verifier import ask_ollama
from modules.knowledge_base import load_knowledge_base
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, date_str="28.07.2026"):
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 "Специфических правил компании пока нет."
detailed_cases_for_ai = []
unexplained_fios = absent_unexplained['fio_clean'].unique() if 'fio_clean' in absent_unexplained.columns else []
# 1. Точный расчет неточных совпадений через Python (Исключает галлюцинации ИИ)
fio_mismatches_python = find_python_fuzzy_matches(absent_unexplained, raw_absent_df)
for fio_clean in unexplained_fios:
staff_matches = []
if raw_staff_df is not None and not raw_staff_df.empty and 'fio_clean' in raw_staff_df.columns:
st_m = raw_staff_df[raw_staff_df['fio_clean'] == fio_clean]
cols_staff = [c for c in ['ФИО', 'Подразделение', 'Должность', 'Таб_№'] if c in st_m.columns]
staff_matches = st_m[cols_staff].to_dict(orient='records')
scud_matches = []
if raw_scud_df is not None and not raw_scud_df.empty and 'fio_clean' in raw_scud_df.columns:
sc_m = raw_scud_df[raw_scud_df['fio_clean'] == fio_clean]
cols_scud = [c for c in ['Сотрудник', 'Подразделение', 'Должность', 'Начало_дня', 'Статус'] if c in sc_m.columns]
scud_matches = sc_m[cols_scud].to_dict(orient='records')
detailed_cases_for_ai.append({
"fio_clean": fio_clean,
"staff_records_1C": staff_matches,
"scud_records_LOGS": scud_matches
})
# 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) | (
@@ -41,25 +79,34 @@ def generate_markdown_report(merged_df, absent_explained, absent_unexplained, sc
prompt = f"""
Ты — старший аудитор кадровой безопасности и контроллинга СКУД.
Проанализируй аномалии и пересечения данных за {date_str}.
Сформируй итоговую сводку кадрового контроля на {date_str}.
📌 НАКОПЛЕННЫЕ ПРАВИЛА ПРЕДПРИЯТИЯ И ИСКЛЮЧЕНИЯ:
📌 БАЗА ЗНАНИЙ И ПРАВИЛА ПРЕДПРИЯТИЯ:
{custom_rules_str}
🚨 ВХОДНЫЕ МЕТРИКИ:
- Всего сотрудников: {total_staff}
- На работе / Удаленно / Командировка: {present_cnt}
- Официально отсутствуют (1С/Реестры): {explained_cnt}
- Неизвестно (требуют выяснения): {unexplained_cnt}
- Выявлено аномалий и конфликтов реестров: {anomalies_cnt}
- Работают (офис / удаленка / командировки): {present_cnt}
- Официально отсутствуют: {explained_cnt}
- Неизвестно (истинно неотмеченные): {unexplained_cnt}
- Выявлено аномалий/конфликтов реестров: {anomalies_cnt}
---
🚨 ОБНАРУЖЕННЫЕ АНОМАЛИИ И КОНФЛИКТЫ МЕЖДУ СКУД, 1С, СТАТИЧЕСКИМИ РЕЕСТРАМИ И ИСКЛЮЧЕНИЯМИ ({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(detailed_cases_for_ai, ensure_ascii=False, indent=2)}
{json.dumps(unexplained_list, ensure_ascii=False, indent=2)}
СТРОГИЕ ИНСТРУКЦИИ ДЛЯ ИИ:
1. КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО придумывать несуществующие совпадения ФИО или приписывать суффиксы "(осн.)". Используй ТОЛЬКО массив `ПОДТВЕРЖДЁННЫЕ PYTHON ОШИБКИ СОПОСТАВЛЕНИЯ ФИО`. Если этот массив пуст, напиши в этом разделе: "Ошибок сопоставления ФИО и неточностей в 1С не обнаружено."
2. В разделе "Неизвестные случаи" выведи НУМЕРОВАННЫЙ СПИСОК всех {unexplained_cnt} человек ровно в том виде, в котором они переданы выше.
3. В разделе "Рекомендации" дай 2-3 конкретные системные рекомендации для кадровой службы. НЕ ПЕРЕЧИСЛЯЙ конкретные ФИО в тексте рекомендаций.
СТРОГИЙ ШАБЛОН ОТВЕТА:
@@ -72,19 +119,17 @@ def generate_markdown_report(merged_df, absent_explained, absent_unexplained, sc
- Выявлено аномалий/конфликтов реестров: **{anomalies_cnt}** шт.
#### 🚨 Выявленные ИИ аномалии и конфликты источников ({anomalies_cnt}):
(Подробно разбери каждый конфликт из `anomalies_list`. Опиши суть нарушения. Если аномалий нет — напиши "Аномалий и конфликтов не обнаружено.")
(Описание аномалий из anomalies_list. Если их нет — "Аномалий не обнаружено.")
#### ⚠️ Подозрения на ложные срабатывания / Совмещения:
(Укажи сотрудников с повторными проходами или несколькими картами, если такие есть)
#### ⚠️ Подозрения на ошибки сопоставления ФИО и несоответствия 1С:
(Выведи данные ИСКЛЮЧИТЕЛЬНО из массива fio_mismatches_python. Если он пуст — "Ошибок сопоставления ФИО и неточностей в 1С не обнаружено.")
#### Неизвестные случаи: {unexplained_cnt}
(Выведи НУМЕРОВАННЫЙ СПИСОК ВСЕХ {unexplained_cnt} человек в формате:
1. ФИО — Подразделение, Должность
2. ФИО — Подразделение, Должность)
(Выведи нумерованный список всех {unexplained_cnt} человек)
### Рекомендации:
(Сформулируй 2-3 общие системные рекомендации для кадровой службы. КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО перечислять конкретные ФИО сотрудников в скобках или тексте рекомендаций — они уже перечислены выше.)
### Точечные рекомендации:
(2-3 системные рекомендации без указания ФИО сотрудников)
"""
sys_prompt = "Ты — эксперт кадрового аудита. Пиши сжато, лаконично, без дублирования ФИО сотрудников в блоке рекомендаций."
sys_prompt = "Ты — эксперт кадрового аудита. Будь точен, не придумывай непереданные данные."
return ask_ollama(prompt, system_prompt=sys_prompt)