готовность к созданию API и базы контекста для ИИ.
This commit is contained in:
+38
-2
@@ -36,7 +36,7 @@ def ask_ollama(prompt, system_prompt=None):
|
||||
|
||||
def get_system_rules_context():
|
||||
"""
|
||||
Загружает глобальные системные правила компании прямо из Базы Данных.
|
||||
Загружает глобальные системные правила компании прямо из Базы Данных SQLite.
|
||||
"""
|
||||
rules = get_all_rules_from_db()
|
||||
if not rules:
|
||||
@@ -146,4 +146,40 @@ def ai_verify_scud_against_staff(unrecognized_scud_fios, staff_fios):
|
||||
except Exception as e:
|
||||
print(f"[!] Ошибка разбора JSON от ИИ при сверке опечаток: {e}")
|
||||
|
||||
return mapping
|
||||
return mapping
|
||||
|
||||
def ai_verify_department_exceptions(unexplained_df, exception_departments):
|
||||
"""
|
||||
Локальный ИИ проверит, не являются ли оставшиеся неотмеченными отделы
|
||||
синонимами/аббревиатурами отделов из exceptions.json.
|
||||
"""
|
||||
if unexplained_df.empty or not exception_departments:
|
||||
return []
|
||||
|
||||
dept_list = unexplained_df['Подразделение'].dropna().unique().tolist()
|
||||
rules_context = get_system_rules_context()
|
||||
|
||||
prompt = f"""
|
||||
{rules_context}
|
||||
Ты — кадровый аудитор.
|
||||
Список отделов-исключений компании: {json.dumps(exception_departments, ensure_ascii=False)}
|
||||
Список отделов сотрудников, попавших в неизвестные: {json.dumps(dept_list, ensure_ascii=False)}
|
||||
|
||||
Определи, какие из отделов сотрудников являются ПОЛНЫМИ НАЗВАНИЯМИ или СИНУНИМАМИ отделов-исключений (например: 'Отдел внутреннего контроля' — это 'ОВК').
|
||||
|
||||
Выдай ответ строго в формате JSON:
|
||||
{{
|
||||
"matched_departments": ["Название отдела 1", "Название отдела 2"]
|
||||
}}
|
||||
"""
|
||||
raw_response = ask_ollama(prompt, system_prompt="Выдавай только валидный JSON.")
|
||||
|
||||
try:
|
||||
match = re.search(r'\{.*\}', raw_response, re.DOTALL)
|
||||
if match:
|
||||
data = json.loads(match.group(0))
|
||||
return data.get("matched_departments", [])
|
||||
except Exception as e:
|
||||
print(f"[⚠️] Ошибка ИИ-арбитража отделов: {e}")
|
||||
|
||||
return []
|
||||
+17
-14
@@ -1,20 +1,23 @@
|
||||
import json
|
||||
import os
|
||||
from core.database import get_all_rules_from_db, add_rule_to_db
|
||||
|
||||
KB_FILE = os.path.join("data", "knowledge_base.json")
|
||||
|
||||
def load_knowledge_base():
|
||||
"""Загружает накопленный опыт и правила компании"""
|
||||
if not os.path.exists(KB_FILE):
|
||||
return {"rules": [], "fio_corrections": {}}
|
||||
with open(KB_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
"""
|
||||
Загружает актуальные правила Базы Знаний компании напрямую из базы данных SQLite.
|
||||
"""
|
||||
rules = get_all_rules_from_db()
|
||||
return {
|
||||
"rules": rules if rules else [],
|
||||
"fio_corrections": {}
|
||||
}
|
||||
|
||||
|
||||
def add_rule_to_kb(new_rule):
|
||||
"""Добавляет новое правило, принятое человеком, в базу знаний"""
|
||||
kb = load_knowledge_base()
|
||||
if new_rule not in kb["rules"]:
|
||||
kb["rules"].append(new_rule)
|
||||
with open(KB_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(kb, f, ensure_ascii=False, indent=2)
|
||||
print(f"[✓] База знаний обновлена! Новое правило: {new_rule}")
|
||||
"""
|
||||
Добавляет новое принятое человеком правило в SQLite таблицу ai_knowledge_base.
|
||||
"""
|
||||
if not new_rule or not new_rule.strip():
|
||||
return
|
||||
add_rule_to_db(new_rule.strip(), added_by="Human")
|
||||
print(f"[✓] База знаний SQLite успешно обновлена! Новое правило: {new_rule.strip()}")
|
||||
+27
-17
@@ -15,7 +15,7 @@ import pyodbc
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
from config import SCUD_DIR, clean_scud_fio_light, load_exceptions
|
||||
from core.database import save_scud_to_db
|
||||
from core.database import save_scud_to_db, has_scud_logs_for_date
|
||||
|
||||
warnings.filterwarnings("ignore", message="pandas only supports SQLAlchemy connectable")
|
||||
|
||||
@@ -85,7 +85,7 @@ WITH DailyLogs AS (
|
||||
Passages AS (
|
||||
SELECT
|
||||
EmployeeID,
|
||||
MIN(TimeVal) AS FirstRawEvent, -- ⭐️ Первое событие ДНЯ ВООБЩЕ (любая дверь/туалет/выход)
|
||||
MIN(TimeVal) AS FirstRawEvent,
|
||||
MIN(CASE WHEN Direction = 'IN' THEN TimeVal END) AS FirstIn,
|
||||
MAX(CASE WHEN Direction = 'OUT' THEN TimeVal END) AS LastOut,
|
||||
MAX(CASE WHEN RowNumDesc = 1 THEN Direction END) AS LastEventType
|
||||
@@ -106,14 +106,11 @@ SELECT
|
||||
ISNULL(CAST(p.TabNumber AS NVARCHAR(50)), N'—') AS [Таб_№],
|
||||
CONVERT(VARCHAR(10), @TargetDate, 104) AS [Дата],
|
||||
ISNULL(CAST(CONVERT(VARCHAR(8), pass.FirstIn, 108) AS NVARCHAR(20)), N'Нет входа') AS [Начало_дня],
|
||||
|
||||
-- ⭐️ НОВАЯ КОЛОНКА: "Первая активность" (если нет входа, но есть события в здании)
|
||||
CASE
|
||||
WHEN pass.FirstIn IS NULL AND pass.FirstRawEvent IS NOT NULL
|
||||
THEN CAST(CONVERT(VARCHAR(8), pass.FirstRawEvent, 108) AS NVARCHAR(20))
|
||||
ELSE N'—'
|
||||
END AS [Первая_активность],
|
||||
|
||||
CASE
|
||||
WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn)
|
||||
THEN N'Нет выхода'
|
||||
@@ -228,9 +225,15 @@ def run_export(input_date: str | None = None, save_xlsx: bool = True, debug: boo
|
||||
processing_date = target["date"]
|
||||
processing_date_str = processing_date.strftime("%d.%m.%Y")
|
||||
period_label = target["name"]
|
||||
is_yesterday = (period_label == "Вчера")
|
||||
|
||||
# ⚡️ ПРОВЕРКА: Если вчерашние данные уже есть в SQLite, повторную выгрузку из MS SQL пропускаем
|
||||
if is_yesterday and has_scud_logs_for_date(processing_date_str):
|
||||
log(f"--- Обработка периода: {period_label} ({processing_date_str}) ---")
|
||||
log(f"[ℹ️] Вчерашние логи СКУД за {processing_date_str} уже присутствуют в SQLite. Повторная выгрузка пропущена.", "INFO")
|
||||
continue
|
||||
|
||||
log(f"--- Обработка периода: {period_label} ({processing_date_str}) ---")
|
||||
|
||||
sql_query = SQL_QUERY_TEMPLATE.format(target_date=processing_date.strftime("%Y-%m-%d"))
|
||||
|
||||
connection = None
|
||||
@@ -241,22 +244,17 @@ def run_export(input_date: str | None = None, save_xlsx: bool = True, debug: boo
|
||||
log(f"База вернула {len(df)} строк за {processing_date_str}")
|
||||
|
||||
if len(df) > 0:
|
||||
# 1. Готовим колонки для локальной БД
|
||||
df['fio_clean'] = df['Сотрудник'].apply(clean_scud_fio_light)
|
||||
|
||||
# Валидация прихода с учетом аномалии неявки на вход
|
||||
df['anomaly_flag'] = 'NONE'
|
||||
mask_anomaly = (df['Начало_дня'] == 'Нет входа') & (df['Первая_активность'] != '—')
|
||||
df.loc[mask_anomaly, 'anomaly_flag'] = 'ANOMALY_NO_IN_HAS_ACTIVITY'
|
||||
|
||||
# Флаг 'Пришел' ставится в True только если был вход
|
||||
df['Пришел'] = df['Статус'].str.contains('Присутствовал', case=False, na=False) & (~mask_anomaly)
|
||||
|
||||
# 2. ПРЯМОЕ СОХРАНЕНИЕ В SQLite (scud_orion_ai.db) С METКОЙ СРЕЗА snapshot_time
|
||||
save_scud_to_db(df, processing_date_str, snapshot_time=snapshot_time)
|
||||
log(f"[✓] Записи за {processing_date_str} успешно сохранены в SQLite с меткой {snapshot_time}!", "SUCCESS")
|
||||
save_scud_to_db(df, processing_date_str, snapshot_time=snapshot_time, is_yesterday=is_yesterday)
|
||||
log(f"[✓] Записи за {processing_date_str} успешно сохранены в SQLite!", "SUCCESS")
|
||||
|
||||
# 3. Сохранение в Excel (если включен флаг save_xlsx)
|
||||
if save_xlsx:
|
||||
file_name = f"Сотрудники_{processing_date_str}.xlsx"
|
||||
file_path = os.path.join(SCUD_DIR, file_name)
|
||||
@@ -285,10 +283,22 @@ def run_export(input_date: str | None = None, save_xlsx: bool = True, debug: boo
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Экспорт данных СКУД в SQLite и Excel")
|
||||
parser.add_argument("--date", dest="input_date", default=None)
|
||||
parser.add_argument("-d", "--debug", action="store_true")
|
||||
parser.add_argument("--no-xlsx", dest="save_xlsx", action="store_false", default=True, help="Отключить сохранение в XLSX")
|
||||
help_text = """
|
||||
Модуль прямого экспорта данных СКУД Орион Pro (MS SQL Server) в SQLite и Excel.
|
||||
|
||||
ПРИМЕРЫ ЗАПУСКА:
|
||||
python services/scud_export.py -- Автоматическая выгрузка за Сегодня и Вчера
|
||||
python services/scud_export.py --date 05.08.2026 -- Точечная выгрузка СКУД за конкретную дату
|
||||
python services/scud_export.py --no-xlsx -- Сохранение логов СКУД ТОЛЬКО в SQLite без дублирования в XLSX
|
||||
python services/scud_export.py -d -- Запуск в режиме отладки MS SQL запросов (DEBUG)
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description=help_text,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
parser.add_argument("--date", dest="input_date", default=None, help="Дата выгрузки в формате ДД.ММ.ГГГГ (по умолчанию выгружаются Вчера и Сегодня)")
|
||||
parser.add_argument("-d", "--debug", action="store_true", help="Включить подробные логи отладки подключения и запросов к MS SQL")
|
||||
parser.add_argument("--no-xlsx", dest="save_xlsx", action="store_false", default=True, help="Отключить генерирование промежуточных XLSX-файлов в папке data/scud/")
|
||||
args = parser.parse_args()
|
||||
|
||||
run_export(args.input_date, save_xlsx=args.save_xlsx, debug=args.debug)
|
||||
+23
-16
@@ -8,12 +8,11 @@ from services.knowledge_base import load_knowledge_base
|
||||
def find_python_fuzzy_matches(unexplained_df, raw_absent_df):
|
||||
"""
|
||||
Точный поиск совпадений ФИО силами Python (без галлюцинаций ИИ).
|
||||
Сравнивает фамилию и полное имя с порогом сходства >= 0.80.
|
||||
Сравнивает фамилию и полное имя с порогом сходства >= 0.75.
|
||||
"""
|
||||
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('ФИО', ''))
|
||||
@@ -37,7 +36,6 @@ def find_python_fuzzy_matches(unexplained_df, raw_absent_df):
|
||||
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:
|
||||
@@ -51,23 +49,13 @@ def find_python_fuzzy_matches(unexplained_df, raw_absent_df):
|
||||
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"):
|
||||
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"):
|
||||
# 1. Загрузка Базы Знаний
|
||||
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}")
|
||||
|
||||
# 2. Предварительный расчет ключевых метрик (РЕШАЕТ NameError: total_staff)
|
||||
total_staff = len(merged_df)
|
||||
is_working_mask = (merged_df['Пришел'] == True) | (
|
||||
merged_df['Вид_отсутствия'].astype(str).str.lower().str.contains('командировк|удален|дистанцион|разъездн', regex=True, na=False)
|
||||
@@ -77,6 +65,25 @@ def generate_markdown_report(merged_df, absent_explained, absent_unexplained, sc
|
||||
unexplained_cnt = len(absent_unexplained)
|
||||
anomalies_cnt = len(anomalies_list) if anomalies_list else 0
|
||||
|
||||
# 3. Точный расчет неточных совпадений через Python
|
||||
fio_mismatches_python = find_python_fuzzy_matches(absent_unexplained, raw_absent_df)
|
||||
|
||||
# 4. Формирование списка неизвестных с приоритетом аббревиатуры СКУД
|
||||
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}")
|
||||
|
||||
# 5. Сборка промпта для Ollama
|
||||
prompt = f"""
|
||||
Ты — старший аудитор кадровой безопасности и контроллинга СКУД.
|
||||
Сформируй итоговую сводку кадрового контроля на {date_str}.
|
||||
|
||||
Reference in New Issue
Block a user