промежуточно. Работает все, неверно формируется вчерашний отчет (нет выхода).

This commit is contained in:
2026-07-28 16:41:26 +03:00
parent 522800d317
commit 2603627098
13 changed files with 297 additions and 283 deletions
+21 -14
View File
@@ -3,27 +3,34 @@ import re
import requests
from config import OLLAMA_URL, OLLAMA_MODEL
def ask_ollama(prompt, model=OLLAMA_MODEL, system_prompt=None):
"""Базовая функция отправки запросов к Ollama"""
if system_prompt is None:
system_prompt = "Ты — офисный аудитор безопасности и кадрового учета."
def ask_ollama(prompt, system_prompt=None):
"""
Универсальная функция отправки запросов к Ollama (поддерживает /api/generate и /api/chat)
"""
payload = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
"model": OLLAMA_MODEL,
"prompt": f"{system_prompt}\n\n{prompt}" if system_prompt else prompt,
"stream": False
}
try:
response = requests.post(OLLAMA_URL, json=payload, timeout=60)
response = requests.post(OLLAMA_URL, json=payload, timeout=120)
if response.status_code == 200:
return response.json()['message']['content']
data = response.json()
# Проверяем формат /api/generate
if "response" in data:
return data["response"].strip()
# Проверяем формат /api/chat
elif "message" in data and "content" in data["message"]:
return data["message"]["content"].strip()
else:
return f"⚠️ Неизвестная структура ответа Ollama: {data}"
else:
return f"Ошибка API Ollama: {response.status_code}"
return f"⚠️ Ошибка Ollama (Код {response.status_code}): {response.text}"
except requests.exceptions.ConnectionError:
return "⚠️ Не удалось подключиться к Ollama. Проверьте, запущен ли сервис (ollama run qwen2.5:14b)."
except Exception as e:
return f"Ошибка подключения к Ollama: {e}"
return f"⚠️ Ошибка при обращении к Ollama: {e}"
def ai_verify_scud_against_staff(unrecognized_scud_fios, staff_fios):
"""ИИ-сверка опечаток и несовпадений ФИО между СКУД и 1С:ЗУП"""
+9 -11
View File
@@ -3,7 +3,8 @@ import pandas as pd
import warnings
from config import (
normalize_fio, clean_scud_fio_light, load_exceptions,
DATA_DIR, SCUD_FILE, SCUD_FILE_TODAY, SCUD_FILE_YESTERDAY
DATA_DIR, SCUD_FILE, SCUD_FILE_TODAY, SCUD_FILE_YESTERDAY,
STAFF_FILE_1C, ABSENT_FILE_1C
)
warnings.filterwarnings('ignore', category=UserWarning, module='pandas')
@@ -20,19 +21,17 @@ def is_excluded(fio, dept, pos, exceptions):
return False
def load_staff_data():
staff_path = os.path.join(DATA_DIR, "Штатные сотрудники - ОУП.xlsx")
if not os.path.exists(staff_path):
raise FileNotFoundError(f"Файл штатного расписания не найден: {staff_path}")
df_staff = pd.read_excel(staff_path, skiprows=7)
if not os.path.exists(STAFF_FILE_1C):
raise FileNotFoundError(f"Файл штатного расписания не найден по пути: {STAFF_FILE_1C}")
df_staff = pd.read_excel(STAFF_FILE_1C, skiprows=7)
df_staff = df_staff.rename(columns={df_staff.columns[1]: 'ФИО'})
df_staff['fio_clean'] = df_staff['ФИО'].apply(normalize_fio)
return df_staff
def load_absent_data():
absent_path = os.path.join(DATA_DIR, "Отсутствия сотрудников - выгрузка.xlsx")
if not os.path.exists(absent_path):
raise FileNotFoundError(f"Файл отсутствий не найден: {absent_path}")
df_absent_raw = pd.read_excel(absent_path, skiprows=3)
if not os.path.exists(ABSENT_FILE_1C):
raise FileNotFoundError(f"Файл отсутствий не найден по пути: {ABSENT_FILE_1C}")
df_absent_raw = pd.read_excel(ABSENT_FILE_1C, skiprows=3)
df_absent = df_absent_raw[1:].copy().reset_index(drop=True)
df_absent = df_absent.rename(columns={df_absent.columns[0]: 'ФИО', df_absent.columns[2]: 'Вид_отсутствия'})
df_absent['fio_clean'] = df_absent['ФИО'].apply(normalize_fio)
@@ -41,7 +40,7 @@ def load_absent_data():
def load_scud_data():
scud_path = SCUD_FILE_TODAY if os.path.exists(SCUD_FILE_TODAY) else (SCUD_FILE_YESTERDAY if os.path.exists(SCUD_FILE_YESTERDAY) else SCUD_FILE)
if not os.path.exists(scud_path):
raise FileNotFoundError(f"Файл СКУД не найден в папке {DATA_DIR}")
raise FileNotFoundError(f"Файл СКУД не найден в папке data/scud: {scud_path}")
df_scud = pd.read_excel(scud_path)
exceptions = load_exceptions()
@@ -63,7 +62,6 @@ def load_scud_data():
df_scud['Пришел'] = df_scud.apply(is_person_present, axis=1)
# Отмечаем флаг исключений, но НЕ удаляем сразу
df_scud['is_excluded'] = df_scud.apply(
lambda r: is_excluded(r.get('Сотрудник', ''), r.get('Подразделение', ''), r.get('Должность', ''), exceptions),
axis=1
+24 -36
View File
@@ -1,46 +1,34 @@
import os
import pandas as pd
from datetime import datetime, timedelta
from config import DATA_DIR, DATE_TODAY, DATE_YESTERDAY
import time
from datetime import datetime
from config import SCUD_FILE_TODAY, STAFF_FILE_1C, ABSENT_FILE_1C
def check_file_freshness():
def check_file_freshness(max_age_hours=24):
"""
Проверяет наличие и актуальность файлов СКУД и 1С.
Возвращает (is_valid, warnings_list, errors_list)
Проверяет существование и свежесть входных файлов.
"""
errors = []
warnings = []
errors = []
now = time.time()
max_age_sec = max_age_hours * 3600
# Ожидаемые имена файлов
scud_today_file = os.path.join(DATA_DIR, f"Сотрудники_{DATE_TODAY}.xlsx")
scud_yesterday_file = os.path.join(DATA_DIR, f"Сотрудники_{DATE_YESTERDAY}.xlsx")
staff_file = os.path.join(DATA_DIR, "Штатные сотрудники - ОУП.xlsx")
absent_file = os.path.join(DATA_DIR, "Отсутствия сотрудников - выгрузка.xlsx")
now = datetime.now()
# 1. Проверка файла СКУД за СЕГОДНЯ
if not os.path.exists(scud_today_file):
errors.append(f"❌ КРИТИЧЕСКАЯ ОШИБКА: Отсутствует актуальный файл СКУД за сегодня: 'Сотрудники_{DATE_TODAY}.xlsx'!")
else:
# Проверяем дату изменения файла (mtime)
file_mtime = datetime.fromtimestamp(os.path.getmtime(scud_today_file))
if file_mtime.date() < now.date():
errors.append(f"⚠️ ОШИБКА АКТУАЛЬНОСТИ: Файл 'Сотрудники_{DATE_TODAY}.xlsx' существует, но изменялся прошлым днем ({file_mtime.strftime('%d.%m.%Y %H:%M')})!")
# 2. Проверка файла СКУД за ВЧЕРА
if not os.path.exists(scud_yesterday_file):
warnings.append(f"⚠️ ПРЕДУПРЕЖДЕНИЕ: Файл СКУД за вчера ({DATE_YESTERDAY}) не найден. Детальный отчет за вчера может быть неполным.")
# 3. Проверка файлов 1С (Штат и Отсутствия)
for f_path, f_name in [(staff_file, "Штатные сотрудники"), (absent_file, "Выгрузка отсутствий")]:
if not os.path.exists(f_path):
errors.append(f"❌ КРИТИЧЕСКАЯ ОШИБКА: Файл 1С '{f_name}' не найден в папке data/!")
# 1. Проверка файлов 1С в data/1c
for file_path, label in [(STAFF_FILE_1C, "Штатное расписание 1С"), (ABSENT_FILE_1C, "Отсутствия 1С")]:
if not os.path.exists(file_path):
errors.append(f"Файл '{label}' не найден по пути: {file_path}")
else:
file_mtime = datetime.fromtimestamp(os.path.getmtime(f_path))
days_old = (now - file_mtime).days
if days_old >= 2:
warnings.append(f"⚠️ УСТАРЕВШИЕ ДАННЫЕ 1С: Файл '{os.path.basename(f_path)}' не обновлялся {days_old} дн. (Дата: {file_mtime.strftime('%d.%m.%Y')})")
mtime = os.path.getmtime(file_path)
age_hours = (now - mtime) / 3600
if age_hours > max_age_hours:
warnings.append(
f"Файл '{label}' обновлялся давно ({age_hours:.1f} ч. назад). "
f"Дата изменения: {datetime.fromtimestamp(mtime).strftime('%d.%m.%Y %H:%M')}"
)
# 2. Проверка файла СКУД в data/scud
if not os.path.exists(SCUD_FILE_TODAY):
warnings.append(f"Сегодняшний файл СКУД еще не сформирован: {SCUD_FILE_TODAY}")
is_valid = len(errors) == 0
return is_valid, warnings, errors
+27 -75
View File
@@ -1,11 +1,5 @@
"""
Скрипт экспорта данных СКУД (Orion) в Excel.
Оптимизированная супер-быстрая версия (использует ROW_NUMBER() вместо подзапросов).
Запуск:
python scud_export.py
python scud_export.py -d
python scud_export.py --date 28.07.2026 -d
Модуль автоматического экспорта данных СКУД (Orion) из MS SQL Server в Excel.
"""
import argparse
@@ -19,13 +13,12 @@ import pandas as pd
import pyodbc
from openpyxl.utils import get_column_letter
from config import SCUD_DIR
warnings.filterwarnings("ignore", message="pandas only supports SQLAlchemy connectable")
# ==============================================================================
# 1. Настройки путей и логов
# ==============================================================================
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LOG_DIR = os.path.join(SCRIPT_DIR, "logs")
LOG_DIR = os.path.join(SCRIPT_DIR, "..", "logs")
os.makedirs(LOG_DIR, exist_ok=True)
TODAY_DATE_STR = datetime.now().strftime("%d.%m.%Y")
@@ -34,13 +27,14 @@ LOG_FILE = os.path.join(LOG_DIR, f"export_{TODAY_DATE_STR}.log")
logger = logging.getLogger("scud_export")
logger.setLevel(logging.INFO)
_file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
_console_handler = logging.StreamHandler(sys.stdout)
_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
_file_handler.setFormatter(_formatter)
_console_handler.setFormatter(_formatter)
logger.addHandler(_file_handler)
logger.addHandler(_console_handler)
if not logger.handlers:
_file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
_console_handler = logging.StreamHandler(sys.stdout)
_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
_file_handler.setFormatter(_formatter)
_console_handler.setFormatter(_formatter)
logger.addHandler(_file_handler)
logger.addHandler(_console_handler)
def log(message: str, level: str = "INFO"):
@@ -56,21 +50,12 @@ def log(message: str, level: str = "INFO"):
logger.log(level_map.get(level, logging.INFO), message)
# ==============================================================================
# 2. Настройки базы данных и папки
# ==============================================================================
SERVER_NAME = r"192.168.1.253\SQL"
DATABASE_NAME = "Orion-14.01.21-1"
SQL_USER = "sa"
SQL_PASSWORD = "123456"
FOLDER_PATH = r"C:\scud_exchsnge"
ODBC_DRIVER = "SQL Server Native Client 10.0"
# ==============================================================================
# 3. СУПЕР-ОПТИМИЗИРОВАННЫЙ SQL-ЗАПРОС (Мгновенное выполнение)
# ==============================================================================
SQL_QUERY_TEMPLATE = r"""
DECLARE @InputDate DATE = '{target_date}';
DECLARE @TargetDate DATE = @InputDate;
@@ -78,7 +63,6 @@ DECLARE @TargetDate DATE = @InputDate;
DECLARE @StartDate DATETIME = CAST(@TargetDate AS DATETIME);
DECLARE @EndDate DATETIME = DATEADD(SECOND, -1, DATEADD(DAY, 1, @StartDate));
-- 1. Фильтруем логи только за нужные 24 часа за один проход
WITH DailyLogs AS (
SELECT
log.HozOrgan AS EmployeeID,
@@ -95,7 +79,6 @@ WITH DailyLogs AS (
AND log.HozOrgan > 0
AND (log.Event IN (28, 29, 26, 27, 32, 33) OR log.Mode IN (1, 2))
),
-- 2. Агрегируем времена входа/выхода и определяем последнее событие
Passages AS (
SELECT
EmployeeID,
@@ -167,9 +150,6 @@ ORDER BY p.Name ASC;
"""
# ==============================================================================
# 4. Вспомогательные функции
# ==============================================================================
def auto_fit_columns(file_path: str, sheet_name: str, padding: float = 2.0, min_width: float = 8.0, max_width: float = 60.0):
from openpyxl import load_workbook
@@ -196,7 +176,6 @@ def get_targets(input_date: str | None):
try:
parsed = datetime.strptime(input_date, "%d.%m.%Y").date()
targets.append({"name": "Указанная дата", "date": parsed})
log(f"Запущен ручной режим для даты: {input_date}")
except ValueError:
log(f"ОШИБКА: Неверный формат даты '{input_date}'. Используйте ДД.ММ.ГГГГ", "ERROR")
sys.exit(1)
@@ -211,16 +190,13 @@ def get_targets(input_date: str | None):
return targets
# ==============================================================================
# 5. Основная функция выгрузки
# ==============================================================================
def run_export(input_date: str | None, debug: bool = False):
def run_export(input_date: str | None = None, debug: bool = False):
if debug:
logger.setLevel(logging.DEBUG)
log("=== ВКЛЮЧЕН РЕЖИМ ОТЛАДКИ (DEBUG MODE) ===", "WARNING")
log("=== Запуск скрипта экспорта СКУД ===")
os.makedirs(FOLDER_PATH, exist_ok=True)
log("=== [ЭТАП 0] Выгрузка свежих данных СКУД напрямую из БД Орион ===")
os.makedirs(SCUD_DIR, exist_ok=True)
targets = get_targets(input_date)
@@ -234,6 +210,7 @@ def run_export(input_date: str | None, debug: bool = False):
f"Encrypt=no;"
)
success = True
for target in targets:
processing_date = target["date"]
processing_date_str = processing_date.strftime("%d.%m.%Y")
@@ -242,15 +219,13 @@ def run_export(input_date: str | None, debug: bool = False):
log(f"--- Обработка периода: {period_label} ({processing_date_str}) ---")
file_name = f"Сотрудники_{processing_date_str}.xlsx"
file_path = os.path.join(FOLDER_PATH, file_name)
file_path = os.path.join(SCUD_DIR, file_name)
if os.path.exists(file_path):
log(f"Файл {file_name} существует. Перезаписываем...")
try:
os.remove(file_path)
except OSError as e:
log(f"ОШИБКА при удалении файла {file_name}: {e}", "ERROR")
continue
log(f"ОШИБКА при удалении старого файла {file_name}: {e}", "ERROR")
sql_query = SQL_QUERY_TEMPLATE.format(target_date=processing_date.strftime("%Y-%m-%d"))
@@ -261,51 +236,28 @@ def run_export(input_date: str | None, debug: bool = False):
log(f"База вернула {len(df)} строк за {processing_date_str}")
if debug:
present_df = df[df["Начало_дня"] != "Нет входа"]
still_inside_df = df[(df["Начало_дня"] != "Нет входа") & (df["Конец_дня"] == "Нет выхода")]
left_df = df[(df["Начало_дня"] != "Нет входа") & (df["Конец_дня"] != "Нет выхода")]
print("\n" + "=" * 50)
print(f"[DEBUG STATS] Дата: {processing_date_str}")
print(f" Всего активных сотрудников в БД: {len(df)}")
print(f" Присутствуют / были сегодня: {len(present_df)}")
print(f" Физически находятся в здании: {len(still_inside_df)} (Конец_дня = 'Нет выхода')")
print(f" Завершили рабочий день (Ушли): {len(left_df)}")
if not still_inside_df.empty:
print("\n Первые 5 сотрудников, находящихся сейчас в здании:")
for _, r in still_inside_df.head(5).iterrows():
print(f"{r['Сотрудник']} | Вход: {r['Начало_дня']} | В здании: {r['Находился_в_здании']}")
print("=" * 50 + "\n")
if len(df) > 0:
df.to_excel(file_path, sheet_name="Отчет", index=False, engine="openpyxl")
auto_fit_columns(file_path, sheet_name="Отчет")
log(f"Успешно создан файл {file_path}", "SUCCESS")
log(f"[✓] Успешно экспортирован файл: data/scud/{file_name}", "SUCCESS")
else:
log(f"Запрос за {processing_date_str} вернул 0 строк.", "WARNING")
except Exception as e:
log(f"ОШИБКА при обработке даты {processing_date_str}: {e}", "ERROR")
log(f"🛑 ОШИБКА выгрузки СКУД за {processing_date_str}: {e}", "ERROR")
success = False
finally:
if connection is not None:
connection.close()
log("=== Завершение работы скрипта ===")
log("=== Выгрузка СКУД завершена ===\n")
return success
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Экспорт данных СКУД в Excel")
parser.add_argument(
"--date",
dest="input_date",
default=None,
help='Дата в формате ДД.ММ.ГГГГ (например, "28.07.2026")',
)
parser.add_argument(
"-d", "--debug",
action="store_true",
help="Запуск в режиме отладки (вывод детальной статистики)",
)
parser.add_argument("--date", dest="input_date", default=None)
parser.add_argument("-d", "--debug", action="store_true")
args = parser.parse_args()
run_export(args.input_date, debug=args.debug)
+34 -35
View File
@@ -2,7 +2,8 @@ import json
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, raw_scud_df=None, raw_staff_df=None, date_str="27.07.2026"):
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"):
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 "Специфических правил компании пока нет."
@@ -25,67 +26,65 @@ def generate_markdown_report(merged_df, absent_explained, absent_unexplained, sc
detailed_cases_for_ai.append({
"fio_clean": fio_clean,
"staff_records_count": len(staff_matches),
"staff_records_1C": staff_matches,
"scud_records_LOGS": scud_matches
})
cols_crit = [c for c in ['Сотрудник', 'Подразделение', 'Должность', 'Вид_отсутствия'] if c in scud_present_but_absent_in_1c.columns]
critical_list = scud_present_but_absent_in_1c[cols_crit].to_dict(orient='records')
# Исходные точные цифры Python
total_staff = len(merged_df)
present_cnt = len(merged_df[merged_df['Пришел'] == True])
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} и составь итоговый отчет.
Ты — старший аудитор кадровой безопасности и контроллинга СКУД.
Проанализируй аномалии и пересечения данных за {date_str}.
📌 ДИНАМИЧЕСКИЕ ПРАВИЛА И НАКОПЛЕННЫЙ ОПЫТ ПРЕДПРИЯТИЯ:
📌 НАКОПЛЕННЫЕ ПРАВИЛА ПРЕДПРИЯТИЯ И ИСКЛЮЧЕНИЯ:
{custom_rules_str}
🚨 СТРОГИЕ ПРАВИЛА МАТЕМАТИКИ И КАТЕГОРИЙ:
1. Цифры сводки ДОЛЖНЫ СТРОГО СОВПАДАТЬ с первично посчитанным массивом:
- Всего офисных сотрудников: {total_staff}
- Присутствуют на рабочем месте: {present_cnt}
- Официально отсутствуют: {explained_cnt}
- Неизвестно (истинно неотмеченные): {unexplained_cnt} чел.
2. Заголовок "#### Неизвестные случаи: {unexplained_cnt}" должен иметь ровно ту же цифру {unexplained_cnt}.
3. Нумерованный список ниже ДОЛЖЕН содержать ровно {unexplained_cnt} сотрудников из массива `detailed_cases_for_ai` без пропусков.
🚨 ВХОДНЫЕ МЕТРИКИ:
- Всего сотрудников: {total_staff}
- На работе / Удаленно / Командировка: {present_cnt}
- Официально отсутствуют (1С/Реестры): {explained_cnt}
- Неизвестно (требуют выяснения): {unexplained_cnt}
- Выявлено аномалий и конфликтов реестров: {anomalies_cnt}
---
📊 ВХОДНЫЕ МИКРО-ДАННЫЕ НЕИЗВЕСТНЫХ ({unexplained_cnt} чел):
{json.dumps(detailed_cases_for_ai, ensure_ascii=False, indent=2)}
🚨 ОБНАРУЖЕННЫЕ АНОМАЛИИ И КОНФЛИКТЫ МЕЖДУ СКУД, 1С, СТАТИЧЕСКИМИ РЕЕСТРАМИ И ИСКЛЮЧЕНИЯМИ ({anomalies_cnt} шт.):
{json.dumps(anomalies_list, ensure_ascii=False, indent=2)}
🚨 СПИСОК КРИТИЧЕСКИХ АНОМАЛИЙ:
{json.dumps(critical_list, ensure_ascii=False, indent=2)}
---
📊 СПИСОК НЕИЗВЕСТНЫХ СЛУЧАЕВ ({unexplained_cnt} чел):
{json.dumps(detailed_cases_for_ai, ensure_ascii=False, indent=2)}
СТРОГИЙ ШАБЛОН ОТВЕТА:
**Сводка контроллинга СКУД и 1С:ЗУП на {date_str} в 12:00**
**Сводка контроллинга СКУД и 1С:ЗУП на {date_str}**
- Всего офисных сотрудников: **{total_staff}**
- Присутствуют на рабочем месте: **{present_cnt}**
- Работают (офис / удаленка / командировки): **{present_cnt}**
- Официально отсутствуют: **{explained_cnt}**
- Неизвестно (истинно неотмеченные после проверки ИИ): **{unexplained_cnt}** чел.
- Неизвестно (истинно неотмеченные): **{unexplained_cnt}** чел.
- Выявлено аномалий/конфликтов реестров: **{anomalies_cnt}** шт.
#### ⚠️ Подозрения на ложные срабатывания СКУД / Совмещения:
(Укажи сотрудников с аномалиями/вторыми проходами, например Кондрашову Е.В., если у нее дубликат карты)
#### 🚨 Выявленные ИИ аномалии и конфликты источников ({anomalies_cnt}):
(Подробно разбери каждый конфликт из `anomalies_list`. Опиши суть нарушения. Если аномалий нет — напиши "Аномалий и конфликтов не обнаружено.")
#### Критические аномалии (Работа в отпуске / на больничном):
(Детально опиши сотрудников из списка КРИТИЧЕСКИХ АНОМАЛИЙ. Если их нет — напиши "Критических нарушений не выявлено.")
#### ⚠️ Подозрения на ложные срабатывания / Совмещения:
(Укажи сотрудников с повторными проходами или несколькими картами, если такие есть)
#### Неизвестные случаи: {unexplained_cnt}
(Выведи НУМЕРОВАННЫЙ СПИСОК ВСЕХ {unexplained_cnt} человек строго в формате:
1. ФИО — [Аббревиатура], Должность
2. ФИО — [Аббревиатура], Должность
...
{unexplained_cnt}. ФИО — [Аббревиатура], Должность)
(Выведи НУМЕРОВАННЫЙ СПИСОК ВСЕХ {unexplained_cnt} человек в формате:
1. ФИО — Подразделение, Должность
2. ФИО — Подразделение, Должность)
### Рекомендации:
...
(Сформулируй 2-3 общие системные рекомендации для кадровой службы. КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО перечислять конкретные ФИО сотрудников в скобках или тексте рекомендаций — они уже перечислены выше.)
"""
sys_prompt = "Ты — кадровый аудитор. Выводи ровно то количество людей в списке, которое указано в переменной unexplained_cnt."
sys_prompt = "Ты — эксперт кадрового аудита. Пиши сжато, лаконично, без дублирования ФИО сотрудников в блоке рекомендаций."
return ask_ollama(prompt, system_prompt=sys_prompt)