feat(llm): переход на реляционные узлы промпта, db_cli context и очистка от регулярок
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
"""
|
||||
===============================================================================
|
||||
FILE: modules/web_api/llm/db/db_prompts.py
|
||||
PROJECT: SCUD Orion AI
|
||||
PROJECT: SCUD Orion AI (Unified Architecture)
|
||||
MODULE: web_api / llm / db
|
||||
ROLE: Управление системными промптами, правилами базы знаний,
|
||||
декларативным реестром действий инструментов (tool_action_registry)
|
||||
и расширенным сессионным состоянием (session_states + idle_turns).
|
||||
ROLE: Реляционное управление системным промптом (таблица system_prompt_nodes),
|
||||
базой знаний, реестром действий и сессионными стейтами.
|
||||
===============================================================================
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
@@ -14,18 +15,115 @@ from .connection import get_db_connection
|
||||
logger = logging.getLogger("DB_PROMPTS")
|
||||
|
||||
|
||||
def db_get_active_system_prompt() -> str:
|
||||
"""Извлекает актуальный активный системный промпт из SQLite."""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT prompt_text FROM system_prompts WHERE is_active = 1 ORDER BY id DESC LIMIT 1")
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
return row["prompt_text"] if row else "Ты — ИИ-ассистент SCUD Orion AI."
|
||||
def init_prompt_nodes_table():
|
||||
"""Создает реляционную таблицу узлов промпта и заполняет базовыми данными."""
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS system_prompt_nodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
prompt_name TEXT DEFAULT 'main_agent',
|
||||
section_id INTEGER NOT NULL,
|
||||
item_id INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(prompt_name, section_id, item_id)
|
||||
);
|
||||
""")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_prompt_nodes ON system_prompt_nodes(prompt_name, section_id, item_id);")
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM system_prompt_nodes WHERE prompt_name = 'main_agent'")
|
||||
if cursor.fetchone()[0] == 0:
|
||||
seed_nodes = [
|
||||
# Раздел 1
|
||||
(1, 0, "РОЛЬ И ЗАДАЧИ АССИСТЕНТА"),
|
||||
(1, 1, "Управление бэклогом задач проекта (через db_get_tasks, db_add_task, db_update_task_status, db_delete_task)."),
|
||||
(1, 2, "Консультация по правилам и арбитражу кадровых данных/СКУД из базы знаний (через db_get_rules)."),
|
||||
(1, 3, "Предоставление справки о возможностях и примерах команд (СТРОГО через db_get_reference)."),
|
||||
(1, 4, "Просмотр аномалий СКУД ⟷ 1С (СТРОГО через db_get_anomalies)."),
|
||||
(1, 5, "Поддержка диалога с операторами и администраторами системы."),
|
||||
(1, 7, "Работа с логами, снапшотами и срезами СКУД (через db_get_snapshots)."),
|
||||
|
||||
# Раздел 2
|
||||
(2, 0, "ПРАВИЛА ВЫЗОВА ИНСТРУМЕНТОВ И ДАТ"),
|
||||
(2, 1, "ОБЯЗАТЕЛЬНЫЙ ПРЕВЬЮ-МЕРДЖ: Категорически ЗАПРЕЩЕНО изменять промпт напрямую! При ЛЮБОМ запросе пользователя на изменение системного промпта Ты ОБЯЗАН вызвать инструмент db_prompt_node_edit."),
|
||||
(2, 2, "БЕЗУСЛОВНОЕ ПОДТВЕРЖДЕНИЕ ПРЕВЬЮ: Инструмент db_confirm_prompt_preview вызывается СТРОГО после того, как пользователь напишет 'подтверждаю', 'да', 'сохраняй'."),
|
||||
(2, 3, "ПОДТВЕРЖДЕНИЕ УДАЛЕНИЯ ЗАДАЧ: При запросе на удаление задач КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО сразу вызывать db_delete_task! Ты ОБЯЗАН сначала спросить пользователя подтверждение."),
|
||||
(2, 4, "ИСПОЛЬЗОВАНИЕ КАЛЕНДАРЯ: При любых вопросах про даты БЕРИ ТОЧНУЮ ДАТУ ИЗ [СИСТЕМНОГО КАЛЕНДАРЯ СЕРВЕРА] В НАЧАЛЕ КОНТЕКСТА."),
|
||||
(2, 5, "СТРОГИЙ ВЫЗОВ АНОМАЛИЙ: При запросах аномалий или нарушений Ты ОБЯЗАН вызвать инструмент db_get_anomalies."),
|
||||
(2, 6, "СТРОГИЙ ВЫЗОВ СПРАВОЧНИКА: При запросах о возможностях, примерах запросов или командах Ты ОБЯЗАН вызывать db_get_reference."),
|
||||
(2, 7, "Запрещено писать названия функций или код вызова текстом на экран."),
|
||||
(2, 8, "СТРОГИЙ ВЫЗОВ СНАПШОТОВ: При ЛЮБЫХ запросах про снапшоты, срезы или логи СКУД за дату/день недели Ты ОБЯЗАН вызывать инструмент db_get_snapshots."),
|
||||
|
||||
# Раздел 3
|
||||
(3, 0, "ПРАВИЛА СТИЛЯ"),
|
||||
(3, 1, "Никогда не начинай ответ со склеек или слов-паразитов."),
|
||||
(3, 2, "Отвечай в чистом текстовом формате (plain text) без спецсимволов или ###, если прямо не попросили."),
|
||||
(3, 3, "Сохраняй инженерный, лаконичный и профессиональный стиль.")
|
||||
]
|
||||
cursor.executemany("""
|
||||
INSERT OR IGNORE INTO system_prompt_nodes (prompt_name, section_id, item_id, content)
|
||||
VALUES ('main_agent', ?, ?, ?)
|
||||
""", seed_nodes)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def db_get_active_system_prompt(prompt_name: str = "main_agent") -> str:
|
||||
"""Собирает структурированный текст промпта из реляционной таблицы узлов."""
|
||||
init_prompt_nodes_table()
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT section_id, item_id, content
|
||||
FROM system_prompt_nodes
|
||||
WHERE prompt_name = ? AND is_active = 1
|
||||
ORDER BY section_id ASC, item_id ASC
|
||||
""", (prompt_name,))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
if not rows:
|
||||
return "Ты — ИИ-ассистент SCUD Orion AI."
|
||||
|
||||
lines = []
|
||||
current_section = None
|
||||
|
||||
for sec_id, itm_id, content in rows:
|
||||
if itm_id == 0:
|
||||
if current_section is not None:
|
||||
lines.append("")
|
||||
lines.append(f"{sec_id}. {content}")
|
||||
current_section = sec_id
|
||||
else:
|
||||
lines.append(f" {sec_id}.{itm_id}. {content}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def db_apply_prompt_node_action(action: str, section_id: int, item_id: int, content: str = "", prompt_name: str = "main_agent"):
|
||||
"""Прямое добавление, изменение или удаление узла в БД."""
|
||||
init_prompt_nodes_table()
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
action_clean = action.upper()
|
||||
if action_clean in ["ADD", "UPDATE"]:
|
||||
cursor.execute("""
|
||||
INSERT INTO system_prompt_nodes (prompt_name, section_id, item_id, content, is_active, updated_at)
|
||||
VALUES (?, ?, ?, ?, 1, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(prompt_name, section_id, item_id) DO UPDATE SET
|
||||
content = excluded.content,
|
||||
is_active = 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""", (prompt_name, section_id, item_id, content))
|
||||
elif action_clean == "DELETE":
|
||||
cursor.execute("""
|
||||
DELETE FROM system_prompt_nodes
|
||||
WHERE prompt_name = ? AND section_id = ? AND item_id = ?
|
||||
""", (prompt_name, section_id, item_id))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def db_get_tool_action(tool_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""Получение шаблона, кнопок и настроек эфемерности инструмента из SQLite."""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
@@ -44,33 +142,7 @@ def db_get_tool_action(tool_name: str) -> Optional[Dict[str, Any]]:
|
||||
return None
|
||||
|
||||
|
||||
def db_add_system_prompt(name: str, prompt_text: str) -> Dict[str, Any]:
|
||||
"""Сохраняет новую версию системного промпта и активирует её."""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
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)
|
||||
)
|
||||
conn.commit()
|
||||
return {"status": "success", "message": "Системный промпт успешно обновлен"}
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении промпта в БД: {e}")
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def db_get_rules() -> List[Dict[str, Any]]:
|
||||
"""Извлекает список глобальных правил компании из ai_knowledge_base."""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT id, rule_text FROM ai_knowledge_base ORDER BY id ASC")
|
||||
@@ -79,23 +151,11 @@ def db_get_rules() -> List[Dict[str, Any]]:
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# РАБОТА С СЕССИОННЫМИ СОСТОЯНИЯМИ, ЧЕРНОВИКАМИ И СЧЕТЧИКОМ ОТВЛЕЧЕНИЙ (IDLE_TURNS)
|
||||
# =============================================================================
|
||||
|
||||
def db_set_session_state(session_id: str, state_type: str, data: Any) -> None:
|
||||
"""
|
||||
Сохраняет состояние сессии в SQLite.
|
||||
Если передан dict/list — автоматически сериализует его в JSON-строку.
|
||||
"""
|
||||
"""Сохраняет состояние сессии в SQLite."""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
if isinstance(data, (dict, list)):
|
||||
payload_str = json.dumps(data, ensure_ascii=False)
|
||||
else:
|
||||
payload_str = str(data) if data is not None else ""
|
||||
|
||||
payload_str = json.dumps(data, ensure_ascii=False) if isinstance(data, (dict, list)) else (str(data) if data is not None else "")
|
||||
cursor.execute("""
|
||||
INSERT INTO session_states (session_id, state_type, pending_data, updated_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
@@ -109,63 +169,23 @@ def db_set_session_state(session_id: str, state_type: str, data: Any) -> None:
|
||||
|
||||
|
||||
def db_get_session_state(session_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Извлекает состояние сессии из SQLite.
|
||||
Если в pending_data лежит валидный JSON-объект — парсит его в data_json.
|
||||
"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT session_id, state_type, pending_data, updated_at FROM session_states WHERE session_id = ?",
|
||||
(session_id,)
|
||||
)
|
||||
cursor.execute("SELECT session_id, state_type, pending_data, updated_at FROM session_states WHERE session_id = ?", (session_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not row:
|
||||
return None
|
||||
|
||||
res = dict(row)
|
||||
raw_data = res.get("pending_data") or ""
|
||||
|
||||
try:
|
||||
if raw_data.strip().startswith("{") or raw_data.strip().startswith("["):
|
||||
res["data_json"] = json.loads(raw_data)
|
||||
else:
|
||||
res["data_json"] = None
|
||||
res["data_json"] = json.loads(raw_data) if raw_data.strip().startswith(("{", "[")) else None
|
||||
except Exception:
|
||||
res["data_json"] = None
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def db_increment_session_idle(session_id: str) -> int:
|
||||
"""
|
||||
Инкрементирует счетчик idle_turns для активного черновика сессии.
|
||||
Возвращает обновленное значение счетчика отвлечений.
|
||||
"""
|
||||
state = db_get_session_state(session_id)
|
||||
if not state:
|
||||
return 0
|
||||
|
||||
data_meta = state.get("data_json")
|
||||
if isinstance(data_meta, dict):
|
||||
current_turns = data_meta.get("idle_turns", 0) + 1
|
||||
data_meta["idle_turns"] = current_turns
|
||||
db_set_session_state(session_id, state["state_type"], data_meta)
|
||||
return current_turns
|
||||
else:
|
||||
# Если ранее данные были сохранены обычной строкой
|
||||
new_meta = {
|
||||
"draft_text": state.get("pending_data", ""),
|
||||
"idle_turns": 1
|
||||
}
|
||||
db_set_session_state(session_id, state["state_type"], new_meta)
|
||||
return 1
|
||||
|
||||
|
||||
def db_clear_session_state(session_id: str) -> None:
|
||||
"""Сбрасывает и удаляет активное состояние сессии."""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM session_states WHERE session_id = ?", (session_id,))
|
||||
@@ -173,28 +193,10 @@ def db_clear_session_state(session_id: str) -> None:
|
||||
conn.close()
|
||||
|
||||
|
||||
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_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'
|
||||
]
|
||||
tables = ['scud_logs', 'zup_staff', 'zup_absences', 'anomalies_history', 'ai_knowledge_base', 'system_prompt_nodes', 'session_states', 'tasks']
|
||||
stats = {}
|
||||
for t in tables:
|
||||
try:
|
||||
@@ -207,7 +209,6 @@ def db_get_stats() -> Dict[str, Any]:
|
||||
|
||||
|
||||
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"
|
||||
@@ -224,7 +225,6 @@ def db_get_anomalies(limit: int = 100, date_str: Optional[str] = None) -> Dict[s
|
||||
|
||||
|
||||
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"
|
||||
@@ -236,4 +236,51 @@ def db_get_reference(category: Optional[str] = None) -> Dict[str, Any]:
|
||||
cursor.execute(query, params)
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
return {"status": "success", "count": len(rows), "reference_items": [dict(r) for r in rows]}
|
||||
return {"status": "success", "count": len(rows), "reference_items": [dict(r) for r in rows]}
|
||||
|
||||
def db_add_system_prompt(name: str, prompt_text: str) -> Dict[str, Any]:
|
||||
"""Мост обратной совместимости: парсит текст промпта и сохраняет по узлам в БД."""
|
||||
try:
|
||||
init_prompt_nodes_table()
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM system_prompt_nodes WHERE prompt_name = ?", (name,))
|
||||
|
||||
current_sec = 1
|
||||
current_itm = 0
|
||||
|
||||
for line in prompt_text.splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
|
||||
# Проверяем заголовок раздела (например, "1. РОЛЬ И ЗАДАЧИ...")
|
||||
parts = stripped.split(".", 1)
|
||||
if len(parts) == 2 and parts[0].strip().isdigit():
|
||||
sec_num = int(parts[0].strip())
|
||||
rest = parts[1].strip()
|
||||
|
||||
# Проверяем, это подпункт (например, "1.1. Текст") или заголовок раздела
|
||||
sub_parts = rest.split(".", 1)
|
||||
if len(sub_parts) == 2 and sub_parts[0].strip().isdigit():
|
||||
current_sec = sec_num
|
||||
current_itm = int(sub_parts[0].strip())
|
||||
content = sub_parts[1].strip()
|
||||
else:
|
||||
current_sec = sec_num
|
||||
current_itm = 0
|
||||
content = rest
|
||||
else:
|
||||
current_itm += 1
|
||||
content = stripped
|
||||
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO system_prompt_nodes (prompt_name, section_id, item_id, content, is_active)
|
||||
VALUES (?, ?, ?, ?, 1)
|
||||
""", (name, current_sec, current_itm, content))
|
||||
|
||||
conn.commit()
|
||||
return {"status": "success", "message": "Системный промпт успешно сохранен по узлам"}
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при разборе промпта в узлы: {e}")
|
||||
return {"status": "error", "error": str(e)}
|
||||
Reference in New Issue
Block a user