feat(web): стабилизация UI, Gemini-скроллинг, роутеры контекста/снапшотов и актуализация роадмапа
This commit is contained in:
+77
-195
@@ -3,19 +3,11 @@
|
||||
FILE: modules/web_api/llm/agent.py
|
||||
PROJECT: SCUD Orion AI (Unified Architecture)
|
||||
MODULE: web_api / llm (Core Agent Coordinator)
|
||||
ROLE: Нативный оркестратор диалога, диспетчер инструментов (Function Calling)
|
||||
и управление сессионными стейтами через ContextManager.
|
||||
|
||||
AI-CONTEXT-ANCHORS:
|
||||
- ANCHOR[AGENT_IMPORTS]: Системные и доменные импорты.
|
||||
- ANCHOR[AGENT_MAIN_PIPELINE]: Основная точка входа process_chat_message.
|
||||
- ANCHOR[AGENT_SYSTEM_PROMPT]: Формирование динамического контекста.
|
||||
- ANCHOR[AGENT_TOOL_DISPATCHER]: Исполнение нативных вызовов инструментов.
|
||||
- ANCHOR[AGENT_TOPIC_DRIFT]: Защита контекста и обработка свободных тем.
|
||||
ROLE: Нативный оркестратор диалога, диспетчер Function Calling,
|
||||
передача активных срезов в контекст модели и терминальные вызовы.
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
# ANCHOR[AGENT_IMPORTS]
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
@@ -27,7 +19,7 @@ from .db_tools import (
|
||||
db_apply_prompt_node_action,
|
||||
db_get_tool_action,
|
||||
db_get_tasks,
|
||||
db_update_task_status,
|
||||
db_update_task_details,
|
||||
db_delete_task,
|
||||
db_add_task,
|
||||
db_tasks_edit,
|
||||
@@ -70,7 +62,6 @@ if not logger.handlers:
|
||||
logger.addHandler(handler)
|
||||
|
||||
|
||||
# ANCHOR[AGENT_MAIN_PIPELINE]
|
||||
def process_chat_message(
|
||||
user_id: int,
|
||||
user_message: str,
|
||||
@@ -90,7 +81,7 @@ def process_chat_message(
|
||||
|
||||
full_user_content = f"{user_message}\n\n[СОДЕРЖИМОЕ ПРИКРЕПЛЕННОГО ФАЙЛА]:\n{file_context}" if file_context else user_message
|
||||
|
||||
# 1. Быстрый перехват строго системных кнопок UI (подтверждение превью промпта)
|
||||
# 1. Быстрый перехват системных кнопок UI и терминальных действий без задержек LLM
|
||||
fast_path_res = handle_fast_path_intercept(session_id, user_message, full_user_content, session_state)
|
||||
if fast_path_res:
|
||||
return fast_path_res
|
||||
@@ -99,7 +90,6 @@ def process_chat_message(
|
||||
db_save_chat_message(session_id, "user", full_user_content, is_ephemeral=0)
|
||||
db_history = db_get_chat_history(session_id, limit=20)
|
||||
|
||||
# ANCHOR[AGENT_SYSTEM_PROMPT]
|
||||
calendar_context = get_dynamic_calendar_context()
|
||||
user_info = f"Пользователь ID={user_id}" if user_id != 0 else "Гость"
|
||||
|
||||
@@ -111,52 +101,50 @@ def process_chat_message(
|
||||
|
||||
active_state_context = ""
|
||||
if current_state_type == "PROMPT_PREVIEW":
|
||||
active_state_context = (
|
||||
"\n[ТЕКУЩИЙ РЕЖИМ: ПРЕДПРОСМОТР СИСТЕМНОГО ПРОМПТА]\n"
|
||||
"- Открыт предпросмотр изменений промпта. Для любых правок вызывай db_prompt_node_edit.\n"
|
||||
)
|
||||
elif current_state_type == "PROMPT_FOLLOWUP":
|
||||
active_state_context = (
|
||||
"\n[ТЕКУЩИЙ РЕЖИМ: СЕССИЯ РЕДАКТИРОВАНИЯ ПРОМПТА]\n"
|
||||
"- Оператор просматривает или редактирует системный промпт.\n"
|
||||
"- На любые команды вида 'удали пункт X.Y' или 'удали X.Y' ТЫ ОБЯЗАН ВЫЗВАТЬ db_prompt_node_edit с action='DELETE', section_id=X, item_id=Y.\n"
|
||||
"- На любые команды 'добавь пункт X.Y ...' вызывай action='ADD'.\n"
|
||||
"- Запрещено путать ADD и DELETE.\n"
|
||||
)
|
||||
active_state_context = "\n[ТЕКУЩИЙ РЕЖИМ: ПРЕДПРОСМОТР СИСТЕМНОГО ПРОМПТА]\n- Открыт предпросмотр изменений промпта.\n"
|
||||
elif current_state_type == "SNAPSHOTS_VIEW":
|
||||
active_date = state_data.get("query_date", "выбранную дату")
|
||||
active_state_context = f"\n[ТЕКУЩИЙ РЕЖИМ: ПРОСМОТР СНАПШОТОВ СКУД]\n- Отображаются срезы за {active_date}.\n"
|
||||
elif current_state_type == "SNAPSHOT_INSPECT":
|
||||
snap_id = state_data.get("snapshot_id", "")
|
||||
snap_date = state_data.get("log_date", "")
|
||||
records = state_data.get("records", [])
|
||||
|
||||
lines = []
|
||||
for r in records:
|
||||
st = "Присутствовал" if r.get("is_present") else "Отсутствовал"
|
||||
lines.append(f"- {r.get('fio')}: Отдел={r.get('department')}, Вход={r.get('time_in')}, Активность={r.get('first_activity')}, Выход={r.get('time_out')}, ВремяВЗдании={r.get('in_building')}, Статус={st}")
|
||||
|
||||
dump_str = "\n".join(lines)
|
||||
active_state_context = (
|
||||
f"\n[ТЕКУЩИЙ РЕЖИМ: ПРОСМОТР СНАПШОТОВ СКУД]\n"
|
||||
f"- Сейчас на экране отображаются снапшоты за {active_date}.\n"
|
||||
f"- Ты можешь форматировать, фильтровать или анализировать этот текущий срез.\n"
|
||||
f"- Если оператор запрашивает ДРУГУЮ дату или день недели, отличную от {active_date} (например: 'за вчера', 'а за 13.08', 'покажи за сегодня') — "
|
||||
f"ТЫ ОБЯЗАН СРАЗУ ВЫЗВАТЬ db_get_snapshots(date_str=...).\n"
|
||||
f"- Запрещено генерировать текст за другую дату по памяти.\n"
|
||||
)
|
||||
elif current_state_type == "SNAPSHOT_DELETE_CONFIRM":
|
||||
active_state_context = (
|
||||
"\n[ВНИМАНИЕ: ОЖИДАЕТСЯ ПОДТВЕРЖДЕНИЕ УДАЛЕНИЯ СНАПШОТОВ]\n"
|
||||
)
|
||||
elif current_state_type == "TASK_DELETE_CONFIRM":
|
||||
active_state_context = (
|
||||
"\n[ВНИМАНИЕ: ОЖИДАЕТСЯ ПОДТВЕРЖДЕНИЕ УДАЛЕНИЯ ЗАДАЧИ]\n"
|
||||
f"\n[ТЕКУЩИЙ РЕЖИМ: АКТИВНА ИНСПЕКЦИЯ СРЕЗА СКУД #{snap_id} ЗА {snap_date}]\n"
|
||||
f"Оператор сейчас изучает срез #{snap_id}. При любых вопросах о сотрудниках, их времени входа/выхода, отделах или присутствии — "
|
||||
f"ТЫ ОБЯЗАН брать данные исключительно из этого списка активного среза:\n{dump_str}\n"
|
||||
)
|
||||
|
||||
# ⭐️ Промпт с поддержкой Topic Drift и защитой от переспросов по задачам
|
||||
system_prompt_content = (
|
||||
f"Ты — интеллектуальный ассистент SCUD Orion AI.\n"
|
||||
f"Ты управляешь задачами, аналитикой СКУД и системными настройками исключительно через инструменты (tools).\n\n"
|
||||
f"Твоя основная роль — помощь оператору в кадровом аудите СКУД/1С, управлении задачами и настройками системы.\n\n"
|
||||
f"СТРОГИЕ ПРАВИЛА:\n"
|
||||
f"1. К оператору всегда обращайся на Вы.\n"
|
||||
f"2. Для получения данных всегда вызывай соответствующий инструмент:\n"
|
||||
f" - Снапшоты и срезы логов СКУД за любые даты и дни недели -> db_get_snapshots\n"
|
||||
f" - Удаление снапшотов -> db_delete_snapshots\n"
|
||||
f" - Задачи и бэклог (просмотр, создание, смена статуса, удаление, экспорт) -> db_get_tasks, db_tasks_edit\n"
|
||||
f"2. Для работы с данными системы ВСЕГДА вызывай соответствующие инструменты (tools):\n"
|
||||
f" - Снапшоты и срезы СКУД -> db_get_snapshots\n"
|
||||
f" - Удаление срезов -> db_delete_snapshots\n"
|
||||
f" - Задачи и бэклог -> db_get_tasks, db_tasks_edit\n"
|
||||
f" - Системный промпт -> db_get_system_prompt, db_prompt_node_edit\n"
|
||||
f" - База знаний и регламенты -> db_get_rules\n"
|
||||
f" - Аномалии СКУД/1С -> db_get_anomalies\n"
|
||||
f" - База знаний -> db_get_rules\n"
|
||||
f" - Статистика БД -> db_get_stats\n"
|
||||
f" - Справка -> db_get_reference\n"
|
||||
f"3. Запрещено сочинять данные от себя без вызова инструментов.\n\n"
|
||||
f"3. ЗАДАЧИ:\n"
|
||||
f" - При любых запросах на просмотр задач (включая опечатки вроде 'змдачи', 'таски', 'дела', 'покажи задачи') — "
|
||||
f"ТЫ ОБЯЗАН СРАЗУ ВЫЗВАТЬ db_get_tasks без лишних вопросов!\n"
|
||||
f" - КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО переспрашивать у оператора фильтры, статус или категорию задач текстом! "
|
||||
f"Интерактивная карточка в интерфейсе содержит все нужные фильтры.\n"
|
||||
f"4. ПРАВИЛА И РЕГЛАМЕНТЫ: При любых вопросах о правилах компании или арбитраже ТЫ ОБЯЗАН СРАЗУ вызвать db_get_rules.\n"
|
||||
f"5. Запрещено выдумывать факты и цифры по системе СКУД/1С без вызова инструментов.\n"
|
||||
f"6. ОБЩИЙ ДИАЛОГ: На любые отвлечённые, познавательные, научные или бытовые вопросы "
|
||||
f"(расстояние между планетами или городами, программирование, кругозор) отвечай полно, доброжелательно и интересно, не отказывая пользователю.\n\n"
|
||||
f"[СИСТЕМНЫЙ КАЛЕНДАРЬ СЕРВЕРА]\n"
|
||||
f"- Пользователь: {user_info}\n"
|
||||
f"- {calendar_context}\n"
|
||||
@@ -165,7 +153,6 @@ def process_chat_message(
|
||||
|
||||
user_msg_object = {"role": "user", "content": full_user_content}
|
||||
|
||||
# ANCHOR[AGENT_TOOL_DISPATCHER]
|
||||
try:
|
||||
if image_b64:
|
||||
user_msg_object["images"] = [image_b64]
|
||||
@@ -201,7 +188,7 @@ def process_chat_message(
|
||||
|
||||
logger.info(f"Вызов функции (Tool): {fn_name} с аргументами: {fn_args}")
|
||||
|
||||
# Ротация контекста: закрываем старую сессию инструмента
|
||||
# Ротация контекста
|
||||
close_tool_session_and_cleanup(session_id, close_reason=f"ACTIVATE_{fn_name}")
|
||||
session_state = None
|
||||
mark_last_user_message_ephemeral(session_id)
|
||||
@@ -217,7 +204,7 @@ def process_chat_message(
|
||||
}
|
||||
|
||||
# 2. Единый диспетчер задач
|
||||
elif fn_name in ["db_tasks_edit", "db_add_task", "db_update_task_status", "db_delete_task"]:
|
||||
elif fn_name in ["db_tasks_edit", "db_add_task", "db_update_task_details", "db_delete_task"]:
|
||||
action = fn_args.get("action", "UPDATE").upper()
|
||||
if fn_name == "db_add_task": action = "ADD"
|
||||
elif fn_name == "db_delete_task": action = "DELETE"
|
||||
@@ -239,7 +226,7 @@ def process_chat_message(
|
||||
elif action == "EXPORT":
|
||||
export_res = db_export_tasks_markdown(
|
||||
user_id=user_id,
|
||||
filename=fn_args.get("filename"),
|
||||
filename=fn_args.get("filename") or "ROADMAP.md",
|
||||
status_filter=fn_args.get("status")
|
||||
)
|
||||
reply_text = export_res.get("message", "Отчет по задачам успешно экспортирован.")
|
||||
@@ -277,96 +264,46 @@ def process_chat_message(
|
||||
# 3. Системный промпт
|
||||
elif fn_name == "db_get_system_prompt":
|
||||
active_prompt = db_get_active_system_prompt()
|
||||
reply_text = f"Актуальный системный промпт:\n\n{active_prompt}"
|
||||
reply_text = (
|
||||
"📋 **АКТУАЛЬНЫЙ СИСТЕМНЫЙ ПРОМПТ ИЗ БАЗЫ ДАННЫХ:**\n\n"
|
||||
f"```text\n{active_prompt}\n```\n\n"
|
||||
"Вы можете добавить, отредактировать или удалить любой пункт."
|
||||
)
|
||||
db_set_session_state(session_id, "PROMPT_FOLLOWUP", {"idle_turns": 0})
|
||||
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=1)
|
||||
return reply_text, db_get_chat_history(session_id), None
|
||||
|
||||
elif fn_name == "db_prompt_node_edit":
|
||||
action = str(fn_args.get("action", "ADD")).upper()
|
||||
|
||||
try:
|
||||
sec_id = int(str(fn_args.get("section_id", 3)).strip())
|
||||
except Exception:
|
||||
sec_id = 3
|
||||
|
||||
try:
|
||||
itm_id = int(str(fn_args.get("item_id", 1)).strip())
|
||||
except Exception:
|
||||
itm_id = 1
|
||||
|
||||
content = str(fn_args.get("content", "")).strip()
|
||||
baseline_prompt = db_get_active_system_prompt()
|
||||
|
||||
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}
|
||||
nodes_dict_for_draft = {k: v for k, v in nodes_dict.items() if k != (sec_id, itm_id)} if action == "DELETE" else dict(nodes_dict)
|
||||
if action != "DELETE":
|
||||
nodes_dict_for_draft[(sec_id, itm_id)] = content
|
||||
|
||||
draft_lines = []
|
||||
curr_sec = None
|
||||
for (s_id, i_id), txt in sorted(nodes_dict_for_draft.items()):
|
||||
if i_id == 0:
|
||||
if curr_sec is not None: draft_lines.append("")
|
||||
draft_lines.append(f"{s_id}. {txt}")
|
||||
curr_sec = s_id
|
||||
else:
|
||||
draft_lines.append(f" {s_id}.{i_id}. {txt}")
|
||||
merged_prompt = "\n".join(draft_lines)
|
||||
|
||||
diff_lines = []
|
||||
curr_sec = None
|
||||
display_nodes = dict(nodes_dict)
|
||||
if action != "DELETE":
|
||||
display_nodes[(sec_id, itm_id)] = content
|
||||
|
||||
for (s_id, i_id), txt in sorted(display_nodes.items()):
|
||||
if i_id == 0:
|
||||
if curr_sec is not None: diff_lines.append("")
|
||||
diff_lines.append(f"{s_id}. {txt}")
|
||||
curr_sec = s_id
|
||||
else:
|
||||
if s_id == sec_id and i_id == itm_id:
|
||||
line_str = f' <span class="line-through text-rose-600 font-bold bg-rose-50 px-1.5 py-0.5 rounded border border-rose-300 opacity-80">{s_id}.{i_id}. {txt} [УДАЛЕНИЕ]</span>' if action == "DELETE" else f' <span class="text-rose-600 font-bold bg-rose-50 px-1.5 py-0.5 rounded border border-rose-300">{s_id}.{i_id}. {txt}</span>'
|
||||
else:
|
||||
line_str = f" {s_id}.{i_id}. {txt}"
|
||||
diff_lines.append(line_str)
|
||||
|
||||
diff_html = "\n".join(diff_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,
|
||||
"idle_turns": 0
|
||||
})
|
||||
|
||||
preview_reply = f"Предпросмотр изменений системного промпта:\n\n{diff_html}\n\nДля применения подтвердите действие, отредактируйте или отмените."
|
||||
db_save_chat_message(session_id, "assistant", preview_reply, is_ephemeral=1)
|
||||
return preview_reply, db_get_chat_history(session_id), {
|
||||
"type": "PROMPT_PREVIEW",
|
||||
"raw_draft": merged_prompt,
|
||||
"baseline_prompt": baseline_prompt,
|
||||
return reply_text, db_get_chat_history(session_id), {
|
||||
"type": "PROMPT_VIEW",
|
||||
"buttons": [
|
||||
{"label": "Подтвердить", "value": "подтверждаю", "style": "primary"},
|
||||
{"label": "Отменить", "value": "отмена", "style": "danger"},
|
||||
{"label": "✏️ Редактировать", "value": "action:open_editor", "style": "secondary"}
|
||||
{"label": "✏️ Редактировать промпт", "value": "action:open_editor", "style": "primary"},
|
||||
{"label": "База знаний", "value": "покажи правила компании", "style": "secondary"}
|
||||
]
|
||||
}
|
||||
|
||||
# 4. Снапшоты СКУД
|
||||
# 4. База знаний и правила компании (терминальный возврат)
|
||||
elif fn_name == "db_get_rules":
|
||||
rules_data = db_get_rules()
|
||||
if isinstance(rules_data, dict) and "rules" in rules_data:
|
||||
rules_list = rules_data["rules"]
|
||||
elif isinstance(rules_data, list):
|
||||
rules_list = rules_data
|
||||
else:
|
||||
rules_list = [str(rules_data)]
|
||||
|
||||
formatted_rules = "\n\n".join([f"{i+1}. {r.get('rule_text', r) if isinstance(r, dict) else r}" for i, r in enumerate(rules_list)])
|
||||
reply_text = (
|
||||
"📖 **БАЗА ЗНАНИЙ И ПРАВИЛА КОМПАНИИ (КАДРОВЫЙ АРБИТРАЖ):**\n\n"
|
||||
f"```text\n{formatted_rules}\n```"
|
||||
)
|
||||
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=1)
|
||||
return reply_text, db_get_chat_history(session_id), {
|
||||
"type": "RULES_VIEW",
|
||||
"buttons": [
|
||||
{"label": "✏️ Редактировать правила", "value": "action:open_rules_editor", "style": "primary"},
|
||||
{"label": "📋 Системный промпт", "value": "покажи системный промпт", "style": "secondary"}
|
||||
]
|
||||
}
|
||||
|
||||
# 5. Снапшоты СКУД
|
||||
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)
|
||||
query_date = snapshots_res.get("query_date", "выбранную дату")
|
||||
@@ -422,41 +359,14 @@ def process_chat_message(
|
||||
"type": "SNAPSHOTS_CARD",
|
||||
"data": updated_snapshots_res
|
||||
}
|
||||
|
||||
elif fn_name == "db_delete_snapshots":
|
||||
raw_id = fn_args.get("snapshot_id") or fn_args.get("day_str")
|
||||
raw_ids = fn_args.get("snapshot_ids") or ([raw_id] if raw_id else [])
|
||||
|
||||
# Защита: итоговые Y-срезы удалять запрещено
|
||||
safe_ids = [s for s in raw_ids if not str(s).startswith("Y")]
|
||||
if not safe_ids:
|
||||
reply_text = "⚠️ Итоговый срез Y защищен от удаления. Выберите дневные снапшоты."
|
||||
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=1)
|
||||
return reply_text, db_get_chat_history(session_id), None
|
||||
|
||||
# Прямое выполнение удаления в SQLite без лишних текстовых подтверждений
|
||||
del_res = db_delete_snapshots(snapshot_ids=safe_ids)
|
||||
|
||||
# Получаем свежий реестр за ту же дату
|
||||
query_date = fn_args.get("day_str", "")
|
||||
updated_snapshots_res = db_get_snapshots(session_id=session_id, date_str=query_date)
|
||||
|
||||
reply_text = f"✅ Успешно удалено снапшотов: {len(safe_ids)} шт."
|
||||
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=1)
|
||||
return reply_text, db_get_chat_history(session_id), {
|
||||
"type": "SNAPSHOTS_CARD",
|
||||
"data": updated_snapshots_res
|
||||
}
|
||||
|
||||
# 5. Сервисные запросы
|
||||
# 6. Прочие сервисные инструменты
|
||||
elif fn_name == "db_get_current_server_time":
|
||||
tool_result_content = json.dumps(db_get_current_server_time(), ensure_ascii=False)
|
||||
elif fn_name == "db_get_stats":
|
||||
tool_result_content = json.dumps(db_get_stats(), ensure_ascii=False)
|
||||
elif fn_name == "db_get_anomalies":
|
||||
tool_result_content = 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_rules":
|
||||
tool_result_content = json.dumps(db_get_rules(), ensure_ascii=False)
|
||||
elif fn_name == "db_get_reference":
|
||||
tool_result_content = json.dumps(db_get_reference(category=fn_args.get("category")), ensure_ascii=False)
|
||||
else:
|
||||
@@ -468,43 +378,15 @@ def process_chat_message(
|
||||
raw_content = sec_msg.get("content", "").strip().replace("**", "").replace("*", "")
|
||||
final_content = clean_raw_tool_tags(clean_output(raw_content)) or "Запрос выполнен."
|
||||
|
||||
for artifact in ["почемучка,", "почемучка!", "почемучка?", "почемучка", "почемучто", "почему-то"]:
|
||||
if final_content.lower().startswith(artifact):
|
||||
final_content = final_content[len(artifact):].lstrip(",.!?:; -")
|
||||
|
||||
is_ephem = 1 if fn_name in ["db_get_snapshots", "db_get_system_prompt", "db_prompt_node_edit"] else 0
|
||||
db_save_chat_message(session_id, "assistant", final_content, is_ephemeral=is_ephem)
|
||||
db_save_chat_message(session_id, "assistant", final_content, is_ephemeral=0)
|
||||
return final_content, db_get_chat_history(session_id), None
|
||||
|
||||
# ANCHOR[AGENT_TOPIC_DRIFT]
|
||||
# Свободный диалог (Topic Drift)
|
||||
raw_str = msg.get("content", "").strip().replace("**", "").replace("*", "")
|
||||
final_reply = clean_raw_tool_tags(clean_output(raw_str)) or "Запрос обработан."
|
||||
|
||||
for artifact in ["почемучка,", "почемучка!", "почемучка?", "почемучка", "почемучто", "почему-то"]:
|
||||
if final_reply.lower().startswith(artifact):
|
||||
final_reply = final_reply[len(artifact):].lstrip(",.!?:; -")
|
||||
|
||||
action_payload = None
|
||||
|
||||
if session_state and session_state.get("state_type") in ["PROMPT_FOLLOWUP", "PROMPT_PREVIEW"]:
|
||||
idle_turns += 1
|
||||
if idle_turns > 3:
|
||||
db_clear_session_state(session_id)
|
||||
db_purge_ephemeral_messages(session_id)
|
||||
action_payload = None
|
||||
session_state = None
|
||||
elif idle_turns == 3:
|
||||
tool_action = db_get_tool_action("db_confirm_prompt_preview")
|
||||
guard_question = tool_action.get("follow_up_question", "Желаете продолжить работу с системным промптом?") if tool_action else "Желаете продолжить работу с системным промптом?"
|
||||
buttons = tool_action.get("buttons", []) if tool_action else []
|
||||
final_reply += f"\n\n💡 *Напоминание:* {guard_question}"
|
||||
action_payload = {"type": "PROMPT_FOLLOWUP", "buttons": buttons}
|
||||
db_set_session_state(session_id, "PROMPT_FOLLOWUP", {"idle_turns": idle_turns})
|
||||
else:
|
||||
db_set_session_state(session_id, session_state.get("state_type"), {"idle_turns": idle_turns})
|
||||
|
||||
db_save_chat_message(session_id, "assistant", final_reply, is_ephemeral=0)
|
||||
return final_reply, db_get_chat_history(session_id), action_payload
|
||||
return final_reply, db_get_chat_history(session_id), None
|
||||
|
||||
except Exception as ex:
|
||||
logger.exception(f"Непредвиденная ошибка агента: {ex}")
|
||||
|
||||
@@ -3,42 +3,247 @@
|
||||
FILE: modules/web_api/llm/core/fast_path.py
|
||||
PROJECT: SCUD Orion AI (Unified Architecture)
|
||||
MODULE: web_api / llm / core
|
||||
ROLE: Детерминированный мгновенный перехват нажатий кнопок подтверждения
|
||||
(без задержек LLM и обращения к Ollama).
|
||||
|
||||
AI-CONTEXT-ANCHORS:
|
||||
- ANCHOR[FAST_PATH_MAIN]: Точка входа handle_fast_path_intercept.
|
||||
ROLE: Мгновенный перехват UI-действий, инспекции срезов, экспорта и Diff-превью.
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
import json
|
||||
import difflib
|
||||
import logging
|
||||
from typing import Dict, Any, Tuple, Optional
|
||||
|
||||
# Прямые вызовы чистых доменных сервисов
|
||||
from services.prompts.service import save_full_prompt_draft, apply_prompt_action, get_active_system_prompt
|
||||
from services.tasks.service import get_tasks, delete_task
|
||||
from services.knowledge.service import get_rules, add_rule
|
||||
from services.tasks.service import get_tasks, delete_task, execute_task_action
|
||||
from services.snapshots.service import get_snapshots_registry, delete_snapshots_safely
|
||||
|
||||
# Чат и сессии
|
||||
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_set_session_state, db_clear_session_state, db_get_tool_action
|
||||
from modules.web_api.llm.db.db_prompts import db_set_session_state, db_clear_session_state
|
||||
from modules.web_api.llm.core.context_manager import close_tool_session_and_cleanup
|
||||
|
||||
logger = logging.getLogger("FAST_PATH")
|
||||
|
||||
|
||||
# ANCHOR[FAST_PATH_MAIN]
|
||||
def _generate_prompt_diff_html(baseline_text: str, draft_text: str) -> str:
|
||||
base_lines = [line.rstrip() for line in baseline_text.strip().splitlines()]
|
||||
draft_lines = [line.rstrip() for line in draft_text.strip().splitlines()]
|
||||
|
||||
matcher = difflib.SequenceMatcher(None, base_lines, draft_lines)
|
||||
diff_html_lines = []
|
||||
|
||||
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
||||
if tag == 'equal':
|
||||
for line in base_lines[i1:i2]:
|
||||
diff_html_lines.append(line)
|
||||
elif tag == 'delete':
|
||||
for line in base_lines[i1:i2]:
|
||||
diff_html_lines.append(
|
||||
f'<span class="line-through text-rose-600 font-bold bg-rose-50 px-1.5 py-0.5 rounded border border-rose-300 opacity-80">{line} [УДАЛЕНИЕ]</span>'
|
||||
)
|
||||
elif tag == 'insert':
|
||||
for line in draft_lines[j1:j2]:
|
||||
diff_html_lines.append(
|
||||
f'<span class="text-rose-600 font-bold bg-rose-50 px-1.5 py-0.5 rounded border border-rose-300">{line}</span>'
|
||||
)
|
||||
elif tag == 'replace':
|
||||
for line in base_lines[i1:i2]:
|
||||
diff_html_lines.append(
|
||||
f'<span class="line-through text-rose-600 font-bold bg-rose-50 px-1.5 py-0.5 rounded border border-rose-300 opacity-80">{line} [УДАЛЕНИЕ]</span>'
|
||||
)
|
||||
for line in draft_lines[j1:j2]:
|
||||
diff_html_lines.append(
|
||||
f'<span class="text-rose-600 font-bold bg-rose-50 px-1.5 py-0.5 rounded border border-rose-300">{line}</span>'
|
||||
)
|
||||
|
||||
return "\n".join(diff_html_lines)
|
||||
|
||||
|
||||
def handle_fast_path_intercept(
|
||||
session_id: str,
|
||||
user_message: str,
|
||||
full_user_content: str,
|
||||
session_state: Optional[Dict[str, Any]]
|
||||
) -> Optional[Tuple[str, list, Optional[Dict[str, Any]]]]:
|
||||
"""
|
||||
Мгновенный перехват нажатий кнопок подтверждения (Fast-Path).
|
||||
Возвращает (reply, history, action_type) или None, если требуется передать управление LLM.
|
||||
"""
|
||||
msg_raw = user_message.strip()
|
||||
msg_lower = msg_raw.lower()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 0.0 ЭКСПОРТ ЗАДАЧ В ФАЙЛ (МГНОВЕННО, БЕЗ ЛИШНИХ ДИАЛОГОВ)
|
||||
# -------------------------------------------------------------------------
|
||||
if any(msg_lower.startswith(p) for p in ["экспортируй задачи", "экспорт задач", "выгрузи задачи", "скачать задачи"]):
|
||||
filename = "ROADMAP.md"
|
||||
if " в " in msg_lower:
|
||||
parts = msg_raw.split(" в ", 1)[1].strip().split()
|
||||
if parts and ("." in parts[0] or parts[0].endswith("md")):
|
||||
filename = parts[0]
|
||||
|
||||
res = execute_task_action(user_id=1, action="EXPORT", filename=filename)
|
||||
reply = res.get("message", f"Отчет по задачам сформирован в `{filename}`.")
|
||||
db_save_chat_message(session_id, "assistant", reply, is_ephemeral=0)
|
||||
|
||||
action_payload = {
|
||||
"type": "FILE_DOWNLOAD_CARD",
|
||||
"filename": res.get("filename", filename),
|
||||
"download_url": res.get("download_url", "#"),
|
||||
"tasks_count": res.get("tasks_count", 0)
|
||||
}
|
||||
return reply, db_get_chat_history(session_id), action_payload
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 0.1 ИНСПЕКЦИЯ КОНКРЕТНОГО СРЕЗА СКУД (ПО ID СНАПШОТА)
|
||||
# -------------------------------------------------------------------------
|
||||
if msg_lower.startswith("покажи срез ") or msg_lower.startswith("инспекция среза "):
|
||||
target_snap_id = msg_raw.split()[-1].replace("#", "").strip()
|
||||
from core.database import load_scud_from_db_by_snapshot
|
||||
|
||||
df_snap = load_scud_from_db_by_snapshot(date_str="", snapshot_param=target_snap_id)
|
||||
if df_snap is None or df_snap.empty:
|
||||
reply = f"⚠️ Срез СКУД `#{target_snap_id}` не найден в базе данных."
|
||||
db_save_chat_message(session_id, "assistant", reply, is_ephemeral=1)
|
||||
return reply, db_get_chat_history(session_id), None
|
||||
|
||||
total_cnt = len(df_snap)
|
||||
present_cnt = len(df_snap[df_snap['Пришел'] == True]) if 'Пришел' in df_snap.columns else 0
|
||||
absent_cnt = total_cnt - present_cnt
|
||||
|
||||
snap_time = df_snap['snapshot_time'].iloc[0] if 'snapshot_time' in df_snap.columns else '—'
|
||||
log_date = df_snap['log_date'].iloc[0] if 'log_date' in df_snap.columns else '—'
|
||||
|
||||
rows_list = []
|
||||
for _, r in df_snap.iterrows():
|
||||
rows_list.append({
|
||||
"fio": r.get('Сотрудник', r.get('fio', '')),
|
||||
"department": r.get('Подразделение', r.get('department_scud', '—')),
|
||||
"time_in": r.get('Начало_дня', 'Нет входа'),
|
||||
"first_activity": r.get('Первая_активность', '—'),
|
||||
"time_out": r.get('Конец_дня', 'Нет выхода'),
|
||||
"in_building": r.get('Находился_в_здании', '00:00'),
|
||||
"is_present": bool(r.get('Пришел', False)),
|
||||
"anomaly": r.get('anomaly_flag', 'NONE')
|
||||
})
|
||||
|
||||
# Фиксация среза в памяти сессии для последующих вопросов к LLM
|
||||
db_set_session_state(session_id, "SNAPSHOT_INSPECT", {
|
||||
"snapshot_id": target_snap_id,
|
||||
"log_date": log_date,
|
||||
"snapshot_time": snap_time,
|
||||
"records": rows_list,
|
||||
"idle_turns": 0
|
||||
})
|
||||
|
||||
reply = f"🔍 **Инспекция среза #{target_snap_id}** (Дата: {log_date}, Время: {snap_time}). Всего записей: {total_cnt} (Присутствовали: {present_cnt}, Отсутствовали: {absent_cnt})."
|
||||
db_save_chat_message(session_id, "assistant", reply, is_ephemeral=1)
|
||||
|
||||
return reply, db_get_chat_history(session_id), {
|
||||
"type": "SNAPSHOT_INSPECT_CARD",
|
||||
"snapshot_id": target_snap_id,
|
||||
"log_date": log_date,
|
||||
"snapshot_time": snap_time,
|
||||
"total_count": total_cnt,
|
||||
"present_count": present_cnt,
|
||||
"absent_count": absent_cnt,
|
||||
"records": rows_list
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 0.2 ОТКРЫТИЕ ИНЛАЙН-РЕДАКТОРА ПРОМПТА
|
||||
# -------------------------------------------------------------------------
|
||||
if msg_lower in ["action:open_editor", "редактировать промпт", "открыть редактор"]:
|
||||
active_prompt = get_active_system_prompt()
|
||||
state_data = session_state.get("data_json") if session_state else {}
|
||||
draft_text = state_data.get("draft_text") if isinstance(state_data, dict) and state_data.get("draft_text") else active_prompt
|
||||
|
||||
db_set_session_state(session_id, "PROMPT_PREVIEW", {
|
||||
"draft_text": draft_text,
|
||||
"action": "MANUAL_EDIT",
|
||||
"idle_turns": 0
|
||||
})
|
||||
reply = "✏️ Внесите необходимые изменения в текст промпта и нажмите «Показать превью изменений»:"
|
||||
db_save_chat_message(session_id, "assistant", reply, is_ephemeral=1)
|
||||
return reply, db_get_chat_history(session_id), {
|
||||
"type": "PROMPT_EDITOR",
|
||||
"raw_draft": draft_text,
|
||||
"baseline_prompt": active_prompt
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 0.3 ОТКРЫТИЕ ИНЛАЙН-РЕДАКТОРА ПРАВИЛ
|
||||
# -------------------------------------------------------------------------
|
||||
if msg_lower in ["action:open_rules_editor", "редактировать правила", "изменить правила"]:
|
||||
rules_data = get_rules()
|
||||
rules_list = rules_data.get("rules", []) if isinstance(rules_data, dict) else (rules_data if isinstance(rules_data, list) else [str(rules_data)])
|
||||
raw_rules_text = "\n".join([f"{i+1}. {r.get('rule_text', r) if isinstance(r, dict) else r}" for i, r in enumerate(rules_list)])
|
||||
|
||||
db_set_session_state(session_id, "RULES_PREVIEW", {
|
||||
"draft_text": raw_rules_text,
|
||||
"action": "MANUAL_EDIT_RULES",
|
||||
"idle_turns": 0
|
||||
})
|
||||
reply = "✏️ Редактор базы знаний и правил компании. Внесите изменения и нажмите «Показать превью изменений»:"
|
||||
db_save_chat_message(session_id, "assistant", reply, is_ephemeral=1)
|
||||
return reply, db_get_chat_history(session_id), {
|
||||
"type": "RULES_EDITOR",
|
||||
"raw_draft": raw_rules_text,
|
||||
"baseline_prompt": raw_rules_text
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 0.4 ПОСТУПЛЕНИЕ ДРАФТА ПРОМПТА -> DIFF-ПРЕВЬЮ
|
||||
# -------------------------------------------------------------------------
|
||||
if msg_raw.startswith("action:save_draft_prompt:::"):
|
||||
new_draft_content = msg_raw.replace("action:save_draft_prompt:::", "").strip()
|
||||
active_prompt = get_active_system_prompt()
|
||||
diff_html = _generate_prompt_diff_html(active_prompt, new_draft_content)
|
||||
|
||||
db_set_session_state(session_id, "PROMPT_PREVIEW", {
|
||||
"draft_text": new_draft_content,
|
||||
"action": "MANUAL_EDIT",
|
||||
"idle_turns": 0
|
||||
})
|
||||
|
||||
preview_reply = f"Предпросмотр изменений системного промпта:\n\n{diff_html}\n\nДля применения подтвердите действие, отредактируйте или отмените."
|
||||
db_save_chat_message(session_id, "assistant", preview_reply, is_ephemeral=1)
|
||||
return preview_reply, db_get_chat_history(session_id), {
|
||||
"type": "PROMPT_PREVIEW",
|
||||
"raw_draft": new_draft_content,
|
||||
"baseline_prompt": active_prompt,
|
||||
"diff_html": diff_html,
|
||||
"buttons": [
|
||||
{"label": "Подтвердить", "value": "подтверждаю", "style": "primary"},
|
||||
{"label": "Отменить", "value": "отмена", "style": "danger"},
|
||||
{"label": "✏️ Редактировать", "value": "action:open_editor", "style": "secondary"}
|
||||
]
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 0.5 ПОСТУПЛЕНИЕ ДРАФТА ПРАВИЛ -> DIFF-ПРЕВЬЮ
|
||||
# -------------------------------------------------------------------------
|
||||
if msg_raw.startswith("action:save_draft_rules:::"):
|
||||
new_draft_content = msg_raw.replace("action:save_draft_rules:::", "").strip()
|
||||
rules_data = get_rules()
|
||||
rules_list = rules_data.get("rules", []) if isinstance(rules_data, dict) else (rules_data if isinstance(rules_data, list) else [str(rules_data)])
|
||||
baseline_rules = "\n".join([f"{i+1}. {r.get('rule_text', r) if isinstance(r, dict) else r}" for i, r in enumerate(rules_list)])
|
||||
diff_html = _generate_prompt_diff_html(baseline_rules, new_draft_content)
|
||||
|
||||
db_set_session_state(session_id, "RULES_PREVIEW", {
|
||||
"draft_text": new_draft_content,
|
||||
"action": "MANUAL_EDIT_RULES",
|
||||
"idle_turns": 0
|
||||
})
|
||||
|
||||
preview_reply = f"Предпросмотр изменений базы знаний компании:\n\n{diff_html}\n\nПодтвердите сохранение или отмените действие."
|
||||
db_save_chat_message(session_id, "assistant", preview_reply, is_ephemeral=1)
|
||||
return preview_reply, db_get_chat_history(session_id), {
|
||||
"type": "PROMPT_PREVIEW",
|
||||
"raw_draft": new_draft_content,
|
||||
"baseline_prompt": baseline_rules,
|
||||
"diff_html": diff_html,
|
||||
"buttons": [
|
||||
{"label": "Подтвердить", "value": "подтверждаю сохранение правил", "style": "primary"},
|
||||
{"label": "Отменить", "value": "отмена", "style": "danger"},
|
||||
{"label": "✏️ Редактировать", "value": "action:open_rules_editor", "style": "secondary"}
|
||||
]
|
||||
}
|
||||
|
||||
if not session_state:
|
||||
return None
|
||||
|
||||
@@ -47,38 +252,22 @@ def handle_fast_path_intercept(
|
||||
if not isinstance(state_data, dict):
|
||||
state_data = {}
|
||||
|
||||
msg_lower = user_message.strip().lower()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 1. ПОДТВЕРЖДЕНИЕ ПРЕВЬЮ СИСТЕМНОГО ПРОМПТА
|
||||
# 1. ПОДТВЕРЖДЕНИЕ ПРОМПТА
|
||||
# -------------------------------------------------------------------------
|
||||
if state_type == "PROMPT_PREVIEW":
|
||||
is_confirm = msg_lower in ["подтверждаю", "да", "сохраняй", "применить", "ок", "подтвердить"]
|
||||
is_cancel = msg_lower in ["отмена", "отменить", "нет", "отклонить"]
|
||||
|
||||
if is_confirm:
|
||||
action = state_data.get("action", "MANUAL_EDIT")
|
||||
draft_text = state_data.get("draft_text", "")
|
||||
|
||||
# Применение изменений
|
||||
if action == "MANUAL_EDIT" and draft_text:
|
||||
if draft_text:
|
||||
save_full_prompt_draft(draft_text, prompt_name="main_agent")
|
||||
else:
|
||||
sec_id = state_data.get("section_id")
|
||||
itm_id = state_data.get("item_id")
|
||||
content = state_data.get("content", "")
|
||||
if sec_id is not None and itm_id is not None:
|
||||
apply_prompt_action(action=action, section_id=sec_id, item_id=itm_id, content=content)
|
||||
elif draft_text:
|
||||
save_full_prompt_draft(draft_text, prompt_name="main_agent")
|
||||
|
||||
# Закрытие сессии и зачистка
|
||||
close_tool_session_and_cleanup(session_id, close_reason="PROMPT_APPLIED_SUCCESSFULLY")
|
||||
db_clear_session_state(session_id)
|
||||
|
||||
tool_action = db_get_tool_action("db_confirm_prompt_preview")
|
||||
reply = tool_action.get("success_template", "✅ Системный промпт успешно сохранен и применен в базе данных.") if tool_action else "✅ Системный промпт успешно сохранен и применен в базе данных."
|
||||
|
||||
reply = "✅ Системный промпт успешно сохранен и применен в базе данных."
|
||||
db_save_chat_message(session_id, "user", full_user_content, is_ephemeral=0)
|
||||
db_save_chat_message(session_id, "assistant", reply, is_ephemeral=0)
|
||||
return reply, db_get_chat_history(session_id), None
|
||||
@@ -87,80 +276,40 @@ def handle_fast_path_intercept(
|
||||
close_tool_session_and_cleanup(session_id, close_reason="PROMPT_EDIT_CANCELLED")
|
||||
db_clear_session_state(session_id)
|
||||
|
||||
tool_action = db_get_tool_action("db_cancel_prompt_preview")
|
||||
reply = tool_action.get("success_template", "❌ Изменения системного промпта отменены.") if tool_action else "❌ Изменения системного промпта отменены."
|
||||
|
||||
reply = "❌ Изменения системного промпта отменены."
|
||||
db_save_chat_message(session_id, "user", full_user_content, is_ephemeral=0)
|
||||
db_save_chat_message(session_id, "assistant", reply, is_ephemeral=0)
|
||||
return reply, db_get_chat_history(session_id), None
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 2. ПОДТВЕРЖДЕНИЕ УДАЛЕНИЯ СНАПШОТОВ СКУД
|
||||
# 2. ПОДТВЕРЖДЕНИЕ ПРАВИЛ
|
||||
# -------------------------------------------------------------------------
|
||||
elif state_type == "SNAPSHOT_DELETE_CONFIRM":
|
||||
is_confirm = "подтверждаю удаление снапшот" in msg_lower or msg_lower in ["подтверждаю", "да", "удалить"]
|
||||
elif state_type == "RULES_PREVIEW":
|
||||
is_confirm = "сохранение правил" in msg_lower or msg_lower in ["подтверждаю", "да", "сохраняй", "применить"]
|
||||
is_cancel = msg_lower in ["отмена", "отменить", "нет"]
|
||||
|
||||
if is_confirm:
|
||||
snap_ids = state_data.get("snapshot_ids", [])
|
||||
query_date = state_data.get("query_date", "")
|
||||
draft_text = state_data.get("draft_text", "")
|
||||
lines = [l.strip() for l in draft_text.splitlines() if l.strip()]
|
||||
for line in lines:
|
||||
clean_line = line
|
||||
if line[0].isdigit() and "." in line[:5]:
|
||||
clean_line = line.split(".", 1)[1].strip()
|
||||
add_rule(clean_line)
|
||||
|
||||
# Безопасное удаление через сервис
|
||||
del_res = delete_snapshots_safely(snapshot_ids=snap_ids)
|
||||
|
||||
close_tool_session_and_cleanup(session_id, close_reason="SNAPSHOTS_DELETED")
|
||||
|
||||
# Получаем свежий список за ту же дату
|
||||
updated_data = get_snapshots_registry(date_str=query_date if query_date else None)
|
||||
db_set_session_state(session_id, "SNAPSHOTS_VIEW", updated_data)
|
||||
|
||||
reply = f"✅ Успешно удалено снапшотов: {len(snap_ids)} шт."
|
||||
db_save_chat_message(session_id, "user", full_user_content, is_ephemeral=1)
|
||||
db_save_chat_message(session_id, "assistant", reply, is_ephemeral=1)
|
||||
|
||||
return reply, db_get_chat_history(session_id), {
|
||||
"type": "SNAPSHOTS_CARD",
|
||||
"data": updated_data
|
||||
}
|
||||
|
||||
elif is_cancel:
|
||||
close_tool_session_and_cleanup(session_id, close_reason="SNAPSHOT_DELETE_CANCELLED")
|
||||
close_tool_session_and_cleanup(session_id, close_reason="RULES_SAVED_SUCCESS")
|
||||
db_clear_session_state(session_id)
|
||||
|
||||
reply = "❌ Удаление снапшотов отменено."
|
||||
|
||||
reply = "✅ База знаний и правила компании успешно сохранены в базе данных."
|
||||
db_save_chat_message(session_id, "user", full_user_content, is_ephemeral=0)
|
||||
db_save_chat_message(session_id, "assistant", reply, is_ephemeral=0)
|
||||
return reply, db_get_chat_history(session_id), None
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 3. ПОДТВЕРЖДЕНИЕ УДАЛЕНИЯ ЗАДАЧИ
|
||||
# -------------------------------------------------------------------------
|
||||
elif state_type == "TASK_DELETE_CONFIRM":
|
||||
is_confirm = "подтверждаю удаление задачи" in msg_lower or msg_lower in ["подтверждаю", "да", "удалить"]
|
||||
is_cancel = msg_lower in ["отмена", "отменить", "нет"]
|
||||
|
||||
if is_confirm:
|
||||
task_id = state_data.get("task_id")
|
||||
delete_task(user_id=1, task_id=str(task_id))
|
||||
|
||||
close_tool_session_and_cleanup(session_id, close_reason="TASK_DELETED")
|
||||
db_clear_session_state(session_id)
|
||||
|
||||
raw_tasks = get_tasks(user_id=1)
|
||||
reply = f"🗑 Задача #{task_id} удалена."
|
||||
db_save_chat_message(session_id, "user", full_user_content, is_ephemeral=1)
|
||||
db_save_chat_message(session_id, "assistant", reply, is_ephemeral=1)
|
||||
|
||||
return reply, db_get_chat_history(session_id), {
|
||||
"type": "TASK_INTERACTIVE_CARD",
|
||||
"tasks": raw_tasks
|
||||
}
|
||||
|
||||
elif is_cancel:
|
||||
close_tool_session_and_cleanup(session_id, close_reason="TASK_DELETE_CANCELLED")
|
||||
close_tool_session_and_cleanup(session_id, close_reason="RULES_EDIT_CANCELLED")
|
||||
db_clear_session_state(session_id)
|
||||
|
||||
reply = "❌ Удаление задачи отменено."
|
||||
reply = "❌ Изменение правил компании отменено."
|
||||
db_save_chat_message(session_id, "user", full_user_content, is_ephemeral=0)
|
||||
db_save_chat_message(session_id, "assistant", reply, is_ephemeral=0)
|
||||
return reply, db_get_chat_history(session_id), None
|
||||
|
||||
@@ -15,7 +15,7 @@ from typing import Dict, Any, Optional
|
||||
logger = logging.getLogger("OLLAMA_CLIENT")
|
||||
|
||||
OLLAMA_URL = "http://10.121.17.227:11434/api/chat"
|
||||
TEXT_MODEL = "qwen2.5:14b-instruct-q8_0"
|
||||
TEXT_MODEL = "qwen2.5:14b"
|
||||
VISION_MODEL = "qwen2.5vl:7b-q8_0"
|
||||
|
||||
LLM_OPTIONS = {
|
||||
|
||||
@@ -3,11 +3,7 @@
|
||||
FILE: modules/web_api/llm/core/tool_injector.py
|
||||
PROJECT: SCUD Orion AI (Unified Architecture)
|
||||
MODULE: web_api / llm / core
|
||||
ROLE: Семантический анализ намерений оператора (Intent Classifier) и
|
||||
детерминированная сборка вызовов инструментов при сбоях нативного Function Calling.
|
||||
|
||||
AI-CONTEXT-ANCHORS:
|
||||
- ANCHOR[INTENT_INJECTOR_MAIN]: Точка входа inject_tools_if_needed.
|
||||
ROLE: Базовая санитарная очистка вывода и тегов инструментов.
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
@@ -32,107 +28,9 @@ def clean_output(text: str) -> str:
|
||||
return text.strip() if text else ""
|
||||
|
||||
|
||||
# ANCHOR[INTENT_INJECTOR_MAIN]
|
||||
def inject_tools_if_needed(user_message: str, raw_reply: str, existing_tool_calls: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Гибридный семантический анализатор:
|
||||
Если модель ответила текстом с сомнениями или пропустила tool_call,
|
||||
распознает доменное намерение и конструирует синтетический tool_call.
|
||||
Модель управляется через TOOLS_SCHEMA и системный контекст.
|
||||
Любые синтетические перехваты текста регулярными выражениями отключены согласно ROADMAP.
|
||||
"""
|
||||
if existing_tool_calls:
|
||||
return existing_tool_calls
|
||||
|
||||
msg_clean = user_message.strip().lower()
|
||||
|
||||
# 1. СЕМАНТИКА: Просмотр системного промпта
|
||||
# Паттерны: "покажи системный промпт", "выведи промпт", "текущий промпт", "какой сейчас системный промпт"
|
||||
if "промпт" in msg_clean and any(kw in msg_clean for kw in ["покажи", "выведи", "какой", "дай", "текст", "актуальн"]):
|
||||
logger.info("[IntentInjector] Распознано намерение просмотра системного промпта")
|
||||
return [{
|
||||
"function": {
|
||||
"name": "db_get_system_prompt",
|
||||
"arguments": {}
|
||||
}
|
||||
}]
|
||||
|
||||
# 2. СЕМАНТИКА: Удаление задач
|
||||
# Паттерны: "удали задачу 37", "убери 37 задачу", "сотри таску #37", "сними с повестки задачу 37"
|
||||
if any(kw in msg_clean for kw in ["удали", "удалить", "убери", "сотри", "сними"]) and any(kw in msg_clean for kw in ["задач", "таск"]):
|
||||
task_match = re.search(r'#?\s*(\d+)', msg_clean)
|
||||
if task_match:
|
||||
task_id = task_match.group(1)
|
||||
logger.info(f"[IntentInjector] Распознано намерение удаления задачи: #{task_id}")
|
||||
return [{
|
||||
"function": {
|
||||
"name": "db_tasks_edit",
|
||||
"arguments": {"action": "DELETE", "task_id": task_id}
|
||||
}
|
||||
}]
|
||||
|
||||
# 3. СЕМАНТИКА: Управление системным промптом (удаление и мульти-удаление)
|
||||
# Паттерны: "удали 1.8 и 3.4", "удали пункт 2.3", "вычеркни 1.8, 3.4 из промпта"
|
||||
if any(kw in msg_clean for kw in ["удали", "удалить", "убери", "вычеркни", "сотри"]) and not any(kw in msg_clean for kw in ["задач", "снапшот", "срез"]):
|
||||
node_matches = re.findall(r'(\d+)[\.\s]+(\d+)', user_message)
|
||||
if node_matches:
|
||||
formatted_nodes = [f"{s}.{i}" for s, i in node_matches]
|
||||
logger.info(f"[IntentInjector] Распознано намерение удаления узлов промпта: {formatted_nodes}")
|
||||
return [{
|
||||
"function": {
|
||||
"name": "db_prompt_node_edit",
|
||||
"arguments": {
|
||||
"action": "BATCH_DELETE" if len(formatted_nodes) > 1 else "DELETE",
|
||||
"section_id": int(node_matches[0][0]),
|
||||
"item_id": int(node_matches[0][1]),
|
||||
"nodes_list": formatted_nodes,
|
||||
"content": ""
|
||||
}
|
||||
}
|
||||
}]
|
||||
|
||||
# 4. СЕМАНТИКА: Добавление пункта промпта
|
||||
# Паттерны: "добавь 3.4 Текст", "добавь пункт 3.4 Текст", "впиши в 3.4 Текст"
|
||||
if any(kw in msg_clean for kw in ["добавь", "добавить", "впиши", "запиши"]) and not any(kw in msg_clean for kw in ["задач", "таск"]):
|
||||
add_match = re.search(r'(\d+)[\.\s]+(\d+)[\.\s\:\-]+(.*)', user_message)
|
||||
if add_match:
|
||||
sec_id = int(add_match.group(1))
|
||||
itm_id = int(add_match.group(2))
|
||||
content = add_match.group(3).strip()
|
||||
logger.info(f"[IntentInjector] Распознано намерение добавления узла промпта: {sec_id}.{itm_id}")
|
||||
return [{
|
||||
"function": {
|
||||
"name": "db_prompt_node_edit",
|
||||
"arguments": {
|
||||
"action": "ADD",
|
||||
"section_id": sec_id,
|
||||
"item_id": itm_id,
|
||||
"content": content
|
||||
}
|
||||
}
|
||||
}]
|
||||
|
||||
# 5. СЕМАНТИКА: Смена статуса задач
|
||||
if any(kw in msg_clean for kw in ["в работу", "начни", "стартуй", "за работу"]):
|
||||
task_match = re.search(r'#?\s*(\d+)', msg_clean)
|
||||
if task_match:
|
||||
task_id = task_match.group(1)
|
||||
logger.info(f"[IntentInjector] Распознано намерение взятия в работу задачи: #{task_id}")
|
||||
return [{
|
||||
"function": {
|
||||
"name": "db_tasks_edit",
|
||||
"arguments": {"action": "UPDATE", "task_id": task_id, "status": "IN_PROGRESS"}
|
||||
}
|
||||
}]
|
||||
|
||||
if any(kw in msg_clean for kw in ["заверши", "закрой", "готово", "выполнено"]):
|
||||
task_match = re.search(r'#?\s*(\d+)', msg_clean)
|
||||
if task_match:
|
||||
task_id = task_match.group(1)
|
||||
logger.info(f"[IntentInjector] Распознано намерение закрытия задачи: #{task_id}")
|
||||
return [{
|
||||
"function": {
|
||||
"name": "db_tasks_edit",
|
||||
"arguments": {"action": "UPDATE", "task_id": task_id, "status": "COMPLETED"}
|
||||
}
|
||||
}]
|
||||
|
||||
return existing_tool_calls
|
||||
@@ -22,6 +22,9 @@ from services.tasks.service import (
|
||||
delete_task as db_delete_task,
|
||||
execute_task_action as db_tasks_edit
|
||||
)
|
||||
# Защитный алиас для обратной совместимости
|
||||
db_update_task_status = db_update_task_details
|
||||
|
||||
from services.tasks.exporter import export_tasks_to_markdown as db_export_tasks_markdown
|
||||
from services.tasks.repository import normalize_task_id
|
||||
|
||||
|
||||
@@ -69,14 +69,18 @@ TOOLS_SCHEMA = [
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "db_get_tasks",
|
||||
"description": "Просмотр реестра задач и бэклога текущего пользователя.",
|
||||
"description": (
|
||||
"Просмотр реестра задач и бэклога текущего пользователя.\n"
|
||||
"Вызывай этот инструмент ВСЕГДА при любых запросах просмотра задач ('покажи задачи', 'мои задачи', опечатки 'змдачи').\n"
|
||||
"Запрещено переспрашивать статус или параметры текстом: просто вызывай функцию с аргументами {}."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["ALL", "IN_PROGRESS", "PLANNED", "COMPLETED"],
|
||||
"description": "Опциональный фильтр статуса задач"
|
||||
"description": "Опциональный фильтр статуса задач (по умолчанию ALL)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user