feat(etl): stable pipeline, exception registry in SQLite, multi-pass aggregation and db_cli
This commit is contained in:
+132
-49
@@ -7,7 +7,7 @@ from datetime import datetime
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from config import DATA_DIR, DATE_TODAY, OUTPUT_DIR
|
||||
from config import DATA_DIR, DATE_TODAY, OUTPUT_DIR, EXCEPTIONS_PATH, normalize_fio
|
||||
from core.database import (
|
||||
get_connection,
|
||||
get_available_snapshots,
|
||||
@@ -15,9 +15,16 @@ from core.database import (
|
||||
load_scud_from_db_by_snapshot,
|
||||
get_latest_snapshot_time
|
||||
)
|
||||
from services.exceptions_repo import (
|
||||
get_all_exceptions_from_db,
|
||||
add_exception_to_db,
|
||||
remove_exception_from_db,
|
||||
sync_json_to_db
|
||||
)
|
||||
|
||||
DB_PATH = os.path.join(DATA_DIR, "scud_orion_ai.db")
|
||||
|
||||
|
||||
def print_tool_actions():
|
||||
"""Выводит реестр декларативных действий инструментов и шаблоны кнопок."""
|
||||
print("\n" + "=" * 110)
|
||||
@@ -25,25 +32,29 @@ def print_tool_actions():
|
||||
print("=" * 110)
|
||||
with get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT id, tool_name, category, bypass_llm, success_template, follow_up_question, buttons_json
|
||||
FROM tool_action_registry
|
||||
WHERE is_active = 1
|
||||
ORDER BY id ASC
|
||||
""")
|
||||
rows = cursor.fetchall()
|
||||
if not rows:
|
||||
print("Таблица tool_action_registry пуста.")
|
||||
else:
|
||||
for r in rows:
|
||||
print(f"ID: {r[0]} | Tool: [{r[1]}] | Категория: {r[2]} | Bypass LLM: {'ДА (0.05с)' if r[3] else 'НЕТ'}")
|
||||
print(f" • Сообщение: {r[4]}")
|
||||
if r[5]:
|
||||
print(f" • Вопрос: {r[5]}")
|
||||
print(f" • Кнопки: {r[6]}")
|
||||
print("-" * 110)
|
||||
try:
|
||||
cursor.execute("""
|
||||
SELECT id, tool_name, category, bypass_llm, success_template, follow_up_question, buttons_json
|
||||
FROM tool_action_registry
|
||||
WHERE is_active = 1
|
||||
ORDER BY id ASC
|
||||
""")
|
||||
rows = cursor.fetchall()
|
||||
if not rows:
|
||||
print("Таблица tool_action_registry пуста.")
|
||||
else:
|
||||
for r in rows:
|
||||
print(f"ID: {r[0]} | Tool: [{r[1]}] | Категория: {r[2]} | Bypass LLM: {'ДА (0.05с)' if r[3] else 'НЕТ'}")
|
||||
print(f" • Сообщение: {r[4]}")
|
||||
if r[5]:
|
||||
print(f" • Вопрос: {r[5]}")
|
||||
print(f" • Кнопки: {r[6]}")
|
||||
print("-" * 110)
|
||||
except Exception as e:
|
||||
print(f"Таблица tool_action_registry недоступна: {e}")
|
||||
print("=" * 110 + "\n")
|
||||
|
||||
|
||||
def print_stats():
|
||||
"""Выводит общую статистику по записям в таблицах БД."""
|
||||
print("\n" + "=" * 60)
|
||||
@@ -51,12 +62,16 @@ def print_stats():
|
||||
print("=" * 60)
|
||||
with get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
tables = ['scud_logs', 'zup_staff', 'zup_absences', 'anomalies_history', 'ai_knowledge_base', 'chat_messages', 'session_states', 'system_prompt_nodes', 'tasks']
|
||||
tables = [
|
||||
'scud_logs', 'zup_staff', 'zup_absences', 'anomalies_history',
|
||||
'ai_knowledge_base', 'chat_messages', 'session_states',
|
||||
'system_prompt_nodes', 'tasks', 'exceptions_registry'
|
||||
]
|
||||
for t in tables:
|
||||
try:
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {t}")
|
||||
cnt = cursor.fetchone()[0]
|
||||
print(f" • Таблица [{t:<20}]: {cnt:>6} записей")
|
||||
print(f" • Таблица [{t:<22}]: {cnt:>6} записей")
|
||||
except Exception:
|
||||
pass
|
||||
print("=" * 60 + "\n")
|
||||
@@ -82,13 +97,11 @@ def print_snapshots_list(date_str=None):
|
||||
def snapshot_sort_key(row):
|
||||
snap_id = row[0] or ""
|
||||
snap_time = row[2] or ""
|
||||
|
||||
seq_num = 0
|
||||
if "-" in snap_id:
|
||||
parts = snap_id.replace("Y", "").split("-")
|
||||
if len(parts) > 1 and parts[1].isdigit():
|
||||
seq_num = int(parts[1])
|
||||
|
||||
return (snap_time, seq_num)
|
||||
|
||||
sorted_rows = sorted(rows, key=snapshot_sort_key, reverse=True)
|
||||
@@ -104,11 +117,7 @@ def print_snapshots_list(date_str=None):
|
||||
time_part = snap_time.split(" ")[1]
|
||||
|
||||
slice_datetime_str = f"{log_date} {time_part}" if time_part != "—" else log_date
|
||||
|
||||
if not snap_id.startswith("Y"):
|
||||
formatted_snap_id = f" {snap_id}"
|
||||
else:
|
||||
formatted_snap_id = snap_id
|
||||
formatted_snap_id = f" {snap_id}" if not snap_id.startswith("Y") else snap_id
|
||||
|
||||
print(f"{formatted_snap_id:<16} | {snap_time:<24} | {slice_datetime_str:<20} | {count:<8}")
|
||||
|
||||
@@ -141,31 +150,44 @@ def inspect_scud(snapshot_id=None, date_str=None, export_xlsx=None):
|
||||
"""Инспектирует логи СКУД за выбранный снапшот или дату и опционально сохраняет XLSX."""
|
||||
target_date = date_str if date_str else DATE_TODAY
|
||||
|
||||
print("\n" + "=" * 90)
|
||||
print("\n" + "=" * 115)
|
||||
if snapshot_id:
|
||||
print(f"🔍 ИНСПЕКЦИЯ СКУД ПО СНАПШОТУ [{snapshot_id}] (Дата: {target_date}):")
|
||||
print(f"🔍 ИНСПЕКЦИЯ СКУД ПО СНАПШОТУ [{snapshot_id}] (Дата среза: {target_date}):")
|
||||
else:
|
||||
print(f"🔍 ИНСПЕКЦИЯ СКУД ЗА ТЕКУЩУЮ ДАТУ [{target_date}] (ПОСЛЕДНИЙ СРЕЗ):")
|
||||
print("=" * 90)
|
||||
print("=" * 115)
|
||||
|
||||
df = load_scud_from_db_by_snapshot(target_date, snapshot_param=snapshot_id)
|
||||
|
||||
if df.empty:
|
||||
if df is None or df.empty:
|
||||
print("Записи СКУД не найдены.")
|
||||
print("=" * 90 + "\n")
|
||||
print("=" * 115 + "\n")
|
||||
return
|
||||
|
||||
fio_col = next((c for c in ['Сотрудник', 'fio', 'fio_clean'] if c in df.columns), None)
|
||||
dept_col = next((c for c in ['Подразделение', 'department_scud', 'department'] if c in df.columns), None)
|
||||
pos_col = next((c for c in ['Должность', 'position'] if c in df.columns), None)
|
||||
in_col = next((c for c in ['Начало_дня', 'time_in'] if c in df.columns), None)
|
||||
first_act_col = next((c for c in ['Первая_активность', 'first_activity'] if c in df.columns), None)
|
||||
out_col = next((c for c in ['Конец_дня', 'time_out'] if c in df.columns), None)
|
||||
dur_col = next((c for c in ['Находился_в_здании', 'time_in_building', 'duration'] if c in df.columns), None)
|
||||
present_col = next((c for c in ['Пришел', 'is_present'] if c in df.columns), None)
|
||||
anom_col = 'anomaly_flag' if 'anomaly_flag' in df.columns else None
|
||||
snap_col = 'snapshot_id' if 'snapshot_id' in df.columns else None
|
||||
|
||||
total = len(df)
|
||||
present_cnt = len(df[df['Пришел'] == True]) if 'Пришел' in df.columns else 0
|
||||
if present_col:
|
||||
present_cnt = len(df[df[present_col].astype(str).str.lower().isin(['true', '1'])])
|
||||
else:
|
||||
present_cnt = 0
|
||||
absent_cnt = total - present_cnt
|
||||
|
||||
print(f"Всего записей: {total} | Пришли: {present_cnt} | Не пришли: {absent_cnt}")
|
||||
print("-" * 90)
|
||||
print(f"Всего записей: {total} | Присутствовали: {present_cnt} | Отсутствовали: {absent_cnt}")
|
||||
print("-" * 115)
|
||||
|
||||
cols_to_show = ['fio', 'department', 'position', 'time_in', 'first_activity', 'time_out', 'is_present', 'anomaly_flag', 'snapshot_id']
|
||||
existing_cols = [c for c in cols_to_show if c in df.columns]
|
||||
display_cols = [c for c in [fio_col, dept_col, in_col, first_act_col, out_col, dur_col, present_col, anom_col, snap_col] if c]
|
||||
print(df[display_cols].head(30).to_string(index=False))
|
||||
|
||||
print(df[existing_cols].head(30).to_string(index=False))
|
||||
if len(df) > 30:
|
||||
print(f"\n... и ещё {len(df) - 30} строк.")
|
||||
|
||||
@@ -175,11 +197,11 @@ def inspect_scud(snapshot_id=None, date_str=None, export_xlsx=None):
|
||||
out_path = os.path.join(OUTPUT_DIR, out_path)
|
||||
|
||||
df.to_excel(out_path, index=False)
|
||||
print("\n" + "*" * 90)
|
||||
print("\n" + "*" * 115)
|
||||
print(f"[✓] УСПЕШНЫЙ ЭКСПОРТ ДЕБАГ-ФАЙЛА В EXCEL: {out_path}")
|
||||
print("*" * 90)
|
||||
print("*" * 115)
|
||||
|
||||
print("=" * 90 + "\n")
|
||||
print("=" * 115 + "\n")
|
||||
|
||||
|
||||
def print_absences(date_str=None):
|
||||
@@ -239,7 +261,7 @@ def dump_all_to_excel(out_filename="db_dump_full.xlsx"):
|
||||
out_path = os.path.join(OUTPUT_DIR, out_filename)
|
||||
print(f"\n[🔄] Создание полного дампа БД в файл: {out_path} ...")
|
||||
with get_connection() as conn, pd.ExcelWriter(out_path, engine='openpyxl') as writer:
|
||||
for table in ['scud_logs', 'zup_staff', 'zup_absences', 'anomalies_history', 'ai_knowledge_base', 'chat_messages', 'session_states', 'system_prompt_nodes', 'tasks']:
|
||||
for table in ['scud_logs', 'zup_staff', 'zup_absences', 'anomalies_history', 'ai_knowledge_base', 'chat_messages', 'session_states', 'system_prompt_nodes', 'tasks', 'exceptions_registry']:
|
||||
try:
|
||||
df = pd.read_sql_query(f"SELECT * FROM {table}", conn)
|
||||
df.to_excel(writer, sheet_name=table[:31], index=False)
|
||||
@@ -373,6 +395,24 @@ def purge_chat_context(session_id=None, purge_all=False):
|
||||
print(f"\n[✓] Умная зачистка контекста выполнена! Удалено сообщений: {deleted_msgs}\n")
|
||||
|
||||
|
||||
# ⭐️ Новые функции управления исключениями (Exceptions & Whitelist)
|
||||
def print_exceptions():
|
||||
"""Выводит реестр исключений и белый список сотрудников из базы SQLite."""
|
||||
exc = get_all_exceptions_from_db()
|
||||
print("\n" + "=" * 80)
|
||||
print("📋 РЕЕСТР ИСКЛЮЧЕНИЙ И БЕЛЫЙ СПИСОК (exceptions_registry):")
|
||||
print("=" * 80)
|
||||
for cat, items in exc.items():
|
||||
print(f"[{cat.upper()}] ({len(items)} шт.):")
|
||||
if items:
|
||||
for it in items:
|
||||
print(f" • {it}")
|
||||
else:
|
||||
print(" — пусто")
|
||||
print("-" * 80)
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
|
||||
HELP_TEXT = """
|
||||
CLI-утилита инспекции и управления SQLite базой данных СКУД (scud_orion_ai.db)
|
||||
|
||||
@@ -391,6 +431,11 @@ CLI-утилита инспекции и управления SQLite базой
|
||||
dump [output.xlsx] -- Полный дамп всех таблиц БД в многостраничный Excel
|
||||
snapshot del [ID] или [--day ДД.ММ.ГГГГ] -- Удаление снапшота по ID или всех за выбранный день
|
||||
|
||||
exceptions [list] -- Посмотреть реестр исключений и белый список (SQLite)
|
||||
exceptions add -c CATEGORY -v VALUE [-m COMMENT] -- Добавить исключение (fio, include_fio, departments, positions, position_keywords)
|
||||
exceptions del -c CATEGORY -v VALUE -- Удалить исключение из БД
|
||||
exceptions sync -- Синхронизировать exceptions.json -> SQLite
|
||||
|
||||
ПРИМЕРЫ ЗАПУСКА:
|
||||
python scripts/db_cli.py stats
|
||||
python scripts/db_cli.py snapshots 06.08.2026
|
||||
@@ -405,6 +450,11 @@ CLI-утилита инспекции и управления SQLite базой
|
||||
python scripts/db_cli.py context purge web_session_main --all
|
||||
python scripts/db_cli.py snapshot del Y20260805-007
|
||||
python scripts/db_cli.py dump my_dump.xlsx
|
||||
python scripts/db_cli.py exceptions
|
||||
python scripts/db_cli.py exceptions add -c include_fio -v "Тарасенко Александр Александрович"
|
||||
python scripts/db_cli.py exceptions add -c fio -v "Михалев Сергей Геннадьевич" -m "Уборщик"
|
||||
python scripts/db_cli.py exceptions del -c fio -v "Михалев Сергей Геннадьевич"
|
||||
python scripts/db_cli.py exceptions sync
|
||||
"""
|
||||
|
||||
|
||||
@@ -418,9 +468,16 @@ def main():
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
add_help=False
|
||||
)
|
||||
parser.add_argument('command', nargs='?', default=None, choices=['stats', 'snapshots', 'scud', 'absences', 'anomalies', 'rules', 'prompts', 'sessions', 'dump', 'snapshot', 'tools', 'context'], help="Основная команда")
|
||||
parser.add_argument('action', nargs='?', default=None, help="Дополнительное действие (например, 'del', 'purge') или session_id для контекста")
|
||||
parser.add_argument('param', nargs='?', default=None, help="Параметр (дата, ID снапшота, session_id, '--all' или имя файла)")
|
||||
parser.add_argument('command', nargs='?', default=None, choices=[
|
||||
'stats', 'snapshots', 'scud', 'absences', 'anomalies',
|
||||
'rules', 'prompts', 'sessions', 'dump', 'snapshot',
|
||||
'tools', 'context', 'exceptions'
|
||||
], help="Основная команда")
|
||||
parser.add_argument('action', nargs='?', default=None, help="Действие ('del', 'purge', 'add', 'sync') или дата/сессия")
|
||||
parser.add_argument('param', nargs='?', default=None, help="Параметр (дата, ID снапшота, session_id, имя файла)")
|
||||
parser.add_argument('-c', '--category', type=str, default=None, choices=['departments', 'positions', 'fio', 'position_keywords', 'include_fio'], help="Категория исключения")
|
||||
parser.add_argument('-v', '--value', type=str, default=None, help="Значение исключения (ФИО, отдел, должность)")
|
||||
parser.add_argument('-m', '--comment', type=str, default="", help="Комментарий к исключению")
|
||||
parser.add_argument('--snapshot', type=str, default=None, help="ID конкретного снапшота для инспекции")
|
||||
parser.add_argument('--export-xlsx', type=str, default=None, help="Экспорт среза СКУД в Excel-файл")
|
||||
parser.add_argument('--day', type=str, default=None, help="Удалить снапшоты за конкретный день (ДД.ММ.ГГГГ)")
|
||||
@@ -436,12 +493,13 @@ def main():
|
||||
if args.command == 'stats':
|
||||
print_stats()
|
||||
elif args.command == 'snapshots':
|
||||
date_val = args.param or args.action
|
||||
date_val = args.action or args.param
|
||||
print_snapshots_list(date_str=date_val)
|
||||
elif args.command == 'scud':
|
||||
inspect_scud(snapshot_id=args.snapshot, date_str=args.param, export_xlsx=args.export_xlsx)
|
||||
date_val = args.action or args.param
|
||||
inspect_scud(snapshot_id=args.snapshot, date_str=date_val, export_xlsx=args.export_xlsx)
|
||||
elif args.command == 'absences':
|
||||
date_val = args.param or args.action
|
||||
date_val = args.action or args.param
|
||||
print_absences(date_str=date_val)
|
||||
elif args.command == 'anomalies':
|
||||
print_anomalies()
|
||||
@@ -462,7 +520,7 @@ def main():
|
||||
sess_id = args.action if args.action else None
|
||||
print_chat_messages(session_id=sess_id, limit=args.limit)
|
||||
elif args.command == 'dump':
|
||||
filename = args.param if args.param else "db_dump_full.xlsx"
|
||||
filename = args.action or args.param or "db_dump_full.xlsx"
|
||||
dump_all_to_excel(filename)
|
||||
elif args.command == 'snapshot':
|
||||
if args.action == 'del':
|
||||
@@ -476,9 +534,34 @@ def main():
|
||||
else:
|
||||
print(f"\n[❌] Ошибка: Неизвестное действие '{args.action}' для команды snapshot.")
|
||||
print("Используйте: python scripts/db_cli.py snapshot del [ID или --day 'ДД.ММ.ГГГГ']\n")
|
||||
elif args.command == 'exceptions':
|
||||
if args.action == 'add':
|
||||
if not args.category or not args.value:
|
||||
print("\n[❌] Ошибка: Для добавления исключения укажите флаги -c/--category и -v/--value")
|
||||
print("Пример: python scripts/db_cli.py exceptions add -c include_fio -v \"Тарасенко Александр Александрович\"\n")
|
||||
return
|
||||
if add_exception_to_db(args.category, args.value, args.comment):
|
||||
print(f"\n[✓] Успешно добавлено исключение: [{args.category}] {args.value}\n")
|
||||
else:
|
||||
print(f"\n[❌] Ошибка добавления исключения [{args.category}] {args.value}\n")
|
||||
elif args.action in ['del', 'delete', 'remove']:
|
||||
if not args.category or not args.value:
|
||||
print("\n[❌] Ошибка: Для удаления исключения укажите флаги -c/--category и -v/--value")
|
||||
print("Пример: python scripts/db_cli.py exceptions del -c fio -v \"Михалев Сергей Геннадьевич\"\n")
|
||||
return
|
||||
if remove_exception_from_db(args.category, args.value):
|
||||
print(f"\n[✓] Успешно удалено исключение: [{args.category}] {args.value}\n")
|
||||
else:
|
||||
print(f"\n[⚠️] Запись не найдена в базе: [{args.category}] {args.value}\n")
|
||||
elif args.action == 'sync':
|
||||
sync_json_to_db()
|
||||
print("\n[✓] Синхронизация exceptions.json -> SQLite успешно завершена.\n")
|
||||
else:
|
||||
print_exceptions()
|
||||
else:
|
||||
print("\n[❌] Ошибка: Неизвестная команда.")
|
||||
print(HELP_TEXT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user