feat(etl): stable pipeline, exception registry in SQLite, multi-pass aggregation and db_cli
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
===============================================================================
|
||||
FILE: modules/ai_engine/agent.py
|
||||
PROJECT: SCUD Orion AI (Unified Architecture)
|
||||
MODULE: modules / ai_engine
|
||||
ROLE: Лаконичный нативный оркестратор Function Calling, диспетчер handlers
|
||||
и менеджер свободных диалогов (Topic Drift).
|
||||
|
||||
AI-CONTEXT-ANCHORS:
|
||||
- ANCHOR[AGENT_PIPELINE_ENTRY]: Главная точка входа process_chat_message.
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Dict, Any, Tuple, Optional
|
||||
|
||||
from modules.web_api.llm.schemas import TOOLS_SCHEMA
|
||||
from modules.web_api.llm.core.ollama_client import call_ollama_chat
|
||||
from modules.web_api.llm.core.fast_path import handle_fast_path_intercept
|
||||
from modules.web_api.llm.core.tool_injector import clean_raw_tool_tags, clean_output, inject_tools_if_needed
|
||||
from modules.web_api.llm.core.context_manager import mark_last_user_message_ephemeral, close_tool_session_and_cleanup
|
||||
from modules.web_api.llm.db.db_chat import db_save_chat_message, db_get_chat_history, db_purge_ephemeral_messages
|
||||
from modules.web_api.llm.db.db_prompts import (
|
||||
db_get_session_state, db_clear_session_state, db_set_session_state,
|
||||
db_get_stats, db_get_anomalies, db_get_reference
|
||||
)
|
||||
from services.knowledge.service import get_rules
|
||||
|
||||
from .context_builder import build_agent_system_context
|
||||
from .handlers.task_handler import handle_tasks_call
|
||||
from .handlers.prompt_handler import handle_prompt_call
|
||||
from .handlers.snapshot_handler import handle_snapshots_call
|
||||
|
||||
logger = logging.getLogger("AI_AGENT")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
# ANCHOR[AGENT_PIPELINE_ENTRY]
|
||||
def process_chat_message(
|
||||
user_id: int,
|
||||
user_message: str,
|
||||
file_context: str = "",
|
||||
image_b64: Optional[str] = None,
|
||||
chat_history: List[Dict[str, Any]] = None,
|
||||
session_id: str = "web_session_main"
|
||||
) -> Tuple[str, List[Dict[str, Any]], Optional[Dict[str, Any]]]:
|
||||
"""Главный конвейер обработки сообщений чата."""
|
||||
logger.info(f"Сообщение от user_id={user_id}, session_id={session_id}: {user_message}")
|
||||
|
||||
session_state = db_get_session_state(session_id)
|
||||
if not session_state:
|
||||
db_purge_ephemeral_messages(session_id)
|
||||
|
||||
full_user_content = f"{user_message}\n\n[СОДЕРЖИМОЕ ПРИКРЕПЛЕННОГО ФАЙЛА]:\n{file_context}" if file_context else user_message
|
||||
|
||||
# 1. Быстрый Fast-Path перехват кнопок подтверждения
|
||||
fast_path_res = handle_fast_path_intercept(session_id, user_message, full_user_content, session_state)
|
||||
if fast_path_res:
|
||||
return fast_path_res
|
||||
|
||||
db_save_chat_message(session_id, "user", full_user_content, is_ephemeral=0)
|
||||
db_history = db_get_chat_history(session_id, limit=20)
|
||||
system_prompt = build_agent_system_context(user_id, session_state)
|
||||
|
||||
user_msg_obj = {"role": "user", "content": full_user_content}
|
||||
|
||||
try:
|
||||
if image_b64:
|
||||
user_msg_obj["images"] = [image_b64]
|
||||
messages = [{"role": "system", "content": "Строгий модуль OCR. Перепиши весь текст буква в букву."}, user_msg_obj]
|
||||
msg = call_ollama_chat(messages, is_vision=True)
|
||||
else:
|
||||
clean_history = [dict(m) for m in db_history]
|
||||
for m in clean_history: m.pop("images", None)
|
||||
messages = [{"role": "system", "content": system_prompt}] + clean_history + [user_msg_obj]
|
||||
msg = call_ollama_chat(messages, tools=TOOLS_SCHEMA, is_vision=False)
|
||||
|
||||
raw_reply = msg.get("content", "")
|
||||
tool_calls = msg.get("tool_calls", [])
|
||||
|
||||
# 2. Гибридный семантический классификатор намерений (Fallback Safety Net)
|
||||
tool_calls = inject_tools_if_needed(user_message, raw_reply, tool_calls)
|
||||
|
||||
# 3. Исполнение инструментов через изолированные handlers
|
||||
if tool_calls:
|
||||
tool = tool_calls[0]
|
||||
fn_name = tool["function"]["name"]
|
||||
fn_args = tool["function"].get("arguments", {})
|
||||
if isinstance(fn_args, str):
|
||||
try: fn_args = json.loads(fn_args)
|
||||
except Exception: fn_args = {}
|
||||
|
||||
logger.info(f"Вызов инструмента: {fn_name} с аргументами: {fn_args}")
|
||||
close_tool_session_and_cleanup(session_id, close_reason=f"ACTIVATE_{fn_name}")
|
||||
mark_last_user_message_ephemeral(session_id)
|
||||
|
||||
state_data = session_state.get("data_json") or {} if session_state else {}
|
||||
|
||||
if fn_name in ["db_get_tasks", "db_tasks_edit", "db_add_task", "db_update_task_status", "db_delete_task"]:
|
||||
return handle_tasks_call(fn_name, fn_args, user_id, session_id)
|
||||
|
||||
elif fn_name in ["db_get_system_prompt", "db_prompt_node_edit"]:
|
||||
return handle_prompt_call(fn_name, fn_args, session_id)
|
||||
|
||||
elif fn_name in ["db_get_snapshots", "db_delete_snapshots"]:
|
||||
return handle_snapshots_call(fn_name, fn_args, session_id, user_message, state_data)
|
||||
|
||||
elif fn_name == "db_get_rules":
|
||||
res_str = json.dumps(get_rules(), ensure_ascii=False)
|
||||
elif fn_name == "db_get_stats":
|
||||
res_str = json.dumps(db_get_stats(), ensure_ascii=False)
|
||||
elif fn_name == "db_get_anomalies":
|
||||
res_str = json.dumps(db_get_anomalies(limit=fn_args.get("limit", 100), date_str=fn_args.get("date_str")), ensure_ascii=False)
|
||||
elif fn_name == "db_get_reference":
|
||||
res_str = json.dumps(db_get_reference(category=fn_args.get("category")), ensure_ascii=False)
|
||||
else:
|
||||
res_str = "{}"
|
||||
|
||||
messages.append(msg)
|
||||
messages.append({"role": "tool", "content": res_str})
|
||||
sec_msg = call_ollama_chat(messages, is_vision=False)
|
||||
final_content = clean_raw_tool_tags(clean_output(sec_msg.get("content", ""))) or "Запрос выполнен."
|
||||
db_save_chat_message(session_id, "assistant", final_content, is_ephemeral=0)
|
||||
return final_content, db_get_chat_history(session_id), None
|
||||
|
||||
# 4. Обычный содержательный диалог и управление Topic Drift
|
||||
final_reply = clean_raw_tool_tags(clean_output(raw_reply)) or "Запрос обработан."
|
||||
for artifact in ["почемучка,", "почемучка!", "почемучка?", "почемучка", "почемучто,", "почемучто", "почему-то"]:
|
||||
if final_reply.lower().startswith(artifact):
|
||||
final_reply = final_reply[len(artifact):].lstrip(",.!?:; -")
|
||||
|
||||
action_payload = None
|
||||
|
||||
# Обработка ответа "нет / спасибо" в режиме открытого инструмента
|
||||
if session_state and any(kw in user_message.lower() for kw in ["нет", "спасибо", "не надо", "готово", "хватит"]):
|
||||
close_tool_session_and_cleanup(session_id, close_reason="USER_DISMISSED_TOOL")
|
||||
db_save_chat_message(session_id, "assistant", final_reply, is_ephemeral=0)
|
||||
return final_reply, db_get_chat_history(session_id), None
|
||||
|
||||
# Инкремент счётчика шагов в сторону от инструмента (Topic Drift)
|
||||
if session_state and session_state.get("state_type") in ["PROMPT_FOLLOWUP", "PROMPT_PREVIEW", "SNAPSHOTS_VIEW"]:
|
||||
state_type = session_state.get("state_type")
|
||||
state_data = session_state.get("data_json") or {}
|
||||
if not isinstance(state_data, dict):
|
||||
state_data = {}
|
||||
|
||||
idle_turns = state_data.get("idle_turns", 0) + 1
|
||||
state_data["idle_turns"] = idle_turns
|
||||
|
||||
if idle_turns >= 4:
|
||||
# 4-й шаг не по теме: бесшумно закрываем сессию и вычищаем эфемерные карточки
|
||||
close_tool_session_and_cleanup(session_id, close_reason="TOPIC_DRIFT_TIMEOUT")
|
||||
elif idle_turns == 3:
|
||||
# 3-й шаг: выводим вежливое напоминание с кнопками
|
||||
tool_label = "системным промптом" if "PROMPT" in state_type else "снапшотами СКУД"
|
||||
guard_question = f"Желаете продолжить работу с {tool_label}?"
|
||||
final_reply += f"\n\n💡 *Напоминание:* {guard_question}"
|
||||
action_payload = {
|
||||
"type": "FOLLOW_UP_ACTION",
|
||||
"buttons": [
|
||||
{"label": "Показать снова", "value": "покажи системный промпт" if "PROMPT" in state_type else "покажи снапшоты", "style": "primary"},
|
||||
{"label": "Завершить", "value": "нет, спасибо", "style": "secondary"}
|
||||
]
|
||||
}
|
||||
db_set_session_state(session_id, state_type, state_data)
|
||||
else:
|
||||
# 1-й и 2-й шаг: фиксируем обновленный счётчик
|
||||
db_set_session_state(session_id, state_type, state_data)
|
||||
|
||||
is_ephem_reply = 1 if "актуальный системный промпт:" in final_reply.lower() else 0
|
||||
db_save_chat_message(session_id, "assistant", final_reply, is_ephemeral=is_ephem_reply)
|
||||
return final_reply, db_get_chat_history(session_id), action_payload
|
||||
|
||||
except Exception as ex:
|
||||
logger.exception(f"Ошибка в агенте: {ex}")
|
||||
return f"Внутренняя ошибка сервера: {ex}", db_get_chat_history(session_id), None
|
||||
Reference in New Issue
Block a user