feat(llm): переход на реляционные узлы промпта, db_cli context и очистка от регулярок
This commit is contained in:
+78
-109
@@ -3,12 +3,13 @@
|
||||
FILE: modules/web_api/llm/agent.py
|
||||
PROJECT: SCUD Orion AI (Unified Architecture)
|
||||
MODULE: web_api / llm (Core Agent Coordinator)
|
||||
ROLE: Оркестратор диалога, диспетчер Function Calling и Topic Drift контроллер.
|
||||
ROLE: Оркестратор диалога, диспетчер реляционных узлов промпта и инструментов.
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
# --- [SECTION 1: SYSTEM PATHS & IMPORTS] --- # ANCHOR[SYS_IMPORTS]
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Dict, Any, Tuple, Optional
|
||||
@@ -16,7 +17,7 @@ from typing import List, Dict, Any, Tuple, Optional
|
||||
from .db.connection import get_db_connection
|
||||
from .db_tools import (
|
||||
db_get_active_system_prompt,
|
||||
db_add_system_prompt,
|
||||
db_apply_prompt_node_action,
|
||||
db_get_tool_action,
|
||||
db_get_tasks,
|
||||
db_update_task_status,
|
||||
@@ -26,7 +27,6 @@ from .db_tools import (
|
||||
db_set_session_state,
|
||||
db_get_session_state,
|
||||
db_clear_session_state,
|
||||
db_increment_session_idle,
|
||||
db_get_snapshots,
|
||||
db_delete_snapshots,
|
||||
db_get_current_server_time,
|
||||
@@ -35,16 +35,14 @@ from .db_tools import (
|
||||
db_purge_ephemeral_messages,
|
||||
db_get_stats,
|
||||
db_get_anomalies,
|
||||
db_get_session_states,
|
||||
db_get_reference
|
||||
)
|
||||
|
||||
from .schemas import TOOLS_SCHEMA
|
||||
from .core.calendar_utils import get_dynamic_calendar_context
|
||||
from .core.tool_injector import clean_raw_tool_tags, clean_output
|
||||
from .core.tool_injector import clean_raw_tool_tags, clean_output, inject_tools_if_needed
|
||||
from .core.ollama_client import call_ollama_chat
|
||||
from .core.fast_path import handle_fast_path_intercept
|
||||
from .core.prompt_merger import build_prompt_preview_merge
|
||||
|
||||
# --- [SECTION 2: LOGGING CONFIGURATION] --- # ANCHOR[LOGGING_CONFIG]
|
||||
logger = logging.getLogger("SCUD_AGENT")
|
||||
@@ -67,12 +65,7 @@ def process_chat_message(
|
||||
chat_history: List[Dict[str, Any]] = None,
|
||||
session_id: str = "web_session_main"
|
||||
) -> Tuple[str, List[Dict[str, Any]], Optional[Dict[str, Any]]]:
|
||||
"""
|
||||
Главный конвейер диалога:
|
||||
1. Проверка Fast-Path команд (подтверждение, отмена, завершение).
|
||||
2. Формирование контекста и вызов Ollama LLM.
|
||||
3. Выполнение инструментов и обработка Context Guard.
|
||||
"""
|
||||
"""Главный конвейер диалога с поддержкой реляционного редактора промпта."""
|
||||
logger.info(f"Получено сообщение от user_id={user_id}, session_id={session_id}: {user_message}")
|
||||
|
||||
full_user_content = f"{user_message}\n\n[СОДЕРЖИМОЕ ПРИКРЕПЛЕННОГО ФАЙЛА]:\n{file_context}" if file_context else user_message
|
||||
@@ -94,28 +87,26 @@ def process_chat_message(
|
||||
user_info = f"Пользователь ID={user_id}" if user_id != 0 else "Гость"
|
||||
|
||||
active_state_context = ""
|
||||
if session_state and session_state.get("state_type") in ["PROMPT_PREVIEW", "PROMPT_FOLLOWUP"]:
|
||||
active_state_context = (
|
||||
"\n[АКТИВНОЕ ДЕЙСТВИЕ В СЕССИИ]\n"
|
||||
"В данный момент оператор находится в процессе настройки системного промпта.\n"
|
||||
"- Если оператор просит продолжить правки — продолжай работу с ним.\n"
|
||||
"- Если оператор переключился на другую тему — выполни его команду штатно.\n"
|
||||
)
|
||||
if session_state:
|
||||
state_type = session_state.get("state_type", "GENERAL")
|
||||
active_state_context = f"\n[АКТИВНЫЙ РЕЖИМ СЕССИИ: {state_type}]\n"
|
||||
|
||||
system_prompt_content = (
|
||||
f"Ты — интеллектуальный ассистент SCUD Orion AI. "
|
||||
f"Ты управляешь задачами, аналитикой СКУД и системными настройками с помощью инструментов (tools).\n\n"
|
||||
f"[ОКРУЖЕНИЕ]\n"
|
||||
f"- Пользователь: {user_info}\n"
|
||||
f"- {calendar_context}\n"
|
||||
f"{active_state_context}\n"
|
||||
f"[ПРАВИЛА ИСПОЛЬЗОВАНИЯ ИНСТРУМЕНТОВ]\n"
|
||||
f"1. Для любых изменений системного промпта ВСЕГДА вызывай db_preview_prompt_merge(prompt_text=...).\n"
|
||||
f"2. Для просмотра системного промпта ВСЕГДА вызывай db_get_system_prompt().\n"
|
||||
f"3. Для просмотра задач ВСЕГДА вызывай db_get_tasks().\n"
|
||||
f"4. Не симулируй выполнение функций текстом — сразу вызывай инструмент.\n\n"
|
||||
f"[ТЕКУЩИЙ АКТИВНЫЙ СИСТЕМНЫЙ ПРОМПТ]:\n{dynamic_prompt_text}"
|
||||
)
|
||||
f"Ты — интеллектуальный ассистент SCUD Orion AI. "
|
||||
f"Ты управляешь задачами, аналитикой СКУД и системными настройками с помощью инструментов (tools).\n\n"
|
||||
f"СТРОГИЕ ПРАВИЛА СТИЛЯ:\n"
|
||||
f"- Запрещено использовать панибратские или шутливые обращения. Отвечай профессионально и строго по существу.\n\n"
|
||||
f"[ОКРУЖЕНИЕ]\n"
|
||||
f"- Пользователь: {user_info}\n"
|
||||
f"- {calendar_context}\n"
|
||||
f"{active_state_context}\n"
|
||||
f"[ПРАВИЛА ИСПОЛЬЗОВАНИЯ ИНСТРУМЕНТОВ]\n"
|
||||
f"1. Для любых изменений системного промпта ВСЕГДА вызывай db_prompt_node_edit(action=..., section_id=..., item_id=..., content=...). Абсолютно запрещено изменять промпт напрямую текстом.\n"
|
||||
f" Пример: если просят 'добавь пункт 2.9 Тестовый пункт', ты ОБЯЗАН вызвать инструмент db_prompt_node_edit с аргументами: action='ADD', section_id=2, item_id=9, content='Тестовый пункт'.\n"
|
||||
f"2. Для просмотра системного промпта ВСЕГДА вызывай db_get_system_prompt().\n"
|
||||
f"3. Для просмотра задач ВСЕГДА вызывай db_get_tasks().\n\n"
|
||||
f"[ТЕКУЩИЙ АКТИВНЫЙ СИСТЕМНЫЙ ПРОМПТ]:\n{dynamic_prompt_text}"
|
||||
)
|
||||
|
||||
user_msg_object = {"role": "user", "content": full_user_content}
|
||||
|
||||
@@ -135,20 +126,24 @@ def process_chat_message(
|
||||
messages = [{"role": "system", "content": system_prompt_content}] + clean_db_history + [user_msg_object]
|
||||
msg = call_ollama_chat(messages, tools=TOOLS_SCHEMA, is_vision=False)
|
||||
|
||||
raw_text_reply = msg.get("content", "")
|
||||
tool_calls = msg.get("tool_calls", [])
|
||||
tool_calls = inject_tools_if_needed(user_message, raw_text_reply, tool_calls)
|
||||
|
||||
if tool_calls:
|
||||
logger.info(f"Ответ от Ollama получен. Tool calls: True (кол-во: {len(tool_calls)})")
|
||||
messages.append(msg)
|
||||
|
||||
tool_names_called = []
|
||||
|
||||
for tool in tool_calls:
|
||||
fn_name = tool["function"]["name"]
|
||||
fn_args = tool["function"].get("arguments", {})
|
||||
tool_names_called.append(fn_name)
|
||||
logger.info(f"Вызов функции (Tool): {fn_name} с аргументами: {fn_args}")
|
||||
tool_result_content = ""
|
||||
action_cfg = db_get_tool_action(fn_name)
|
||||
|
||||
# ANCHOR[TASK_INTERACTIVE_DISPATCH]
|
||||
if fn_name == "db_get_tasks":
|
||||
raw_tasks = db_get_tasks(user_id)
|
||||
reply_text = "Вот интерактивный список ваших текущих задач:"
|
||||
@@ -158,13 +153,48 @@ def process_chat_message(
|
||||
"tasks": raw_tasks
|
||||
}
|
||||
|
||||
elif fn_name == "db_preview_prompt_merge":
|
||||
proposed_text = fn_args.get("prompt_text") or fn_args.get("proposed_prompt") or ""
|
||||
if isinstance(fn_args, str):
|
||||
proposed_text = fn_args
|
||||
elif fn_name == "db_prompt_node_edit":
|
||||
action = fn_args.get("action", "ADD").upper()
|
||||
sec_id = int(fn_args.get("section_id", 2))
|
||||
itm_id = int(fn_args.get("item_id", 1))
|
||||
content = fn_args.get("content", "").strip()
|
||||
|
||||
merged_prompt = build_prompt_preview_merge(db_get_active_system_prompt(), user_message, proposed_text)
|
||||
db_set_session_state(session_id, "PROMPT_PREVIEW", {"draft_text": merged_prompt, "idle_turns": 0})
|
||||
with get_db_connection() as temp_conn:
|
||||
temp_cursor = temp_conn.cursor()
|
||||
temp_cursor.execute("SELECT section_id, item_id, content FROM system_prompt_nodes WHERE prompt_name = 'main_agent' AND is_active = 1 ORDER BY section_id, item_id")
|
||||
existing_nodes = temp_cursor.fetchall()
|
||||
|
||||
nodes_dict = {(sec, itm): txt for sec, itm, txt in existing_nodes}
|
||||
|
||||
if action == "DELETE":
|
||||
nodes_dict.pop((sec_id, itm_id), None)
|
||||
else:
|
||||
nodes_dict[(sec_id, itm_id)] = content
|
||||
|
||||
virtual_lines = []
|
||||
curr_sec = None
|
||||
for (s_id, i_id), txt in sorted(nodes_dict.items()):
|
||||
if i_id == 0:
|
||||
if curr_sec is not None:
|
||||
virtual_lines.append("")
|
||||
virtual_lines.append(f"{s_id}. {txt}")
|
||||
curr_sec = s_id
|
||||
else:
|
||||
line_str = f" {s_id}.{i_id}. {txt}"
|
||||
if s_id == sec_id and i_id == itm_id:
|
||||
line_str = f'<span class="text-rose-600 font-bold bg-rose-50 px-1.5 py-0.5 rounded border border-rose-200">{s_id}.{i_id}. {txt}</span>'
|
||||
virtual_lines.append(line_str)
|
||||
|
||||
merged_prompt = "\n".join([re.sub(r'<[^>]+>', '', l) for l in virtual_lines])
|
||||
diff_html = "\n".join(virtual_lines)
|
||||
|
||||
db_set_session_state(session_id, "PROMPT_PREVIEW", {
|
||||
"draft_text": merged_prompt,
|
||||
"action": action,
|
||||
"section_id": sec_id,
|
||||
"item_id": itm_id,
|
||||
"content": content
|
||||
})
|
||||
|
||||
with get_db_connection() as conn_fix:
|
||||
conn_fix.cursor().execute("""
|
||||
@@ -175,16 +205,19 @@ def process_chat_message(
|
||||
conn_fix.commit()
|
||||
|
||||
preview_reply = (
|
||||
f"Ваше изменение успешно предпросмотрено. Полный обновленный системный промпт теперь выглядит так:\n\n"
|
||||
f"{merged_prompt}\n\n"
|
||||
f"Для применения изменений подтвердите действие («подтверждаю») или отмените («отмена»)."
|
||||
f"Предпросмотр изменений системного промпта:\n\n"
|
||||
f"{diff_html}\n\n"
|
||||
f"Для применения подтвердите действие, отредактируйте или отмените."
|
||||
)
|
||||
|
||||
db_save_chat_message(session_id, "assistant", preview_reply, is_ephemeral=1)
|
||||
return clean_raw_tool_tags(preview_reply), db_get_chat_history(session_id), {
|
||||
return preview_reply, db_get_chat_history(session_id), {
|
||||
"type": "PROMPT_PREVIEW",
|
||||
"raw_draft": merged_prompt,
|
||||
"buttons": [
|
||||
{"label": "Подтвердить", "value": "подтверждаю", "style": "primary"},
|
||||
{"label": "Отменить", "value": "отмена", "style": "danger"}
|
||||
{"label": "Отменить", "value": "отмена", "style": "danger"},
|
||||
{"label": "✏️ Редактировать", "value": "action:open_editor", "style": "secondary"}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -223,90 +256,26 @@ def process_chat_message(
|
||||
|
||||
elif fn_name == "db_delete_task":
|
||||
res = db_delete_task(user_id=user_id, task_id=str(fn_args.get("task_id", "")).upper())
|
||||
if action_cfg and action_cfg.get("bypass_llm"):
|
||||
reply_text = action_cfg["success_template"]
|
||||
if action_cfg.get("follow_up_question"):
|
||||
reply_text += f"\n\n{action_cfg['follow_up_question']}"
|
||||
reply_text = reply_text.replace("✅", "").replace("❌", "").replace("**", "").replace("*", "").strip()
|
||||
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=action_cfg.get("is_ephemeral", 1))
|
||||
return reply_text, db_get_chat_history(session_id), {
|
||||
"type": action_cfg.get("action_type"),
|
||||
"buttons": action_cfg.get("buttons", [])
|
||||
}
|
||||
tool_result_content = json.dumps(res, ensure_ascii=False)
|
||||
|
||||
elif fn_name == "db_delete_snapshots":
|
||||
res = db_delete_snapshots(snapshot_id=fn_args.get("snapshot_id"), day_str=fn_args.get("day_str"))
|
||||
if action_cfg and action_cfg.get("bypass_llm"):
|
||||
reply_text = action_cfg["success_template"]
|
||||
if action_cfg.get("follow_up_question"):
|
||||
reply_text += f"\n\n{action_cfg['follow_up_question']}"
|
||||
reply_text = reply_text.replace("✅", "").replace("❌", "").replace("**", "").replace("*", "").strip()
|
||||
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=action_cfg.get("is_ephemeral", 1))
|
||||
return reply_text, db_get_chat_history(session_id), {
|
||||
"type": action_cfg.get("action_type"),
|
||||
"buttons": action_cfg.get("buttons", [])
|
||||
}
|
||||
tool_result_content = json.dumps(res, ensure_ascii=False)
|
||||
|
||||
messages.append({"role": "tool", "content": tool_result_content})
|
||||
|
||||
# ANCHOR[SECONDARY_PASS]
|
||||
sec_msg = call_ollama_chat(messages, is_vision=False)
|
||||
raw_content = sec_msg.get("content", "").strip().replace("**", "").replace("*", "")
|
||||
final_content = clean_raw_tool_tags(clean_output(raw_content))
|
||||
|
||||
tool_names_called = [t["function"]["name"] for t in tool_calls]
|
||||
is_output_ephemeral = 1 if any(name in ["db_get_system_prompt", "db_get_system_prompts", "db_preview_prompt_merge"] for name in tool_names_called) else 0
|
||||
|
||||
# Topic Drift & Context Guard
|
||||
if session_state and session_state.get("state_type") in ["PROMPT_PREVIEW", "PROMPT_FOLLOWUP"]:
|
||||
idle_count = db_increment_session_idle(session_id)
|
||||
logger.info(f"Смена темы ({session_state.get('state_type')}). Текущий idle_turns: {idle_count}")
|
||||
|
||||
if idle_count == 3:
|
||||
guard_note = "\n\nНапоминание: Завершить настройку системного промпта и очистить сессию?"
|
||||
final_content += guard_note
|
||||
db_save_chat_message(session_id, "assistant", final_content, is_ephemeral=is_output_ephemeral)
|
||||
return final_content, db_get_chat_history(session_id), {
|
||||
"type": "FOLLOW_UP_ACTION",
|
||||
"buttons": [
|
||||
{"label": "Да, закончить", "value": "нет, закончить настройку", "style": "primary"},
|
||||
{"label": "Показать промпт", "value": "покажи системный промпт", "style": "secondary"}
|
||||
]
|
||||
}
|
||||
elif idle_count > 3:
|
||||
logger.info(f"Автоочистка сессии: оператор переключился на другую тему (idle_turns={idle_count}).")
|
||||
db_clear_session_state(session_id)
|
||||
db_purge_ephemeral_messages(session_id)
|
||||
is_output_ephemeral = 1 if any(name in ["db_get_system_prompt", "db_get_system_prompts", "db_prompt_node_edit"] for name in tool_names_called) else 0
|
||||
|
||||
db_save_chat_message(session_id, "assistant", final_content, is_ephemeral=is_output_ephemeral)
|
||||
return final_content, db_get_chat_history(session_id), None
|
||||
|
||||
# Обычный текстовый ответ без Tool Calls
|
||||
raw_str = msg.get("content", "").strip().replace("**", "").replace("*", "")
|
||||
final_reply = clean_raw_tool_tags(clean_output(raw_str)) or "Запрос обработан."
|
||||
|
||||
if session_state and session_state.get("state_type") in ["PROMPT_PREVIEW", "PROMPT_FOLLOWUP"]:
|
||||
idle_count = db_increment_session_idle(session_id)
|
||||
logger.info(f"Текстовый диалог вне настройки ({session_state.get('state_type')}). Текущий idle_turns: {idle_count}")
|
||||
|
||||
if idle_count == 3:
|
||||
guard_note = "\n\nНапоминание: Завершить настройку системного промпта и очистить сессию?"
|
||||
final_reply += guard_note
|
||||
db_save_chat_message(session_id, "assistant", final_reply, is_ephemeral=0)
|
||||
return final_reply, db_get_chat_history(session_id), {
|
||||
"type": "FOLLOW_UP_ACTION",
|
||||
"buttons": [
|
||||
{"label": "Да, закончить", "value": "нет, закончить настройку", "style": "primary"},
|
||||
{"label": "Показать промпт", "value": "покажи системный промпт", "style": "secondary"}
|
||||
]
|
||||
}
|
||||
elif idle_count > 3:
|
||||
logger.info(f"Автоочистка сессии: оператор переключился на другую тему (idle_turns={idle_count}).")
|
||||
db_clear_session_state(session_id)
|
||||
db_purge_ephemeral_messages(session_id)
|
||||
|
||||
db_save_chat_message(session_id, "assistant", final_reply, is_ephemeral=0)
|
||||
return final_reply, db_get_chat_history(session_id), None
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
===============================================================================
|
||||
FILE: modules/web_api/llm/core/fast_path.py
|
||||
ROLE: Детерминированный быстрый перехват команд оператора без вызова LLM:
|
||||
подтверждение, отмена черновиков и завершение диалога с очисткой памяти.
|
||||
PROJECT: SCUD Orion AI (Unified Architecture)
|
||||
MODULE: web_api / llm / core
|
||||
ROLE: Детерминированный перехват подтверждения, отмены и мгновенной очистки
|
||||
эфемерного контекста без повторного вывода кнопок-вилок.
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
@@ -12,8 +14,6 @@ from typing import Dict, Any, Tuple, Optional
|
||||
|
||||
from llm.db_tools import (
|
||||
db_add_system_prompt,
|
||||
db_get_tool_action,
|
||||
db_set_session_state,
|
||||
db_clear_session_state,
|
||||
db_purge_ephemeral_messages,
|
||||
db_save_chat_message,
|
||||
@@ -31,65 +31,39 @@ def handle_fast_path_intercept(
|
||||
session_state: Optional[Dict[str, Any]]
|
||||
) -> Optional[Tuple[str, list, Optional[Dict[str, Any]]]]:
|
||||
"""
|
||||
Проверяет входящее сообщение на детерминированные команды.
|
||||
Если команда перехвачена — возвращает кортеж (reply_text, chat_history, metadata).
|
||||
Если перехват не требуется — возвращает None.
|
||||
1. Подтвердить -> запись в БД + db_purge_ephemeral_messages + инфо-текст.
|
||||
2. Отменить -> сброс стейта + db_purge_ephemeral_messages + инфо-текст.
|
||||
"""
|
||||
if not session_state or session_state.get("state_type") != "PROMPT_PREVIEW":
|
||||
return None
|
||||
|
||||
user_msg_clean = user_message.lower().strip(" .!?:;")
|
||||
|
||||
# 1. Перехват завершения работы с очисткой эфемерных сообщений
|
||||
if user_msg_clean in ["нет, спасибо", "нет, закончить настройку", "закончить настройку", "завершить", "нет"]:
|
||||
draft_text = ""
|
||||
if session_state.get("data_json") and isinstance(session_state["data_json"], dict):
|
||||
draft_text = session_state["data_json"].get("draft_text", "")
|
||||
else:
|
||||
draft_text = session_state.get("pending_data", "")
|
||||
|
||||
# 1. ПОДТВЕРДИТЬ ИЗМЕНЕНИЯ
|
||||
if user_msg_clean in ["подтверждаю", "подтвердить", "да", "сохранить", "применить", "ок", "хорошо"]:
|
||||
db_add_system_prompt("main_agent", draft_text)
|
||||
db_clear_session_state(session_id)
|
||||
deleted_count = db_purge_ephemeral_messages(session_id)
|
||||
logger.info(f"Завершена работа с инструментом. Удалено эфемерных сообщений: {deleted_count}")
|
||||
|
||||
reply_text = "Хорошо. Настройка завершена, контекст диалога чист. Чем я могу помочь дальше?"
|
||||
logger.info(f"Системный промпт сохранен в БД. Удалено эфемерных сообщений: {deleted_count}")
|
||||
|
||||
reply_text = "Системный промпт успешно сохранен и применен в базе данных."
|
||||
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=0)
|
||||
return reply_text, db_get_chat_history(session_id), None
|
||||
|
||||
# 2. Перехват подтверждения или отмены превью системного промпта
|
||||
if session_state and session_state.get("state_type") == "PROMPT_PREVIEW":
|
||||
draft_text = ""
|
||||
if session_state.get("data_json") and isinstance(session_state["data_json"], dict):
|
||||
draft_text = session_state["data_json"].get("draft_text", "")
|
||||
else:
|
||||
draft_text = session_state.get("pending_data", "")
|
||||
# 2. ОТМЕНИТЬ ИЗМЕНЕНИЯ
|
||||
elif user_msg_clean in ["отмена", "отменить", "отклонить", "назад", "стоп"]:
|
||||
db_clear_session_state(session_id)
|
||||
deleted_count = db_purge_ephemeral_messages(session_id)
|
||||
logger.info(f"Изменения промпта отменены. Удалено эфемерных сообщений: {deleted_count}")
|
||||
|
||||
if user_msg_clean in ["подтверждаю", "подтвердить", "да", "сохранить", "применить", "ок", "хорошо"]:
|
||||
db_add_system_prompt("main_agent", draft_text)
|
||||
db_set_session_state(session_id, "PROMPT_FOLLOWUP", {"idle_turns": 0})
|
||||
|
||||
action_cfg = db_get_tool_action("db_confirm_prompt_preview")
|
||||
reply_text = action_cfg["success_template"] if action_cfg else "Системный промпт успешно сохранен и применен в базе данных."
|
||||
if action_cfg and action_cfg.get("follow_up_question"):
|
||||
reply_text += f"\n\n{action_cfg['follow_up_question']}"
|
||||
|
||||
reply_text = reply_text.replace("✅", "").replace("❌", "").replace("**", "").replace("*", "").strip()
|
||||
|
||||
is_eph = action_cfg.get("is_ephemeral", 1) if action_cfg else 1
|
||||
db_save_chat_message(session_id, "user", full_user_content, is_ephemeral=1)
|
||||
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=is_eph)
|
||||
return reply_text, db_get_chat_history(session_id), {
|
||||
"type": action_cfg.get("action_type") if action_cfg else "FOLLOW_UP_ACTION",
|
||||
"buttons": action_cfg.get("buttons", []) if action_cfg else []
|
||||
}
|
||||
|
||||
elif user_msg_clean in ["отмена", "отменить", "отклонить", "назад", "стоп"]:
|
||||
db_set_session_state(session_id, "PROMPT_FOLLOWUP", {"idle_turns": 0})
|
||||
|
||||
action_cfg = db_get_tool_action("db_cancel_prompt_preview")
|
||||
reply_text = action_cfg["success_template"] if action_cfg else "Изменения системного промпта отменены."
|
||||
if action_cfg and action_cfg.get("follow_up_question"):
|
||||
reply_text += f"\n\n{action_cfg['follow_up_question']}"
|
||||
|
||||
reply_text = reply_text.replace("✅", "").replace("❌", "").replace("**", "").replace("*", "").strip()
|
||||
|
||||
is_eph = action_cfg.get("is_ephemeral", 1) if action_cfg else 1
|
||||
db_save_chat_message(session_id, "user", full_user_content, is_ephemeral=1)
|
||||
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=is_eph)
|
||||
return reply_text, db_get_chat_history(session_id), {
|
||||
"type": action_cfg.get("action_type") if action_cfg else "FOLLOW_UP_ACTION",
|
||||
"buttons": action_cfg.get("buttons", []) if action_cfg else []
|
||||
}
|
||||
reply_text = "Изменения системного промпта отменены. Контекст диалога чист."
|
||||
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=0)
|
||||
return reply_text, db_get_chat_history(session_id), None
|
||||
|
||||
return None
|
||||
@@ -1,15 +1,15 @@
|
||||
"""
|
||||
===============================================================================
|
||||
FILE: modules/web_api/llm/core/prompt_merger.py
|
||||
ROLE: Алгоритмы парсинга, предпросмотра и детерминированного слияния правок
|
||||
системного промпта (добавление, удаление и замена пунктов).
|
||||
PROJECT: SCUD Orion AI (Unified Architecture)
|
||||
MODULE: web_api / llm / core
|
||||
ROLE: Алгоритмы парсинга, предпросмотра, слияния и визуального выделения правок.
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
# ANCHOR[PROMPT_MERGER_IMPORTS]
|
||||
import re
|
||||
import logging
|
||||
from typing import Tuple
|
||||
|
||||
logger = logging.getLogger("PROMPT_MERGER")
|
||||
|
||||
@@ -17,9 +17,7 @@ logger = logging.getLogger("PROMPT_MERGER")
|
||||
# ANCHOR[PROMPT_MERGE_ENGINE]
|
||||
def build_prompt_preview_merge(current_prompt: str, user_message: str, proposed_text: str) -> str:
|
||||
"""
|
||||
Вычисляет результирующий текст промпта на основе намерения пользователя:
|
||||
1. Удаление пункта по номеру.
|
||||
2. Добавление/изменение пункта в соответствующую секцию.
|
||||
Вычисляет результирующий чистый текст промпта для записи в БД.
|
||||
"""
|
||||
user_msg_lower = user_message.lower()
|
||||
|
||||
@@ -56,4 +54,21 @@ def build_prompt_preview_merge(current_prompt: str, user_message: str, proposed_
|
||||
new_lines.append(f" {clean_item}")
|
||||
return "\n".join(new_lines)
|
||||
|
||||
return proposed_text or current_prompt
|
||||
return proposed_text or current_prompt
|
||||
|
||||
|
||||
def highlight_prompt_diff(old_prompt: str, new_prompt: str) -> str:
|
||||
"""
|
||||
Генерирует текст с подсветкой добавленных/измененных строк HTML-классами Tailwind.
|
||||
"""
|
||||
old_lines_set = {line.strip() for line in old_prompt.splitlines() if line.strip()}
|
||||
diff_lines = []
|
||||
|
||||
for line in new_prompt.splitlines():
|
||||
if line.strip() and line.strip() not in old_lines_set:
|
||||
# Выделяем новую или измененную строку красным цветом
|
||||
diff_lines.append(f'<span class="text-rose-600 font-bold bg-rose-50 px-1.5 py-0.5 rounded border border-rose-200">{line}</span>')
|
||||
else:
|
||||
diff_lines.append(line)
|
||||
|
||||
return "\n".join(diff_lines)
|
||||
@@ -1,3 +1,6 @@
|
||||
"""
|
||||
FILE: modules/web_api/llm/core/tool_injector.py
|
||||
"""
|
||||
import re
|
||||
import json
|
||||
import logging
|
||||
@@ -5,6 +8,7 @@ from typing import List, Dict, Any
|
||||
|
||||
logger = logging.getLogger("TOOL_INJECTOR")
|
||||
|
||||
|
||||
def clean_raw_tool_tags(text: str) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
@@ -14,15 +18,25 @@ def clean_raw_tool_tags(text: str) -> str:
|
||||
text = re.sub(r'</tool_call>', '', text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def clean_output(text: str) -> str:
|
||||
if not text:
|
||||
return text
|
||||
artifacts = ["почемучто", "почто", "почему что"]
|
||||
lower_text = text.lower()
|
||||
|
||||
# Фильтрация паразитных артефактов токенизатора Qwen
|
||||
artifacts = [
|
||||
"почемучка", "почемучка,", "почемучка!", "почемучка?",
|
||||
"почемучто", "почто", "почему что", "почему-то",
|
||||
"здравствуйте!", "привет!"
|
||||
]
|
||||
|
||||
cleaned = text.strip()
|
||||
for art in artifacts:
|
||||
if lower_text.startswith(art):
|
||||
text = text[len(art):].lstrip(",.!?:; -")
|
||||
return text.strip()
|
||||
if cleaned.lower().startswith(art):
|
||||
cleaned = cleaned[len(art):].lstrip(",.!?:; -")
|
||||
|
||||
return cleaned.strip()
|
||||
|
||||
|
||||
def inject_tools_if_needed(user_message: str, raw_text_content: str, tool_calls: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
if tool_calls:
|
||||
|
||||
@@ -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)}
|
||||
@@ -9,13 +9,12 @@ from .db.db_snapshots import db_get_snapshots, db_delete_snapshots
|
||||
from .db.db_prompts import (
|
||||
db_get_active_system_prompt,
|
||||
db_add_system_prompt,
|
||||
db_apply_prompt_node_action,
|
||||
db_get_tool_action,
|
||||
db_get_rules,
|
||||
db_set_session_state,
|
||||
db_get_session_state,
|
||||
db_clear_session_state,
|
||||
db_increment_session_idle,
|
||||
db_get_session_states,
|
||||
db_get_stats,
|
||||
db_get_anomalies,
|
||||
db_get_reference
|
||||
|
||||
@@ -82,18 +82,18 @@ TOOLS_SCHEMA = [
|
||||
}
|
||||
},
|
||||
{
|
||||
"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_delete_snapshots",
|
||||
"description": "УДАЛИТЬ СНАПШОТ(Ы) ИЗ БАЗЫ ДАННЫХ. Вызывай, когда пользователь явно просит удалить конкретный снапшот по ID или все снапшоты за выбранный день.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"snapshot_id": {"type": "string", "description": "Идентификатор снапшота для удаления, например Y20260805-007"},
|
||||
"day_str": {"type": "string", "description": "Дата в формате ДД.ММ.ГГГГ для удаления всех снапшотов за день"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
@@ -173,49 +173,35 @@ TOOLS_SCHEMA = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "db_add_system_prompt",
|
||||
"description": "Прямое сохранение системного промпта в БД без предварительного просмотра.",
|
||||
"name": "db_prompt_node_edit",
|
||||
"description": (
|
||||
"Сформировать предпросмотр изменения системного промпта через реляционные узлы. "
|
||||
"Вызывай при любых запросах на добавление, изменение или удаление пунктов. "
|
||||
"Раздел и пункт передавай числами (например, для пункта '2.9' -> section_id=2, item_id=9)."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Имя промпта, по умолчанию main_agent"},
|
||||
"prompt_text": {"type": "string", "description": "Полный текст системного промпта"}
|
||||
},
|
||||
"required": ["prompt_text"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "db_preview_prompt_merge",
|
||||
"description": "Сформировать предпросмотр изменения системного промпта. Вызывается при любых запросах на добавление ('добавь пункт...'), изменение или удаление пунктов системного промпта ('удали пункт...').",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt_text": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"description": "Полный текст добавляемого пункта или номер/описание удаляемого пункта."
|
||||
"enum": ["ADD", "UPDATE", "DELETE"],
|
||||
"description": "Действие: ADD (добавить пункт), UPDATE (изменить пункт), DELETE (удалить пункт)"
|
||||
},
|
||||
"section_id": {
|
||||
"type": "integer",
|
||||
"description": "Номер раздела (например, 2)"
|
||||
},
|
||||
"item_id": {
|
||||
"type": "integer",
|
||||
"description": "Номер пункта внутри раздела (например, 9)"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Текст формулировки пункта (для ADD и UPDATE). Без префикса номера!"
|
||||
}
|
||||
},
|
||||
"required": ["prompt_text"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": {}}
|
||||
"required": ["action", "section_id", "item_id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user