@@ -5,27 +5,8 @@ PROJECT: SCUD Orion AI (Unified Architecture)
MODULE: web_api / llm (Core Agent & Function Calling Dispatcher)
ROLE: Главный оркестратор взаимодействия с Ollama LLM (Qwen 2.5), разбор вызовов
инструментов (Function Calling), интеграция с декларативным реестром
действий SQLite (tool_action_registry) и управление сессионными кнопками.
AI-CONTEXT-ANCHORS & INVARIANTS:
- ANCHOR[LOGGING_CONFIG]: Настройка StreamHandler в sys.stdout с propagate=False
для предотвращения дублирования строк в systemd journalctl.
- ANCHOR[DYNAMIC_CONTEXT]: Сборка системного контекста (календарь, сессия, промпт).
- ANCHOR[INFERENCE_OPTIONS]: Параметры инференса (repeat_penalty, ctx_size) для
предотвращения урезания длинных списков моделью Qwen 2.5.
- ANCHOR[TOOL_ROUTER]: Диспетчеризация функций SQLite (CRUD задач, снапшотов, KB).
- ANCHOR[PROMPT_MERGE_LOGIC]: Универсальный парсер точечного добавления и
удаления пунктов системного промпта в режиме предпросмотра (PROMPT_PREVIEW).
- ANCHOR[DATA_DRIVEN_ACTIONS]: Универсальная обработка инструментов на базе
таблицы tool_action_registry (мгновенный возврат ответов и динамических кнопок).
- ANCHOR[SECONDARY_PASS]: Вторичный вызов LLM для формирования текстового ответа
на основе полученного tool_result.
DEPENDENCIES:
- modules/web_api/llm/db_tools.py (доступ к SQLite и реестру действий)
- modules/web_api/llm/schemas.py (TOOLS_SCHEMA)
- modules/web_api/llm/core/calendar_utils.py (get_dynamic_calendar_context)
- modules/web_api/llm/core/tool_injector.py (clean_raw_tool_tags, clean_output)
действий SQLite (tool_action_registry), детерминированный Fast-Path
для подтверждений, динамические кнопки и автоочистка эфемерных сообщений.
===============================================================================
"""
@@ -57,6 +38,7 @@ from .db_tools import (
db_get_current_server_time ,
db_save_chat_message ,
db_get_chat_history ,
db_purge_ephemeral_messages ,
db_get_stats ,
db_get_anomalies ,
db_get_session_states ,
@@ -70,7 +52,7 @@ from .core.tool_injector import clean_raw_tool_tags, clean_output, inject_tools_
# --- [SECTION 2: LOGGING CONFIGURATION] --- # ANCHOR[LOGGING_CONFIG]
logger = logging . getLogger ( " SCUD_AGENT " )
logger . setLevel ( logging . INFO )
logger . propagate = False # Исключаем дублирование сообщений в родительском логгере Uvicorn
logger . propagate = False
if not logger . handlers :
handler = logging . StreamHandler ( sys . stdout )
@@ -94,10 +76,10 @@ def process_chat_message(
) - > Tuple [ str , List [ Dict [ str , Any ] ] , Optional [ Dict [ str , Any ] ] ] :
"""
Главный конвейер обработки входящего сообщения:
1. Сохранение сообщения пользователя .
2. Формирование системного контекста и вызов Ollama .
3. Выполнение вызванного Tool (если сгенерирован) .
4. О прос Data-Driven реестра действий (tool_action_registry) или вторичный проход LLM .
1. Fast-Path перехват подтверждений/отмен при активном session_state .
2. Перехват завершения работы ( ' нет, закончить настройку ' ) с автоочисткой эфемерных сообщений .
3. Формирование системного контекста и вызов Ollama .
4. Выполнение вызванного Tool и о прос Data-Driven реестра действий.
5. Возврат кортежа: (reply_text, chat_history, action_metadata).
"""
logger . info ( f " Получено сообщение от user_id= { user_id } , session_id= { session_id } : { user_message } " )
@@ -107,34 +89,82 @@ def process_chat_message(
if file_context :
full_user_content = f " { user_message } \n \n [СОДЕРЖИМОЕ ПРИКРЕПЛЕННОГО ФАЙЛА]: \n { file_context } "
session_state = db_get_session_state ( session_id )
user_msg_clean = user_message . lower ( ) . strip ( " .!?:; " )
# --- [FAST-PATH 1: ПЕРЕХВАТ ЗАВЕРШЕНИЯ НАСТРОЙКИ С ОЧИСТКОЙ ЭФЕМЕРНОЙ ПАМЯТИ] ---
if user_msg_clean in [ " нет, спасибо " , " нет, закончить настройку " , " закончить настройку " , " завершить " , " нет " ] :
db_clear_session_state ( session_id )
deleted_count = db_purge_ephemeral_messages ( session_id )
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
# --- [FAST-PATH 2: ПЕРЕХВАТ ПОДТВЕРЖДЕНИЯ / ОТМЕНЫ ПРЕВЬЮ ПРОМПТА] ---
if session_state and session_state . get ( " state_type " ) == " PROMPT_PREVIEW " :
if user_msg_clean in [ " подтверждаю " , " подтвердить " , " да " , " сохранить " , " применить " , " ок " , " хорошо " ] :
db_add_system_prompt ( " main_agent " , session_state . get ( " pending_data " , " " ) )
db_clear_session_state ( session_id )
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 ' ] } "
db_save_chat_message ( session_id , " user " , full_user_content , is_ephemeral = 1 )
db_save_chat_message ( session_id , " assistant " , reply_text , is_ephemeral = 1 )
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_clear_session_state ( session_id )
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 ' ] } "
db_save_chat_message ( session_id , " user " , full_user_content , is_ephemeral = 1 )
db_save_chat_message ( session_id , " assistant " , reply_text , is_ephemeral = 1 )
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 [ ]
}
# 3.2. Сохраняем входящее сообщение в историю диалога
is_user_ephemeral = 1 if session_state else 0
db_save_chat_message ( session_id , " user " , full_user_content , is_ephemeral = is_user_ephemeral )
# 3.3. Извлекаем полную актуальную историю для передачи в LLM
db_history = db_get_chat_history ( session_id , limit = 20 )
db_save_chat_message ( session_id , " user " , full_user_content )
# 3.2. Сборка системного контекста и правил # ANCHOR[DYNAMIC_CONTEXT]
dynamic_prompt_text = db_get_active_system_prompt ( )
calendar_context = get_dynamic_calendar_context ( )
session_state = db_get_session_state ( session_id )
preview_status_note = " "
if session_state and session_state . get ( " state_type " ) == " PROMPT_PREVIEW " :
preview_status_note = " \n \n [АКТИВНО ПРЕВЬЮ ПРОМПТА: Ожидается подтверждение ( ' подтверждаю ' ) или отмена ( ' отмена ' ) пользователем]. "
user_info = f " Пользователь ID= { user_id } " if user_id != 0 else " Гость "
system_prompt_content = (
f " [ТЕКУЩИЙ АВТОРИЗОВАННЫЙ ПОЛЬЗОВАТЕЛЬ] \n "
f " В ы общаетесь с пользователем: { user_info } . \n \n "
f " [СИСТЕМНЫЙ КАЛЕНДАРЬ СЕРВЕРА ] \n "
f " { calendar_context } \n \n "
f " [ПРАВИЛА И СТРОГИЕ ТРИГГЕРЫ ВЫЗОВА ИНСТРУМЕНТОВ] \n "
f " 1. ТРИГГЕРЫ ПРОСМОТРА: Если запрос содержит фразы ' покажи системный промпт ' , ' покажи промпт ' , ' выведи промпт ' — ТЫ ОБЯЗАН СГЕНЕРИРОВАТЬ ToolCall: db_get_system_prompt(). Категорически ЗАПРЕЩЕНО выводить текст промпта из памяти без вызова этой функции! \n "
f " 2 . ТРИГГЕРЫ ПРАВКИ: Если запрос содержит слова ' добавь пункт ' , ' удали пункт ' , ' измени промпт ' — ТЫ ОБЯЗАН СГЕНЕРИРОВАТЬ ToolCall: db_preview_prompt_merge(prompt_text=...).\n "
f " 3 . ТРИГГЕРЫ ЗАДАЧ: При фразах ' покажи задачи ' , ' мои задачи ' , ' список дел ' — СРАЗУ генерируй ToolCall: db_get_tasks ().\n "
f " 4 . ЗАПРЕТ ТЕКСТА: Запрещено объяснять правила или писать названия функций текстом, если сработал триггер — просто вызывай функцию! \n \n "
f " ТЕКУЩИЙ АКТИВНЫЙ СИСТЕМНЫЙ ПРОМПТ: \n { dynamic_prompt_text } { preview_status_note } "
f " Ты — интеллектуальный ассистент SCUD Orion AI. "
f " Т ы управляешь задачами, аналитикой СКУД и системными настройками с помощью инструментов (tools). \n \n "
f " [ОКРУЖЕНИЕ ] \n "
f " - Пользователь: { user_info } \n "
f " - { calendar_context } \n \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 "
f " - Пользователь: ' добавь пункт 3.4. Работать от сюда и до заката. ' -> Вызов: db_preview_prompt_merge(prompt_text= ' 3.4. Работать от сюда и до заката. ' ) \n "
f " - Пользователь: ' удали пункт 3.4 ' -> Вызов: db_preview_prompt_merge(prompt_text= ' 3.4 ' ) \n "
f " - Пользователь: ' покажи системный промпт ' -> Вызов: db_get_system_prompt() \n "
f " - Пользователь: ' покажи мои задачи ' -> Вызов: db_get_tasks() \n \n "
f " [ТЕКУЩИЙ АКТИВНЫЙ СИСТЕМНЫЙ ПРОМПТ]: \n { dynamic_prompt_text } "
)
# 3.3. Параметры инференса # ANCHOR[INFERENCE_OPTIONS]
llm_options = {
" num_predict " : 8192 ,
" num_ctx " : 8192 ,
@@ -174,8 +204,7 @@ def process_chat_message(
tool_calls = msg . get ( " tool_calls " , [ ] )
raw_text_content = msg . get ( " content " , " " )
# Фоллбэк проверка через tool_injector
tool_calls = inject_tools_if_needed ( user_message , raw_text_content , tool_calls )
#tool_calls = inject_tools_if_needed(user_message, raw_text_content, tool_calls)
if tool_calls :
logger . info ( f " Ответ от Ollama получен. Tool calls: True (кол-во: { len ( tool_calls ) } ) " )
@@ -187,7 +216,6 @@ def process_chat_message(
logger . info ( f " 🚀 Вызов функции (Tool): { fn_name } с аргументами: { fn_args } " )
tool_result_content = " "
# --- 5.1. УНИВЕРСАЛЬНАЯ ПРОВЕРКА DATA-DRIVEN РЕЕСТРА (tool_action_registry) --- # ANCHOR[DATA_DRIVEN_ACTIONS]
action_cfg = db_get_tool_action ( fn_name )
if fn_name == " db_confirm_prompt_preview " :
@@ -200,7 +228,8 @@ def process_chat_message(
if action_cfg . get ( " follow_up_question " ) :
reply_text + = f " \n \n { action_cfg [ ' follow_up_question ' ] } "
db_save_chat_message ( session_id , " assistant " , reply_text )
is_eph = action_cfg . get ( " 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 " ) ,
" buttons " : action_cfg . get ( " buttons " , [ ] )
@@ -208,7 +237,7 @@ def process_chat_message(
tool_result_content = json . dumps ( { " status " : " success " } , ensure_ascii = False )
else :
err_reply = " ⚠️ Нет активного превью для подтверждения. "
db_save_chat_message ( session_id , " assistant " , err_reply )
db_save_chat_message ( session_id , " assistant " , err_reply , is_ephemeral = 1 )
return err_reply , db_get_chat_history ( session_id ) , None
elif fn_name == " db_cancel_prompt_preview " :
@@ -218,14 +247,14 @@ def process_chat_message(
if action_cfg . get ( " follow_up_question " ) :
reply_text + = f " \n \n { action_cfg [ ' follow_up_question ' ] } "
db_save_chat_message ( session_id , " assistant " , reply_text )
is_eph = action_cfg . get ( " 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 " ) ,
" buttons " : action_cfg . get ( " buttons " , [ ] )
}
tool_result_content = json . dumps ( { " status " : " cancelled " } , ensure_ascii = False )
# --- 5.2. СТАНДАРТНЫЙ РОУТИНГ ИНСТРУМЕНТОВ ---
elif fn_name == " db_get_snapshots " :
snapshots_res = db_get_snapshots ( session_id = session_id , date_str = fn_args . get ( " date_str " ) , original_user_message = user_message )
tool_result_content = json . dumps ( snapshots_res , ensure_ascii = False )
@@ -254,7 +283,8 @@ def process_chat_message(
reply_text = action_cfg [ " success_template " ]
if action_cfg . get ( " follow_up_question " ) :
reply_text + = f " \n \n { action_cfg [ ' follow_up_question ' ] } "
db_save_chat_message ( session_id , " assistant " , reply_text )
is_eph = action_cfg . get ( " 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 " ) ,
" buttons " : action_cfg . get ( " buttons " , [ ] )
@@ -312,7 +342,7 @@ def process_chat_message(
f " { proposed_text } \n \n "
f " Для применения изменений подтвердите действие («подтверждаю») или отмените («отмена»). "
)
db_save_chat_message ( session_id , " assistant " , preview_reply )
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 ) , {
" type " : " PROMPT_PREVIEW " ,
" buttons " : [
@@ -338,7 +368,8 @@ def process_chat_message(
reply_text = action_cfg [ " success_template " ]
if action_cfg . get ( " follow_up_question " ) :
reply_text + = f " \n \n { action_cfg [ ' follow_up_question ' ] } "
db_save_chat_message ( session_id , " assistant " , reply_text )
is_eph = action_cfg . get ( " 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 " ) ,
" buttons " : action_cfg . get ( " buttons " , [ ] )
@@ -354,14 +385,14 @@ def process_chat_message(
sec_res_data = json . loads ( sec_response . read ( ) . decode ( " utf-8 " ) )
raw_content = sec_res_data . get ( " message " , { } ) . get ( " content " , " " ) . strip ( ) . replace ( " ** " , " " )
final_content = clean_raw_tool_tags ( clean_output ( raw_content ) )
db_save_chat_message ( session_id , " assistant " , final_content )
db_save_chat_message ( session_id , " assistant " , final_content , is_ephemeral = 0 )
return final_content , db_get_chat_history ( session_id ) , None
# Если вызовов функций не было
raw_str = msg . get ( " content " , " " ) . strip ( ) . replace ( " ** " , " " )
content_str = clean_raw_tool_tags ( clean_output ( raw_str ) )
final_reply = content_str or " Запрос обработан. "
db_save_chat_message ( session_id , " assistant " , final_reply )
db_save_chat_message ( session_id , " assistant " , final_reply , is_ephemeral = 0 )
return final_reply , db_get_chat_history ( session_id ) , None
except Exception as ex :