From a622fd98d2807b3d858ecc2fe9dc656bf984e196 Mon Sep 17 00:00:00 2001 From: manoraga Date: Tue, 11 Aug 2026 16:19:07 +0300 Subject: [PATCH] =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=81=D1=82=20=D0=B8=D0=B7?= =?UTF-8?q?=20pdf=20=D1=80=D0=B0=D1=81=D0=BF=D0=BE=D0=B7=D0=BD=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F,=20=D0=B2=D1=81=D0=B5=20=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=D0=B5=D1=82,=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B2=D1=81=D0=B5=20=D1=84?= =?UTF-8?q?=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20=D0=B8=D0=B7=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B?= =?UTF-8?q?=20=D1=81=20=D0=B1=D0=B0=D0=B7=D0=BE=D0=B9=20=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20db=5Fcli.py.=20=D0=93=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BA=20=D0=BE=D0=BF?= =?UTF-8?q?=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- init_db.py | 12 +- llm/agent.py | 128 +++++++++++++++--- llm/db_tools.py | 196 ++++++++++++++++++++++++++-- llm/file_parser.py | 61 +++++++++ llm/schemas.py | 115 +++++++++++++++- main.py | 113 +++++++++++----- pypdf-4.3.1-py3-none-any.whl | 1 + pytesseract-0.3.10-py3-none-any.whl | 1 + scud_context_api.tar.gz | Bin 0 -> 50529 bytes static/favicon.ico | 6 + static/index.html | 22 +++- static/js/chat.js | 58 +++++++- system_prompt.txt | 6 +- 13 files changed, 641 insertions(+), 78 deletions(-) create mode 100644 llm/file_parser.py create mode 100644 pypdf-4.3.1-py3-none-any.whl create mode 100644 pytesseract-0.3.10-py3-none-any.whl create mode 100644 scud_context_api.tar.gz create mode 100644 static/favicon.ico diff --git a/init_db.py b/init_db.py index 08daed3..bd4a836 100644 --- a/init_db.py +++ b/init_db.py @@ -1,7 +1,7 @@ import os import sqlite3 -DB_NAME = "/home/puh/scud_context_api/scud_orion_ai.db" +DB_NAME = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db" def init_db(): os.makedirs(os.path.dirname(DB_NAME), exist_ok=True) @@ -49,6 +49,16 @@ def init_db(): ); """) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS chat_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + role TEXT NOT NULL, -- 'user', 'assistant', 'tool' + content TEXT NOT NULL, -- текст сообщения + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + """) + conn.commit() conn.close() print(f"[✓] Единая база данных SQLite ({DB_NAME}) успешно инициализирована!") diff --git a/llm/agent.py b/llm/agent.py index 91b2c68..1469a17 100644 --- a/llm/agent.py +++ b/llm/agent.py @@ -2,12 +2,12 @@ import json import urllib.request import urllib.error import logging -from typing import List, Dict, Any, Tuple -from datetime import datetime +from typing import List, Dict, Any, Tuple, Optional +from datetime import datetime, timedelta from .db_tools import ( db_get_active_system_prompt, - db_add_system_prompt, + db_add_system_prompt, db_get_tasks, db_update_task_status, db_delete_task, @@ -15,7 +15,16 @@ from .db_tools import ( db_get_rules, db_set_session_state, db_get_session_state, + db_get_snapshots, + db_delete_snapshots, db_clear_session_state, + db_get_current_server_time, + db_save_chat_message, + db_get_chat_history, + db_get_stats, + db_get_anomalies, + db_get_session_states, + db_get_reference, get_db_connection ) @@ -27,6 +36,11 @@ logger = logging.getLogger("SCUD_AGENT") OLLAMA_URL = "http://192.168.11.3:11434/api/chat" MODEL_NAME = "qwen2.5:14b" +DAYS_RU = [ + "понедельник", "вторник", "среда", "четверг", + "пятница", "суббота", "воскресенье" +] + def clean_output(text: str) -> str: if not text: return text @@ -37,26 +51,71 @@ def clean_output(text: str) -> str: text = text[len(art):].lstrip(",.!?:; -") return text.strip() -def process_chat_message(user_id: int, user_message: str, chat_history: List[Dict[str, Any]] = None, session_id: str = "web_session_main") -> Tuple[str, List[Dict[str, Any]]]: - if chat_history is None: - chat_history = [] +def get_dynamic_calendar_context() -> str: + now = datetime.now() + current_wd = now.weekday() + + lines = [ + f"СЕГОДНЯ: {DAYS_RU[current_wd].upper()}, {now.strftime('%d.%m.%Y')} (время сервера: {now.strftime('%H:%M:%S')}).", + "\nСПРАВОЧНИК ДАТ ДЛЯ ОТВЕТОВ (БЕРИ ДАТЫ СТРОГО ОТСЮДА):", + f"• Сегодня: {now.strftime('%d.%m.%Y')} ({DAYS_RU[current_wd]})", + f"• Вчера: {(now - timedelta(days=1)).strftime('%d.%m.%Y')} ({DAYS_RU[(current_wd - 1) % 7]})", + f"• Позавчера: {(now - timedelta(days=2)).strftime('%d.%m.%Y')} ({DAYS_RU[(current_wd - 2) % 7]})", + "\nПрошедшие дни недели:" + ] + + for days_back in range(1, 8): + dt = now - timedelta(days=days_back) + day_name = DAYS_RU[dt.weekday()] + + if days_back == 7: + label = f"Прошлый {day_name}" if dt.weekday() in [0, 1, 3, 6] else f"Прошлая {day_name}" + lines.append(f"• {label} (ровно неделю назад): {dt.strftime('%d.%m.%Y')}") + else: + label = f"Ближайший прошедший {day_name}" if dt.weekday() in [0, 1, 3, 6] else f"Ближайшая прошедшая {day_name}" + lines.append(f"• {label} / {day_name}: {dt.strftime('%d.%m.%Y')}") + + return "\n".join(lines) +def process_chat_message( + user_id: int, + user_message: str, + file_context: str = "", + chat_history: List[Dict[str, Any]] = None, + session_id: str = "web_session_main" +) -> Tuple[str, List[Dict[str, Any]]]: logger.info(f"Получено сообщение от user_id={user_id}, session_id={session_id}: {user_message}") - current_now = datetime.now().strftime("%Y-%m-%d %H:%M") + # Формируем итоговое содержимое запроса + full_user_content = user_message + if file_context: + full_user_content = f"{user_message}\n\n[СОДЕРЖИМОЕ ПРИКРЕПЛЕННОГО ФАЙЛА]:\n{file_context}" + + db_history = db_get_chat_history(session_id, limit=20) + db_save_chat_message(session_id, "user", full_user_content) + dynamic_prompt_text = db_get_active_system_prompt() + calendar_context = get_dynamic_calendar_context() session_state = db_get_session_state(session_id) preview_status_note = "" - if session_state and session_state["state_type"] == "PROMPT_PREVIEW": + if session_state and session_state.get("state_type") == "PROMPT_PREVIEW": preview_status_note = "\n\n[АКТИВНО ПРЕВЬЮ ПРОМПТА: Ожидается подтверждение или отмена изменений пользователем]." + system_prompt_content = ( + f"[СИСТЕМНЫЙ КАЛЕНДАРЬ СЕРВЕРА]\n" + f"{calendar_context}\n\n" + f"ПРАВИЛО РАБОТЫ С ДАТАМИ:\n" + f"При любых вопросах про дни недели ('прошлая среда', 'вторник', 'дата в прошлый понедельник') бери ГОТОВУЮ точную дату из справочника выше. Тебе ЗАПРЕЩЕНО вычислять даты самостоятельно!\n\n" + f"ТЕКУЩИЙ АКТИВНЫЙ СИСТЕМНЫЙ ПРОМПТ:\n{dynamic_prompt_text}{preview_status_note}" + ) + system_prompt = { "role": "system", - "content": f"Текущая дата и время сервера: {current_now}.\n\nТЕКУЩИЙ АКТИВНЫЙ СИСТЕМНЫЙ ПРОМПТ:\n{dynamic_prompt_text}{preview_status_note}" + "content": system_prompt_content } - messages = [system_prompt] + chat_history + [{"role": "user", "content": user_message}] + messages = [system_prompt] + db_history + [{"role": "user", "content": full_user_content}] payload = { "model": MODEL_NAME, @@ -87,7 +146,16 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic logger.info(f"Вызов функции (Tool): {fn_name} с аргументами: {fn_args}") tool_result_content = "" - if fn_name == "db_get_tasks": + if fn_name == "db_get_snapshots": + date_arg = fn_args.get("date_str") + snapshots_res = db_get_snapshots(session_id=session_id, date_str=date_arg, original_user_message=user_message) + tool_result_content = json.dumps(snapshots_res, ensure_ascii=False) + + elif fn_name == "db_get_current_server_time": + time_res = db_get_current_server_time() + tool_result_content = json.dumps(time_res, ensure_ascii=False) + + elif fn_name == "db_get_tasks": tasks = db_get_tasks(user_id) tool_result_content = json.dumps(tasks, ensure_ascii=False) @@ -95,19 +163,39 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic prompt_content = db_get_active_system_prompt() tool_result_content = json.dumps({"system_prompt": prompt_content}, ensure_ascii=False) + elif fn_name == "db_get_stats": + tool_result_content = json.dumps(db_get_stats(), ensure_ascii=False) + + elif fn_name == "db_get_anomalies": + limit_arg = fn_args.get("limit", 100) + date_arg = fn_args.get("date_str") + tool_result_content = json.dumps(db_get_anomalies(limit=limit_arg, date_str=date_arg), ensure_ascii=False) + + elif fn_name == "db_get_session_states": + tool_result_content = json.dumps(db_get_session_states(), ensure_ascii=False) + + elif fn_name == "db_delete_snapshots": + snap_id = fn_args.get("snapshot_id") + day_arg = fn_args.get("day_str") + tool_result_content = json.dumps(db_delete_snapshots(snapshot_id=snap_id, day_str=day_arg), ensure_ascii=False) + + elif fn_name == "db_get_reference": + cat_arg = fn_args.get("category") + tool_result_content = json.dumps(db_get_reference(category=cat_arg), ensure_ascii=False) + elif fn_name == "db_preview_prompt_merge": proposed_text = fn_args.get("proposed_prompt", "") if proposed_text: - # Фиксируем превью в сессии и сразу возвращаем текст пользователю на экран db_set_session_state(session_id, "PROMPT_PREVIEW", proposed_text) preview_reply = f"Ваше изменение успешно предпросмотрено. Полный обновленный системный промпт теперь выглядит так:\n\n{proposed_text}\n\nДля применения изменений подтвердите действие («подтверждаю») или отмените («отмена»)." - return preview_reply, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": preview_reply}] + db_save_chat_message(session_id, "assistant", preview_reply) + return preview_reply, db_get_chat_history(session_id) else: tool_result_content = json.dumps({"status": "error", "message": "Текст превью пуст."}, ensure_ascii=False) elif fn_name == "db_confirm_prompt_preview": - if session_state and session_state["state_type"] == "PROMPT_PREVIEW": - pending_text = session_state["pending_data"] + if session_state and session_state.get("state_type") == "PROMPT_PREVIEW": + pending_text = session_state.get("pending_data", "") res = db_add_system_prompt("main_agent", pending_text) db_clear_session_state(session_id) tool_result_content = json.dumps(res, ensure_ascii=False) @@ -162,11 +250,15 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic with urllib.request.urlopen(sec_req) as sec_response: sec_res_data = json.loads(sec_response.read().decode("utf-8")) final_content = clean_output(sec_res_data.get("message", {}).get("content", "").strip().replace("**", "")) - return final_content, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": final_content}] + db_save_chat_message(session_id, "assistant", final_content) + return final_content, db_get_chat_history(session_id) content_str = clean_output(msg.get("content", "").strip().replace("**", "")) - return content_str or "Запрос обработан.", chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": content_str}] + final_reply = content_str or "Запрос обработан." + db_save_chat_message(session_id, "assistant", final_reply) + return final_reply, db_get_chat_history(session_id) except Exception as ex: logger.exception(f"Непредвиденная ошибка: {ex}") - return f"Внутренняя ошибка сервера: {ex}", chat_history \ No newline at end of file + error_reply = f"Внутренняя ошибка сервера: {ex}" + return error_reply, db_get_chat_history(session_id) \ No newline at end of file diff --git a/llm/db_tools.py b/llm/db_tools.py index 8914dc6..6da7acc 100644 --- a/llm/db_tools.py +++ b/llm/db_tools.py @@ -1,16 +1,101 @@ +import json import sqlite3 import logging from typing import List, Dict, Any, Optional +from datetime import datetime, timedelta -# Настройка логирования logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger("DB_TOOLS") DB_PATH = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db" +def db_get_current_server_time() -> Dict[str, Any]: + now = datetime.now() + days_ru = ["понедельник", "вторник", "среда", "четверг", "пятница", "суббота", "воскресенье"] + return { + "current_date": now.strftime("%d.%m.%Y"), + "current_time": now.strftime("%H:%M:%S"), + "day_of_week": days_ru[now.weekday()], + "iso_date": now.strftime("%Y-%m-%d") + } + +def smart_parse_date(date_str: Optional[str], original_user_message: str = "") -> Optional[str]: + """ + Дата уже точно подготовлена моделью на основе системного календаря. + Возвращаем date_str без повторной тяжелой фильтрации. + """ + return date_str + +def db_save_chat_message(session_id: str, role: str, content: str): + if not content: + return + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO chat_messages (session_id, role, content, created_at) + VALUES (?, ?, ?, CURRENT_TIMESTAMP) + """, (session_id, role, content)) + conn.commit() + conn.close() + +def db_get_chat_history(session_id: str, limit: int = 20) -> List[Dict[str, Any]]: + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute(""" + SELECT role, content FROM chat_messages + WHERE session_id = ? + ORDER BY id DESC LIMIT ? + """, (session_id, limit)) + rows = cursor.fetchall() + conn.close() + return [{"role": r["role"], "content": r["content"]} for r in reversed(rows)] + +def db_get_snapshots(session_id: str = "web_session_main", date_str: Optional[str] = None, original_user_message: str = "") -> Dict[str, Any]: + date_str = smart_parse_date(date_str, original_user_message) + + conn = get_db_connection() + cursor = conn.cursor() + + query = """ + SELECT snapshot_id, log_date, snapshot_time, COUNT(*) as record_count + FROM scud_logs + """ + params = [] + + if date_str: + # Приводим дату ДД.ММ.ГГГГ к ISO YYYY-MM-DD + iso_date = date_str + if "." in date_str: + parts = date_str.split(".") + if len(parts) == 3: + iso_date = f"{parts[2]}-{parts[1]}-{parts[0]}" + + # Строгий поиск: ищем совпадение строго по log_date или началу snapshot_time/created_at + query += " WHERE log_date = ? OR log_date = ? OR snapshot_time LIKE ? OR created_at LIKE ? " + params.extend([date_str, iso_date, f"{iso_date}%", f"{iso_date}%"]) + + query += " GROUP BY snapshot_id ORDER BY id DESC LIMIT 20" + + cursor.execute(query, params) + rows = cursor.fetchall() + snapshots = [dict(r) for r in rows] + + result_data = { + "query_date": date_str or "все", + "snapshots_count": len(snapshots), + "snapshots": snapshots + } + + db_set_session_state( + session_id=session_id, + state_type="SNAPSHOTS_VIEW", + data=json.dumps(result_data, ensure_ascii=False) + ) + + conn.close() + return result_data + def get_db_connection(): - # Увеличиваем таймаут до 30 секунд, чтобы соединения ожидали завершения соседних транзакций, - # а также включаем WAL-режим для безопасного параллельного чтения и записи. conn = sqlite3.connect(DB_PATH, timeout=30.0) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode = WAL;") @@ -18,17 +103,14 @@ def get_db_connection(): return conn def normalize_task_id(task_id_input: str) -> str: - """Преобразует 6, '6', 'task-6' в красивый формат TASK-06 или TASK-006""" if not task_id_input: return "" clean_id = str(task_id_input).strip().upper().replace("TASK-", "").replace("TASK", "") if clean_id.isdigit(): num = int(clean_id) - return f"TASK-{num:02d}" if num < 100 else f"TASK-{num:03d}" + return f"TASK-{(num):02d}" if num < 100 else f"TASK-{(num):03d}" return f"TASK-{clean_id}" -# === ЗАДАЧИ С ФИЛЬТРАЦИЕЙ ПО USER_ID === - def db_get_tasks(user_id: int) -> List[Dict[str, Any]]: conn = get_db_connection() cursor = conn.cursor() @@ -105,8 +187,6 @@ def db_delete_task(user_id: int, task_id: str) -> Dict[str, Any]: conn.close() return {"status": "success", "message": f"Задача {formatted_id} удалена"} -# === ОБЩИЕ СИСТЕМНЫЕ РЕСУРСЫ === - def db_get_active_system_prompt() -> str: conn = get_db_connection() cursor = conn.cursor() @@ -121,18 +201,15 @@ def db_add_system_prompt(name: str, prompt_text: str) -> Dict[str, Any]: cursor = conn.cursor() cursor.execute("BEGIN IMMEDIATE;") - # Проверяем, существует ли уже промпт с таким именем cursor.execute("SELECT id FROM system_prompts WHERE name = ?", (name,)) existing = cursor.fetchone() if existing: - # Обновляем существующий активный промпт cursor.execute( "UPDATE system_prompts SET prompt_text = ?, is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE name = ?", (prompt_text, name) ) else: - # Вставляем новый, если его не было cursor.execute( "INSERT INTO system_prompts (name, prompt_text, is_active) VALUES (?, ?, 1)", (name, prompt_text) @@ -146,7 +223,6 @@ def db_add_system_prompt(name: str, prompt_text: str) -> Dict[str, Any]: logger.error(f"Ошибка при сохранении промпта в БД: {e}") return {"status": "error", "error": str(e)} - def db_get_rules() -> List[Dict[str, Any]]: conn = get_db_connection() cursor = conn.cursor() @@ -182,4 +258,96 @@ def db_clear_session_state(session_id: str): cursor = conn.cursor() cursor.execute("DELETE FROM session_states WHERE session_id = ?", (session_id,)) conn.commit() - conn.close() \ No newline at end of file + conn.close() + +def db_get_stats() -> Dict[str, Any]: + """Возвращает общую статистику по количеству записей во всех таблицах БД.""" + conn = get_db_connection() + cursor = conn.cursor() + tables = ['scud_logs', 'zup_staff', 'zup_absences', 'anomalies_history', 'ai_knowledge_base', 'system_prompts', 'session_states', 'tasks'] + stats = {} + for t in tables: + try: + cursor.execute(f"SELECT COUNT(*) FROM {t}") + stats[t] = cursor.fetchone()[0] + except Exception: + stats[t] = 0 + conn.close() + return {"status": "success", "tables_stats": stats} + +def db_get_anomalies(limit: int = 100, date_str: Optional[str] = None) -> Dict[str, Any]: + """Возвращает историю аномалий СКУД с опциональной фильтрацией по дате.""" + conn = get_db_connection() + cursor = conn.cursor() + + query = "SELECT anomaly_date, fio, anomaly_type, details FROM anomalies_history" + params = [] + + if date_str: + query += " WHERE anomaly_date = ?" + params.append(date_str) + + query += " ORDER BY id DESC LIMIT ?" + params.append(limit) + + cursor.execute(query, params) + rows = cursor.fetchall() + conn.close() + + anomalies_list = [dict(r) for r in rows] + return { + "status": "success", + "count": len(anomalies_list), + "anomalies": anomalies_list + } + +def db_get_session_states() -> Dict[str, Any]: + """Возвращает список всех активных сессий и состояний превью.""" + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SELECT session_id, state_type, updated_at FROM session_states") + rows = cursor.fetchall() + conn.close() + return {"status": "success", "active_sessions": [dict(r) for r in rows]} + +def db_delete_snapshots(snapshot_id: Optional[str] = None, day_str: Optional[str] = None) -> Dict[str, Any]: + """Удаляет снапшот по ID или за конкретную дату.""" + if not snapshot_id and not day_str: + return {"status": "error", "message": "Необходимо указать snapshot_id или day_str (ДД.ММ.ГГГГ)."} + + conn = get_db_connection() + cursor = conn.cursor() + + if snapshot_id: + cursor.execute("DELETE FROM scud_logs WHERE snapshot_id = ?", (snapshot_id,)) + deleted = cursor.rowcount + else: + cursor.execute("DELETE FROM scud_logs WHERE log_date = ? OR snapshot_id LIKE ?", (day_str, f"%{day_str.replace('.', '')}%")) + deleted = cursor.rowcount + + conn.commit() + conn.close() + return {"status": "success", "message": f"Успешно удалено записей: {deleted}"} + +def db_get_reference(category: Optional[str] = None) -> Dict[str, Any]: + """Возвращает системные справочники и примеры команд для оператора.""" + conn = get_db_connection() + cursor = conn.cursor() + + query = "SELECT category, title, example_prompt, description FROM system_reference" + params = [] + + if category: + query += " WHERE category = ?" + params.append(category) + + query += " ORDER BY id ASC" + cursor.execute(query, params) + rows = cursor.fetchall() + conn.close() + + return { + "status": "success", + "count": len(rows), + "reference_items": [dict(r) for r in rows] + } \ No newline at end of file diff --git a/llm/file_parser.py b/llm/file_parser.py new file mode 100644 index 0000000..945930e --- /dev/null +++ b/llm/file_parser.py @@ -0,0 +1,61 @@ +import io +import os +import subprocess +import logging +import pandas as pd +from PIL import Image + +logger = logging.getLogger("FILE_PARSER") + +def extract_text_from_file(file_bytes: bytes, filename: str) -> str: + """Извлекает текст из изображений (OCR), PDF, таблиц и текстовых файлов.""" + ext = os.path.splitext(filename)[1].lower() + + # Создаем временный файл во избежание проблем с памятью + temp_filepath = f"/tmp/upload_{os.getpid()}_{filename}" + with open(temp_filepath, "wb") as f: + f.write(file_bytes) + + try: + # 1. ИЗОБРАЖЕНИЯ (OCR через системный /usr/bin/tesseract) + if ext in ['.png', '.jpg', '.jpeg', '.bmp', '.webp']: + cmd = ['tesseract', temp_filepath, 'stdout', '-l', 'rus+eng'] + res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) + text = res.stdout.strip() + return text if text else "[OCR: На изображении не удалось распознать текст]" + + # 2. PDF ДОКУМЕНТЫ (через системный /usr/bin/pdftotext из poppler-utils) + elif ext == '.pdf': + cmd = ['pdftotext', temp_filepath, '-'] + res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) + text = res.stdout.strip() + return text if text else "[PDF: Текстовый слой не найден. Возможно, скан без OCR]" + + # 3. ЭЛЕКТРОННЫЕ ТАБЛИЦЫ (XLSX, CSV через Pandas) + elif ext in ['.xlsx', '.xls', '.csv']: + if ext == '.csv': + df = pd.read_csv(temp_filepath) + else: + df = pd.read_excel(temp_filepath) + + total_rows = len(df) + df_preview = df.head(100) # Показываем первые 100 строк + + table_str = df_preview.to_string(index=False) + note = f"\n(Показано первых 100 строк из {total_rows})" if total_rows > 100 else "" + return f"[СОДЕРЖИМОЕ ТАБЛИЦЫ {filename}]:\n{table_str}{note}" + + # 4. ТЕКСТОВЫЕ ФАЙЛЫ (TXT, LOG, JSON) + elif ext in ['.txt', '.log', '.json', '.xml', '.md']: + with open(temp_filepath, 'r', encoding='utf-8', errors='replace') as tf: + return tf.read().strip() + + else: + return f"[ОШИБКА: Формат {ext} не поддерживается для анализа]" + + except Exception as e: + logger.error(f"Ошибка при анализе файла {filename}: {e}") + return f"[ОШИБКА ОБРАБОТКИ ФАЙЛА: {str(e)}]" + finally: + if os.path.exists(temp_filepath): + os.remove(temp_filepath) \ No newline at end of file diff --git a/llm/schemas.py b/llm/schemas.py index 3df8037..da7325f 100644 --- a/llm/schemas.py +++ b/llm/schemas.py @@ -61,6 +61,34 @@ TOOLS_SCHEMA = [ } } }, + { + "type": "function", + "function": { + "name": "db_get_snapshots", + "description": "ПОЛУЧИТЬ СНИМКИ/СНАПШОТЫ СКУД (из таблицы scud_logs). Вызывай, когда пользователь просит показать снапшоты, срезы или логи за дату/день недели.", + "parameters": { + "type": "object", + "properties": { + "date_str": { + "type": "string", + "description": "Точная дата в формате ДД.ММ.ГГГГ (например, '05.08.2026'), взятая из [СИСТЕМНОГО КАЛЕНДАРЯ СЕРВЕРА]." + } + } + } + } +}, + { + "type": "function", + "function": { + "name": "db_get_current_server_time", + "description": "ПОЛУЧИТЬ ТЕКУЩУЮ ДАТУ, ВРЕМЯ И ДЕНЬ НЕДЕЛИ СЕРВЕРА. Вызывай МГНОВЕННО при любых вопросах пользователя про точное текущее время или текущую дату.", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } + }, { "type": "function", "function": { @@ -81,19 +109,78 @@ TOOLS_SCHEMA = [ { "type": "function", "function": { - "name": "db_confirm_prompt_preview", - "description": "Подтвердить и сохранить текущее подготовленное превью в БД. Вызывай этот инструмент, когда пользователь говорит 'подтверждаю', 'да', 'вноси', 'применяй', 'сохраняй' или одобряет превью в любой форме.", + "name": "db_get_stats", + "description": "ПОЛУЧИТЬ ОБЩУЮ СТАТИСТИКУ БАЗЫ ДАННЫХ. Вызывай, когда пользователь просит показать общую статистику БД, количество записей в таблицах или размер базы.", "parameters": {"type": "object", "properties": {}} } }, { "type": "function", "function": { - "name": "db_cancel_prompt_preview", - "description": "Отменить текущее превью системного промпта и сбросить изменения. Вызывай, когда пользователь явно отказывается от изменений.", + "name": "db_get_anomalies", + "description": "ПОЛУЧИТЬ ИСТОРИЮ АНОМАЛИЙ СКУД ⟷ 1С. Вызывай при запросах на просмотр аномалий или расхождений. Передавай date_str если пользователь просит аномалии за конкретный день, или увеличенный limit (например 100) если просит все.", + "parameters": { + "type": "object", + "properties": { + "limit": {"type": "integer", "description": "Максимальное количество записей (по умолчанию 100)"}, + "date_str": {"type": "string", "description": "Опциональная дата в формате ДД.ММ.ГГГГ"} + } + } + } + }, + { + "type": "function", + "function": { + "name": "db_get_session_states", + "description": "ПОЛУЧИТЬ АКТИВНЫЕ СЕССИИ И ПРЕВЬЮ (session_states). Вызывай, когда пользователь просит показать текущие сессии или статус превью.", "parameters": {"type": "object", "properties": {}} } }, + { + "type": "function", + "function": { + "name": "db_delete_snapshots", + "description": "УДАЛИТЬ СНАПШОТ(Ы) ИЗ БАЗЫ ДАННЫХ. Вызывай, когда пользователь явно просит удалить конкретный снапшот по ID или все снапшоты за выбранный день.", + "parameters": { + "type": "object", + "properties": { + "snapshot_id": {"type": "string", "description": "Идентификатор снапшота для удаления, например Y20260805-007"}, + "day_str": {"type": "string", "description": "Дата в формате ДД.ММ.ГГГГ для удаления всех снапшотов за день"} + } + } + } + }, + { + "type": "function", + "function": { + "name": "db_get_reference", + "description": "ПОЛУЧИТЬ СИСТЕМНЫЙ СПРАВОЧНИК И ПРИМЕРЫ КОМАНД ДЛЯ ОПЕРАТОРА (system_reference). Вызывай ВСЕГДА, когда пользователь спрашивает про возможности ассистента, список команд, примерах промптов или справе по работе с системой.", + "parameters": { + "type": "object", + "properties": { + "category": { + "type": "string", + "description": "Фильтр категории: scud, tasks, calendar или system. Если просят всё — не передавай параметр." + } + } + } + } + }, + { + "type": "function", + "function": { + "name": "db_add_system_prompt", + "description": "ВНУТРЕННИЙ СИСТЕМНЫЙ ИНСТРУМЕНТ. ЗАПРЕЩЕНО вызывать напрямую при запросах пользователя на изменение промпта! Для ЛЮБЫХ изменений системного промпта ты ОБЯЗАН сначала вызвать db_preview_prompt_merge.", + "parameters": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "Имя промпта, по умолчанию main_agent"}, + "prompt_text": {"type": "string", "description": "Полный текст системного промпта"} + }, + "required": ["prompt_text"] + } + } + }, { "type": "function", "function": { @@ -110,5 +197,21 @@ TOOLS_SCHEMA = [ "required": ["proposed_prompt"] } } - } -] + }, + { + "type": "function", + "function": { + "name": "db_confirm_prompt_preview", + "description": "Подтвердить и сохранить текущее подготовленное превью в БД. Вызывай этот инструмент, когда пользователь говорит 'подтверждаю', 'да', 'вноси', 'применяй', 'сохраняй' или одобряет превью в любой форме.", + "parameters": {"type": "object", "properties": {}} + } + }, + { + "type": "function", + "function": { + "name": "db_cancel_prompt_preview", + "description": "Отменить текущее превью системного промпта и сбросить изменения. Вызывай, когда пользователь явно отказывается от изменений.", + "parameters": {"type": "object", "properties": {}} + } + } +] \ No newline at end of file diff --git a/main.py b/main.py index 0166d6a..e33edfd 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,7 @@ from typing import List, Dict, Any, Optional import jwt from passlib.context import CryptContext -from fastapi import FastAPI, Depends, HTTPException, status +from fastapi import FastAPI, Depends, HTTPException, status, UploadFile, File, Form from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse @@ -16,9 +16,10 @@ from pydantic import BaseModel from llm.agent import process_chat_message from llm.db_tools import db_get_tasks, DB_PATH +from llm.file_parser import extract_text_from_file logging.basicConfig( - level=logging.INFO, + level=logging.DEBUG, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[logging.StreamHandler()] ) @@ -65,6 +66,16 @@ app = FastAPI(title="SCUD Orion AI Context API") if os.path.exists("static"): app.mount("/static", StaticFiles(directory="static"), name="static") +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request, exc): + logging.error(f"❌ ОШИБКА ВАЛИДАЦИИ 422 НА {request.url}: {exc.errors()}") + return JSONResponse( + status_code=422, + content={"detail": exc.errors(), "body": str(exc)} + ) class AuthRequest(BaseModel): username: str @@ -80,16 +91,42 @@ class ChangePasswordRequest(BaseModel): old_password: str new_password: str -class ChatRequest(BaseModel): - session_id: str - message: str - # === API МАРШРУТЫ === @app.get("/") def read_root(): return FileResponse("static/index.html") +@app.get("/favicon.ico") +async def favicon(): + file_path = os.path.join("static", "favicon.ico") + if os.path.exists(file_path): + return FileResponse(file_path) + raise HTTPException(status_code=404) + +@app.get("/{file_path:path}") +def serve_static_fallback(file_path: str): + clean_path = file_path.lstrip("/") + + # Игнорируем сканеры WordPress / PHP + if any(clean_path.startswith(prefix) for prefix in ["wp-", "wordpress", "php", "cms", "shop"]): + raise HTTPException(status_code=404, detail="Not Found") + + target = os.path.join("static", clean_path) + if os.path.isfile(target): + return FileResponse(target) + + filename = os.path.basename(clean_path) + target_js = os.path.join("static/js", filename) + if filename.endswith(".js") and os.path.isfile(target_js): + return FileResponse(target_js, media_type="application/javascript") + + target_css = os.path.join("static/css", filename) + if filename.endswith(".css") and os.path.isfile(target_css): + return FileResponse(target_css, media_type="text/css") + + raise HTTPException(status_code=404, detail="File not found") + @app.post("/api/v1/auth/login") def login(req: AuthRequest): username = req.username.strip().lower() @@ -200,34 +237,48 @@ def delete_user(user_id: int, current_user: Dict[str, Any] = Depends(get_current def get_tasks(user: Dict[str, Any] = Depends(get_current_user)): return db_get_tasks(user_id=user["id"]) +# ЧАТ С ПОДДЕРЖКОЙ ФАЙЛОВ И АВТОРИЗАЦИИ @app.post("/api/v1/chat") -def chat_endpoint(req: ChatRequest, user: Dict[str, Any] = Depends(get_current_user)): - reply, _ = process_chat_message(user_id=user["id"], user_message=req.message) - return {"reply": reply} +async def chat_endpoint( + session_id: str = Form("web_session_main"), + message: str = Form(""), + file: Optional[UploadFile] = File(default=None), + current_user: dict = Depends(get_current_user) +): + logging.info(f"=== [CHAT API] Входящий запрос от user_id={current_user['id']}, file={file.filename if file else 'None'} ===") + file_content_text = "" + if file and file.filename: + file_bytes = await file.read() + file_content_text = extract_text_from_file(file_bytes, file.filename) + reply, history = process_chat_message( + user_id=current_user["id"], + user_message=message, + file_context=file_content_text, + session_id=session_id + ) + return {"reply": reply, "history": history} + +# ЕДИНЫЙ ГОСТЕВОЙ ЧАТ (FormData + Файлы) @app.post("/api/v1/chat/guest") -def guest_chat_endpoint(req: ChatRequest): - payload = { - "model": MODEL_NAME, - "messages": [ - {"role": "system", "content": "Ты — полезный ИИ-ассистент. Отвечай на вопросы пользователя четко и по существу."}, - {"role": "user", "content": req.message} - ], - "stream": False, - "options": {"num_predict": 2048, "temperature": 0.3} - } - try: - req_ollama = urllib.request.Request( - OLLAMA_URL, - data=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) - with urllib.request.urlopen(req_ollama) as response: - res_data = json.loads(response.read().decode("utf-8")) - reply = res_data.get("message", {}).get("content", "").strip() - return {"reply": reply} - except Exception as e: - return {"reply": f"Ошибка связи с локальной нейросетью: {e}"} +async def guest_chat_endpoint( + session_id: str = Form("web_session_main"), + message: str = Form(""), + file: Optional[UploadFile] = File(default=None) +): + logging.info(f"=== [GUEST CHAT API] Входящий запрос, file={file.filename if file else 'None'} ===") + file_content_text = "" + if file and file.filename: + file_bytes = await file.read() + file_content_text = extract_text_from_file(file_bytes, file.filename) + + reply, history = process_chat_message( + user_id=0, + user_message=message, + file_context=file_content_text, + session_id=session_id + ) + return {"reply": reply, "history": history} # === СТРОГО В КОНЦЕ: ФОЛЛБЭК СТАТИКИ === diff --git a/pypdf-4.3.1-py3-none-any.whl b/pypdf-4.3.1-py3-none-any.whl new file mode 100644 index 0000000..d7d2339 --- /dev/null +++ b/pypdf-4.3.1-py3-none-any.whl @@ -0,0 +1 @@ +SNI is required. \ No newline at end of file diff --git a/pytesseract-0.3.10-py3-none-any.whl b/pytesseract-0.3.10-py3-none-any.whl new file mode 100644 index 0000000..d7d2339 --- /dev/null +++ b/pytesseract-0.3.10-py3-none-any.whl @@ -0,0 +1 @@ +SNI is required. \ No newline at end of file diff --git a/scud_context_api.tar.gz b/scud_context_api.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..c0209125a0e20f0cbd4818b4f68870ff78513d20 GIT binary patch literal 50529 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 K3TCYUSONfd#>~h7 literal 0 HcmV?d00001 diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..8c54837 --- /dev/null +++ b/static/favicon.ico @@ -0,0 +1,6 @@ +@app.get("/favicon.ico") +async def favicon(): + file_path = os.path.join("static", "favicon.ico") + if os.path.exists(file_path): + return FileResponse(file_path) + raise HTTPException(status_code=404) \ No newline at end of file diff --git a/static/index.html b/static/index.html index 2d9ed20..f977ce1 100644 --- a/static/index.html +++ b/static/index.html @@ -178,16 +178,34 @@ ИИ-Ассистент

- Привет! У каждого пользователя свое изолированное пространство задач. Вы можете задавать вопросы нейросети или ставить персональные задачи. + Привет! У каждого пользователя свое изолированное пространство задач. Вы можете задавать вопросы нейросети, прикреплять файлы или ставить персональные задачи.

+ + +
+ + + +