135 lines
7.5 KiB
Python
135 lines
7.5 KiB
Python
import json
|
|
import difflib
|
|
import re
|
|
from services.ai_verifier import ask_ollama
|
|
from services.knowledge_base import load_knowledge_base
|
|
|
|
|
|
def find_ai_identity_suggestions(unexplained_df, raw_staff_df):
|
|
"""
|
|
⭐️ Теневой ИИ-арбитраж: ищет потенциальные опечатки операторов СКУД
|
|
среди нераспознанных сотрудников и формирует рекомендации для администратора.
|
|
"""
|
|
if unexplained_df.empty or raw_staff_df is None or raw_staff_df.empty:
|
|
return []
|
|
|
|
staff_fios = raw_staff_df['fio_clean'].dropna().unique().tolist() if 'fio_clean' in raw_staff_df.columns else []
|
|
unexp_fios = unexplained_df['fio_clean'].dropna().unique().tolist() if 'fio_clean' in unexplained_df.columns else []
|
|
|
|
suggestions = []
|
|
for scud_fio in unexp_fios:
|
|
f_parts = scud_fio.split()
|
|
if not f_parts:
|
|
continue
|
|
surname = f_parts[0].lower()
|
|
|
|
for staff_fio in staff_fios:
|
|
if staff_fio.lower().startswith(surname[:4]):
|
|
ratio = difflib.SequenceMatcher(None, scud_fio.lower(), staff_fio.lower()).ratio()
|
|
if 0.75 <= ratio < 1.0:
|
|
suggestions.append({
|
|
"scud_fio": scud_fio,
|
|
"suggested_zup_fio": staff_fio,
|
|
"confidence": round(ratio, 2)
|
|
})
|
|
break
|
|
|
|
return suggestions
|
|
|
|
|
|
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)
|
|
present_office_cnt = len(merged_df[merged_df['Пришел'] == True])
|
|
|
|
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])
|
|
|
|
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
|
|
|
|
# ⭐️ Теневые подсказки ИИ
|
|
ai_suggestions = find_ai_identity_suggestions(absent_unexplained, raw_staff_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}
|
|
|
|
---
|
|
💡 ПРЕДЛОЖЕНИЯ ИИ ПО СОПОСТАВЛЕНИЮ ФИО ({len(ai_suggestions)} шт):
|
|
{json.dumps(ai_suggestions, ensure_ascii=False, indent=2)}
|
|
|
|
---
|
|
📊 СПИСОК НЕИЗВЕСТНЫХ СЛУЧАЕВ ({unexplained_cnt} чел):
|
|
{json.dumps(unexplained_list, ensure_ascii=False, indent=2)}
|
|
|
|
СТРОГИЕ ИНСТРУКЦИИ:
|
|
1. КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО выводить JSON! В блоке "Выявленные ИИ аномалии" выведи читаемый нумерованный список строк ровно так, как передано в разделе "ПОДТВЕРЖДЁННЫЕ АНОМАЛИИ".
|
|
2. В разделе "Подозрения на ошибки сопоставления ФИО" выведи рекомендации по сопоставлению из массива "ПРЕДЛОЖЕНИЯ ИИ". Если массив пуст — напиши "Ошибок сопоставления ФИО не обнаружено."
|
|
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}):
|
|
(Список аномалий или 'Аномалий не обнаружено.')
|
|
|
|
#### 💡 Подозрения на ошибки сопоставления ФИО и предложения ИИ:
|
|
(Список предложений сопоставления или 'Ошибок сопоставления ФИО не обнаружено.')
|
|
|
|
#### Неизвестные случаи: {unexplained_cnt}
|
|
(Нумерованный список всех {unexplained_cnt} человек)
|
|
|
|
### Точечные рекомендации:
|
|
(2-3 системные рекомендации)
|
|
"""
|
|
|
|
sys_prompt = "Ты — русскоязычный кадровый аудитор. Пиши СТРОГО на русском языке форматированным текстом. Запрещено выводить фигурные скобки JSON."
|
|
return ask_ollama(prompt, system_prompt=sys_prompt) |