текст из pdf распознается, все работает, добавлены все функции из файла работы с базой данных db_cli.py. Готовность к оптимизации интерфейса.

This commit is contained in:
2026-08-11 16:19:07 +03:00
parent 286355e4c0
commit a622fd98d2
13 changed files with 641 additions and 78 deletions
+11 -1
View File
@@ -1,7 +1,7 @@
import os import os
import sqlite3 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(): def init_db():
os.makedirs(os.path.dirname(DB_NAME), exist_ok=True) 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.commit()
conn.close() conn.close()
print(f"[✓] Единая база данных SQLite ({DB_NAME}) успешно инициализирована!") print(f"[✓] Единая база данных SQLite ({DB_NAME}) успешно инициализирована!")
+110 -18
View File
@@ -2,12 +2,12 @@ import json
import urllib.request import urllib.request
import urllib.error import urllib.error
import logging import logging
from typing import List, Dict, Any, Tuple from typing import List, Dict, Any, Tuple, Optional
from datetime import datetime from datetime import datetime, timedelta
from .db_tools import ( from .db_tools import (
db_get_active_system_prompt, db_get_active_system_prompt,
db_add_system_prompt, db_add_system_prompt,
db_get_tasks, db_get_tasks,
db_update_task_status, db_update_task_status,
db_delete_task, db_delete_task,
@@ -15,7 +15,16 @@ from .db_tools import (
db_get_rules, db_get_rules,
db_set_session_state, db_set_session_state,
db_get_session_state, db_get_session_state,
db_get_snapshots,
db_delete_snapshots,
db_clear_session_state, 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 get_db_connection
) )
@@ -27,6 +36,11 @@ logger = logging.getLogger("SCUD_AGENT")
OLLAMA_URL = "http://192.168.11.3:11434/api/chat" OLLAMA_URL = "http://192.168.11.3:11434/api/chat"
MODEL_NAME = "qwen2.5:14b" MODEL_NAME = "qwen2.5:14b"
DAYS_RU = [
"понедельник", "вторник", "среда", "четверг",
"пятница", "суббота", "воскресенье"
]
def clean_output(text: str) -> str: def clean_output(text: str) -> str:
if not text: if not text:
return text return text
@@ -37,26 +51,71 @@ def clean_output(text: str) -> str:
text = text[len(art):].lstrip(",.!?:; -") text = text[len(art):].lstrip(",.!?:; -")
return text.strip() 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]]]: def get_dynamic_calendar_context() -> str:
if chat_history is None: now = datetime.now()
chat_history = [] 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}") 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() dynamic_prompt_text = db_get_active_system_prompt()
calendar_context = get_dynamic_calendar_context()
session_state = db_get_session_state(session_id) session_state = db_get_session_state(session_id)
preview_status_note = "" 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[АКТИВНО ПРЕВЬЮ ПРОМПТА: Ожидается подтверждение или отмена изменений пользователем]." 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 = { system_prompt = {
"role": "system", "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 = { payload = {
"model": MODEL_NAME, "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}") logger.info(f"Вызов функции (Tool): {fn_name} с аргументами: {fn_args}")
tool_result_content = "" 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) tasks = db_get_tasks(user_id)
tool_result_content = json.dumps(tasks, ensure_ascii=False) 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() prompt_content = db_get_active_system_prompt()
tool_result_content = json.dumps({"system_prompt": prompt_content}, ensure_ascii=False) 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": elif fn_name == "db_preview_prompt_merge":
proposed_text = fn_args.get("proposed_prompt", "") proposed_text = fn_args.get("proposed_prompt", "")
if proposed_text: if proposed_text:
# Фиксируем превью в сессии и сразу возвращаем текст пользователю на экран
db_set_session_state(session_id, "PROMPT_PREVIEW", proposed_text) db_set_session_state(session_id, "PROMPT_PREVIEW", proposed_text)
preview_reply = f"Ваше изменение успешно предпросмотрено. Полный обновленный системный промпт теперь выглядит так:\n\n{proposed_text}\n\nДля применения изменений подтвердите действие («подтверждаю») или отмените («отмена»)." 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: else:
tool_result_content = json.dumps({"status": "error", "message": "Текст превью пуст."}, ensure_ascii=False) tool_result_content = json.dumps({"status": "error", "message": "Текст превью пуст."}, ensure_ascii=False)
elif fn_name == "db_confirm_prompt_preview": elif fn_name == "db_confirm_prompt_preview":
if session_state and session_state["state_type"] == "PROMPT_PREVIEW": if session_state and session_state.get("state_type") == "PROMPT_PREVIEW":
pending_text = session_state["pending_data"] pending_text = session_state.get("pending_data", "")
res = db_add_system_prompt("main_agent", pending_text) res = db_add_system_prompt("main_agent", pending_text)
db_clear_session_state(session_id) db_clear_session_state(session_id)
tool_result_content = json.dumps(res, ensure_ascii=False) 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: with urllib.request.urlopen(sec_req) as sec_response:
sec_res_data = json.loads(sec_response.read().decode("utf-8")) sec_res_data = json.loads(sec_response.read().decode("utf-8"))
final_content = clean_output(sec_res_data.get("message", {}).get("content", "").strip().replace("**", "")) 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("**", "")) 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: except Exception as ex:
logger.exception(f"Непредвиденная ошибка: {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
View File
@@ -1,16 +1,101 @@
import json
import sqlite3 import sqlite3
import logging import logging
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta
# Настройка логирования
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("DB_TOOLS") logger = logging.getLogger("DB_TOOLS")
DB_PATH = "/home/puh/scud_orion_ai_v2/data/scud_orion_ai.db" 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(): def get_db_connection():
# Увеличиваем таймаут до 30 секунд, чтобы соединения ожидали завершения соседних транзакций,
# а также включаем WAL-режим для безопасного параллельного чтения и записи.
conn = sqlite3.connect(DB_PATH, timeout=30.0) conn = sqlite3.connect(DB_PATH, timeout=30.0)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode = WAL;") conn.execute("PRAGMA journal_mode = WAL;")
@@ -18,17 +103,14 @@ def get_db_connection():
return conn return conn
def normalize_task_id(task_id_input: str) -> str: def normalize_task_id(task_id_input: str) -> str:
"""Преобразует 6, '6', 'task-6' в красивый формат TASK-06 или TASK-006"""
if not task_id_input: if not task_id_input:
return "" return ""
clean_id = str(task_id_input).strip().upper().replace("TASK-", "").replace("TASK", "") clean_id = str(task_id_input).strip().upper().replace("TASK-", "").replace("TASK", "")
if clean_id.isdigit(): if clean_id.isdigit():
num = int(clean_id) 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}" return f"TASK-{clean_id}"
# === ЗАДАЧИ С ФИЛЬТРАЦИЕЙ ПО USER_ID ===
def db_get_tasks(user_id: int) -> List[Dict[str, Any]]: def db_get_tasks(user_id: int) -> List[Dict[str, Any]]:
conn = get_db_connection() conn = get_db_connection()
cursor = conn.cursor() cursor = conn.cursor()
@@ -105,8 +187,6 @@ def db_delete_task(user_id: int, task_id: str) -> Dict[str, Any]:
conn.close() conn.close()
return {"status": "success", "message": f"Задача {formatted_id} удалена"} return {"status": "success", "message": f"Задача {formatted_id} удалена"}
# === ОБЩИЕ СИСТЕМНЫЕ РЕСУРСЫ ===
def db_get_active_system_prompt() -> str: def db_get_active_system_prompt() -> str:
conn = get_db_connection() conn = get_db_connection()
cursor = conn.cursor() cursor = conn.cursor()
@@ -121,18 +201,15 @@ def db_add_system_prompt(name: str, prompt_text: str) -> Dict[str, Any]:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("BEGIN IMMEDIATE;") cursor.execute("BEGIN IMMEDIATE;")
# Проверяем, существует ли уже промпт с таким именем
cursor.execute("SELECT id FROM system_prompts WHERE name = ?", (name,)) cursor.execute("SELECT id FROM system_prompts WHERE name = ?", (name,))
existing = cursor.fetchone() existing = cursor.fetchone()
if existing: if existing:
# Обновляем существующий активный промпт
cursor.execute( cursor.execute(
"UPDATE system_prompts SET prompt_text = ?, is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE name = ?", "UPDATE system_prompts SET prompt_text = ?, is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE name = ?",
(prompt_text, name) (prompt_text, name)
) )
else: else:
# Вставляем новый, если его не было
cursor.execute( cursor.execute(
"INSERT INTO system_prompts (name, prompt_text, is_active) VALUES (?, ?, 1)", "INSERT INTO system_prompts (name, prompt_text, is_active) VALUES (?, ?, 1)",
(name, prompt_text) (name, prompt_text)
@@ -146,7 +223,6 @@ def db_add_system_prompt(name: str, prompt_text: str) -> Dict[str, Any]:
logger.error(f"Ошибка при сохранении промпта в БД: {e}") logger.error(f"Ошибка при сохранении промпта в БД: {e}")
return {"status": "error", "error": str(e)} return {"status": "error", "error": str(e)}
def db_get_rules() -> List[Dict[str, Any]]: def db_get_rules() -> List[Dict[str, Any]]:
conn = get_db_connection() conn = get_db_connection()
cursor = conn.cursor() cursor = conn.cursor()
@@ -182,4 +258,96 @@ def db_clear_session_state(session_id: str):
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM session_states WHERE session_id = ?", (session_id,)) cursor.execute("DELETE FROM session_states WHERE session_id = ?", (session_id,))
conn.commit() 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]
}
+61
View File
@@ -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
View File
@@ -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", "type": "function",
"function": { "function": {
@@ -81,19 +109,78 @@ TOOLS_SCHEMA = [
{ {
"type": "function", "type": "function",
"function": { "function": {
"name": "db_confirm_prompt_preview", "name": "db_get_stats",
"description": "Подтвердить и сохранить текущее подготовленное превью в БД. Вызывай этот инструмент, когда пользователь говорит 'подтверждаю', 'да', 'вноси', 'применяй', 'сохраняй' или одобряет превью в любой форме.", "description": "ПОЛУЧИТЬ ОБЩУЮ СТАТИСТИКУ БАЗЫ ДАННЫХ. Вызывай, когда пользователь просит показать общую статистику БД, количество записей в таблицах или размер базы.",
"parameters": {"type": "object", "properties": {}} "parameters": {"type": "object", "properties": {}}
} }
}, },
{ {
"type": "function", "type": "function",
"function": { "function": {
"name": "db_cancel_prompt_preview", "name": "db_get_anomalies",
"description": "Отменить текущее превью системного промпта и сбросить изменения. Вызывай, когда пользователь явно отказывается от изменений.", "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": {}} "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", "type": "function",
"function": { "function": {
@@ -110,5 +197,21 @@ TOOLS_SCHEMA = [
"required": ["proposed_prompt"] "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": {}}
}
}
]
+82 -31
View File
@@ -8,7 +8,7 @@ from typing import List, Dict, Any, Optional
import jwt import jwt
from passlib.context import CryptContext 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.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
@@ -16,9 +16,10 @@ from pydantic import BaseModel
from llm.agent import process_chat_message from llm.agent import process_chat_message
from llm.db_tools import db_get_tasks, DB_PATH from llm.db_tools import db_get_tasks, DB_PATH
from llm.file_parser import extract_text_from_file
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s", format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[logging.StreamHandler()] handlers=[logging.StreamHandler()]
) )
@@ -65,6 +66,16 @@ app = FastAPI(title="SCUD Orion AI Context API")
if os.path.exists("static"): if os.path.exists("static"):
app.mount("/static", StaticFiles(directory="static"), name="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): class AuthRequest(BaseModel):
username: str username: str
@@ -80,16 +91,42 @@ class ChangePasswordRequest(BaseModel):
old_password: str old_password: str
new_password: str new_password: str
class ChatRequest(BaseModel):
session_id: str
message: str
# === API МАРШРУТЫ === # === API МАРШРУТЫ ===
@app.get("/") @app.get("/")
def read_root(): def read_root():
return FileResponse("static/index.html") 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") @app.post("/api/v1/auth/login")
def login(req: AuthRequest): def login(req: AuthRequest):
username = req.username.strip().lower() 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)): def get_tasks(user: Dict[str, Any] = Depends(get_current_user)):
return db_get_tasks(user_id=user["id"]) return db_get_tasks(user_id=user["id"])
# ЧАТ С ПОДДЕРЖКОЙ ФАЙЛОВ И АВТОРИЗАЦИИ
@app.post("/api/v1/chat") @app.post("/api/v1/chat")
def chat_endpoint(req: ChatRequest, user: Dict[str, Any] = Depends(get_current_user)): async def chat_endpoint(
reply, _ = process_chat_message(user_id=user["id"], user_message=req.message) session_id: str = Form("web_session_main"),
return {"reply": reply} 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") @app.post("/api/v1/chat/guest")
def guest_chat_endpoint(req: ChatRequest): async def guest_chat_endpoint(
payload = { session_id: str = Form("web_session_main"),
"model": MODEL_NAME, message: str = Form(""),
"messages": [ file: Optional[UploadFile] = File(default=None)
{"role": "system", "content": "Ты — полезный ИИ-ассистент. Отвечай на вопросы пользователя четко и по существу."}, ):
{"role": "user", "content": req.message} logging.info(f"=== [GUEST CHAT API] Входящий запрос, file={file.filename if file else 'None'} ===")
], file_content_text = ""
"stream": False, if file and file.filename:
"options": {"num_predict": 2048, "temperature": 0.3} file_bytes = await file.read()
} file_content_text = extract_text_from_file(file_bytes, file.filename)
try:
req_ollama = urllib.request.Request( reply, history = process_chat_message(
OLLAMA_URL, user_id=0,
data=json.dumps(payload).encode("utf-8"), user_message=message,
headers={"Content-Type": "application/json"} file_context=file_content_text,
) session_id=session_id
with urllib.request.urlopen(req_ollama) as response: )
res_data = json.loads(response.read().decode("utf-8")) return {"reply": reply, "history": history}
reply = res_data.get("message", {}).get("content", "").strip()
return {"reply": reply}
except Exception as e:
return {"reply": f"Ошибка связи с локальной нейросетью: {e}"}
# === СТРОГО В КОНЦЕ: ФОЛЛБЭК СТАТИКИ === # === СТРОГО В КОНЦЕ: ФОЛЛБЭК СТАТИКИ ===
+1
View File
@@ -0,0 +1 @@
SNI is required.
+1
View File
@@ -0,0 +1 @@
SNI is required.
Binary file not shown.
+6
View File
@@ -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
View File
@@ -178,16 +178,34 @@
<i class="fa-solid fa-robot mr-1"></i> ИИ-Ассистент <i class="fa-solid fa-robot mr-1"></i> ИИ-Ассистент
</p> </p>
<p class="text-slate-800 text-xs sm:text-sm leading-relaxed"> <p class="text-slate-800 text-xs sm:text-sm leading-relaxed">
Привет! У каждого пользователя свое изолированное пространство задач. Вы можете задавать вопросы нейросети или ставить персональные задачи. Привет! У каждого пользователя свое изолированное пространство задач. Вы можете задавать вопросы нейросети, прикреплять файлы или ставить персональные задачи.
</p> </p>
</div> </div>
</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="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"> <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"> <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" <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> 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> </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"> <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
View File
@@ -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) { async function sendMessage(e) {
if (e && e.preventDefault) e.preventDefault(); if (e && e.preventDefault) e.preventDefault();
@@ -8,13 +38,19 @@ async function sendMessage(e) {
if (!input || !chatWindow) return; if (!input || !chatWindow) return;
const text = input.value.trim(); 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 = ` const userMsgHtml = `
<div class="flex justify-end mb-3"> <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"> <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>
</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 isGuest = typeof IS_GUEST !== 'undefined' ? IS_GUEST : (localStorage.getItem("scud_is_guest") === "true");
const endpoint = isGuest ? "/api/v1/chat/guest" : "/api/v1/chat"; 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) { if (!isGuest && token) {
headers["Authorization"] = "Bearer " + token; headers["Authorization"] = "Bearer " + token;
} }
@@ -42,7 +88,7 @@ async function sendMessage(e) {
const res = await fetch(endpoint, { const res = await fetch(endpoint, {
method: "POST", method: "POST",
headers: headers, headers: headers,
body: JSON.stringify({ session_id: "web_session_main", message: text }) body: formData
}); });
if (res.status === 401 && !isGuest) { if (res.status === 401 && !isGuest) {
@@ -65,6 +111,8 @@ async function sendMessage(e) {
chatWindow.insertAdjacentHTML("beforeend", botMsgHtml); chatWindow.insertAdjacentHTML("beforeend", botMsgHtml);
chatWindow.scrollTop = chatWindow.scrollHeight; chatWindow.scrollTop = chatWindow.scrollHeight;
clearAttachedFile();
if (!isGuest && typeof loadTasks === 'function') { if (!isGuest && typeof loadTasks === 'function') {
loadTasks(); loadTasks();
} }
+5 -1
View File
@@ -67,4 +67,8 @@ cursor.execute("""
conn.commit() conn.commit()
conn.close() conn.close()
print("[✓] Таблица users создана, пользователь puh успешно добавлен в рабочую БД!") print("[✓] Таблица users создана, пользователь puh успешно добавлен в рабочую БД!")
' '
Обнови системный промпт. Измени пункт 4.4. "4.4. ПРЯМОЙ ВЫЗОВ ВРЕМЕНИ: При вопросах про текущую дату, время или день недели ЗАПРЕЩЕНО отвечать текстом или писать названия функций вроде (db_get_current_server_time). Ты ОБЯЗАН мгновенно вызвать инструмент db_get_current_server_time через механизм tool_calls."