текст из pdf распознается, все работает, добавлены все функции из файла работы с базой данных db_cli.py. Готовность к оптимизации интерфейса.
This commit is contained in:
+11
-1
@@ -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}) успешно инициализирована!")
|
||||
|
||||
+110
-18
@@ -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
|
||||
error_reply = f"Внутренняя ошибка сервера: {ex}"
|
||||
return error_reply, db_get_chat_history(session_id)
|
||||
+182
-14
@@ -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()
|
||||
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]
|
||||
}
|
||||
@@ -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)
|
||||
+109
-6
@@ -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": {}}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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}
|
||||
|
||||
# === СТРОГО В КОНЦЕ: ФОЛЛБЭК СТАТИКИ ===
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
SNI is required.
|
||||
@@ -0,0 +1 @@
|
||||
SNI is required.
|
||||
Binary file not shown.
@@ -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)
|
||||
+20
-2
@@ -178,16 +178,34 @@
|
||||
<i class="fa-solid fa-robot mr-1"></i> ИИ-Ассистент
|
||||
</p>
|
||||
<p class="text-slate-800 text-xs sm:text-sm leading-relaxed">
|
||||
Привет! У каждого пользователя свое изолированное пространство задач. Вы можете задавать вопросы нейросети или ставить персональные задачи.
|
||||
Привет! У каждого пользователя свое изолированное пространство задач. Вы можете задавать вопросы нейросети, прикреплять файлы или ставить персональные задачи.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Превью прикрепленного файла -->
|
||||
<div id="file-preview-container" class="hidden px-4 py-2 bg-slate-100 border-t border-slate-200 flex items-center justify-between text-xs text-slate-700">
|
||||
<div class="flex items-center gap-2 truncate">
|
||||
<i class="fa-solid fa-paperclip text-indigo-600"></i>
|
||||
<span id="file-name-display" class="font-medium truncate">file.pdf</span>
|
||||
<span id="file-size-display" class="text-slate-400 text-[10px]">(0 KB)</span>
|
||||
</div>
|
||||
<button type="button" onclick="clearAttachedFile()" class="text-slate-400 hover:text-red-500 p-1 transition">
|
||||
<i class="fa-solid fa-xmark text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-2.5 pb-6 bg-white border-t border-slate-200 shrink-0 z-10 shadow-lg">
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Скрытый инпут и кнопка прикрепления файла -->
|
||||
<input type="file" id="file-input" class="hidden" onchange="handleFileSelect(event)" accept=".png,.jpg,.jpeg,.pdf,.txt,.csv,.xlsx">
|
||||
<button type="button" onclick="document.getElementById('file-input').click()" class="text-slate-500 hover:text-indigo-600 p-2 rounded-xl transition" title="Прикрепить файл">
|
||||
<i class="fa-solid fa-paperclip text-lg"></i>
|
||||
</button>
|
||||
|
||||
<div class="flex-1 bg-slate-100 border border-slate-300 rounded-2xl px-3 py-1.5 focus-within:border-indigo-600 focus-within:bg-white transition">
|
||||
<textarea id="user-input" rows="1" autocomplete="off" autocorrect="off" spellcheck="false"
|
||||
placeholder="Команда или вопрос..."
|
||||
placeholder="Команда, вопрос или описание файла..."
|
||||
class="w-full bg-transparent text-slate-900 text-sm focus:outline-none resize-none overflow-y-auto max-h-[80px] leading-normal no-scrollbar"></textarea>
|
||||
</div>
|
||||
<button type="button" id="send-btn" onclick="sendMessage()" class="bg-indigo-600 active:bg-indigo-800 text-white font-semibold px-3.5 py-2.5 rounded-2xl text-xs sm:text-sm transition flex items-center justify-center gap-1.5 shrink-0 shadow-sm">
|
||||
|
||||
+53
-5
@@ -1,3 +1,33 @@
|
||||
let selectedFile = null;
|
||||
|
||||
function handleFileSelect(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (file.size > 15 * 1024 * 1024) {
|
||||
alert("Файл слишком большой. Максимальный размер: 15 МБ");
|
||||
e.target.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
selectedFile = file;
|
||||
const fileNameEl = document.getElementById("file-name-display");
|
||||
const fileSizeEl = document.getElementById("file-size-display");
|
||||
const previewContainer = document.getElementById("file-preview-container");
|
||||
|
||||
if (fileNameEl) fileNameEl.innerText = file.name;
|
||||
if (fileSizeEl) fileSizeEl.innerText = `(${(file.size / 1024).toFixed(1)} KB)`;
|
||||
if (previewContainer) previewContainer.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function clearAttachedFile() {
|
||||
selectedFile = null;
|
||||
const fileInput = document.getElementById("file-input");
|
||||
const previewContainer = document.getElementById("file-preview-container");
|
||||
if (fileInput) fileInput.value = "";
|
||||
if (previewContainer) previewContainer.classList.add("hidden");
|
||||
}
|
||||
|
||||
async function sendMessage(e) {
|
||||
if (e && e.preventDefault) e.preventDefault();
|
||||
|
||||
@@ -8,13 +38,19 @@ async function sendMessage(e) {
|
||||
if (!input || !chatWindow) return;
|
||||
const text = input.value.trim();
|
||||
|
||||
if (!text) return;
|
||||
if (!text && !selectedFile) return;
|
||||
|
||||
let userDisplayHtml = escapeHtml(text);
|
||||
if (selectedFile) {
|
||||
userDisplayHtml = `<div class="font-bold border-b border-indigo-400/40 pb-1 mb-1 text-[11px] flex items-center gap-1.5">
|
||||
<i class="fa-solid fa-file"></i> ${escapeHtml(selectedFile.name)}
|
||||
</div>` + userDisplayHtml;
|
||||
}
|
||||
|
||||
// Вывод сообщения пользователя
|
||||
const userMsgHtml = `
|
||||
<div class="flex justify-end mb-3">
|
||||
<div class="bg-indigo-600 text-white rounded-2xl px-4 py-2.5 max-w-2xl text-xs sm:text-sm shadow-sm">
|
||||
${escapeHtml(text)}
|
||||
${userDisplayHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -33,7 +69,17 @@ async function sendMessage(e) {
|
||||
const isGuest = typeof IS_GUEST !== 'undefined' ? IS_GUEST : (localStorage.getItem("scud_is_guest") === "true");
|
||||
|
||||
const endpoint = isGuest ? "/api/v1/chat/guest" : "/api/v1/chat";
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
|
||||
// Формируем единый FormData без дублирования
|
||||
const formData = new FormData();
|
||||
formData.append("session_id", "web_session_main");
|
||||
formData.append("message", text || "Проанализируй прикрепленный файл");
|
||||
|
||||
if (selectedFile instanceof File) {
|
||||
formData.append("file", selectedFile, selectedFile.name);
|
||||
}
|
||||
|
||||
const headers = {};
|
||||
if (!isGuest && token) {
|
||||
headers["Authorization"] = "Bearer " + token;
|
||||
}
|
||||
@@ -42,7 +88,7 @@ async function sendMessage(e) {
|
||||
const res = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: JSON.stringify({ session_id: "web_session_main", message: text })
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (res.status === 401 && !isGuest) {
|
||||
@@ -65,6 +111,8 @@ async function sendMessage(e) {
|
||||
chatWindow.insertAdjacentHTML("beforeend", botMsgHtml);
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight;
|
||||
|
||||
clearAttachedFile();
|
||||
|
||||
if (!isGuest && typeof loadTasks === 'function') {
|
||||
loadTasks();
|
||||
}
|
||||
|
||||
+5
-1
@@ -67,4 +67,8 @@ cursor.execute("""
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("[✓] Таблица users создана, пользователь puh успешно добавлен в рабочую БД!")
|
||||
'
|
||||
'
|
||||
|
||||
|
||||
|
||||
Обнови системный промпт. Измени пункт 4.4. "4.4. ПРЯМОЙ ВЫЗОВ ВРЕМЕНИ: При вопросах про текущую дату, время или день недели ЗАПРЕЩЕНО отвечать текстом или писать названия функций вроде (db_get_current_server_time). Ты ОБЯЗАН мгновенно вызвать инструмент db_get_current_server_time через механизм tool_calls."
|
||||
Reference in New Issue
Block a user