178 lines
9.7 KiB
Python
178 lines
9.7 KiB
Python
"""
|
||
===============================================================================
|
||
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 |