Перед тем, как исправлять выгрузку скуд с глупым Gemini-Lite

This commit is contained in:
2026-08-21 13:13:36 +03:00
parent 7bf1af7c8f
commit baf7c69395
28 changed files with 3064 additions and 1723 deletions
View File
+84
View File
@@ -0,0 +1,84 @@
"""
===============================================================================
FILE: modules/ai_engine/context_builder.py
PROJECT: SCUD Orion AI (Unified Architecture)
MODULE: modules / ai_engine
ROLE: Динамическая сборка системного промпта из БД, календаря и контекста сессий.
AI-CONTEXT-ANCHORS:
- ANCHOR[CONTEXT_BUILDER_MAIN]: Формирование полного системного контекста для LLM.
===============================================================================
"""
from typing import Dict, Any, Optional
from services.prompts.service import get_active_system_prompt
from modules.web_api.llm.core.calendar_utils import get_dynamic_calendar_context
# ANCHOR[CONTEXT_BUILDER_MAIN]
def build_agent_system_context(
user_id: int,
session_state: Optional[Dict[str, Any]]
) -> str:
"""Формирует полный динамический системный контекст агента с правилами из БД."""
# 1. Загружаем актуальный базовый системный промпт из SQLite БД
base_system_prompt = get_active_system_prompt()
# 2. Серверный календарь и контекст дат
calendar_context = get_dynamic_calendar_context()
user_info = f"Пользователь ID={user_id}" if user_id != 0 else "Гость"
# 3. Контекст текущего активного стейта сессии
current_state_type = session_state.get("state_type") if session_state else None
state_data = session_state.get("data_json") or {} if session_state else {}
if not isinstance(state_data, dict):
state_data = {}
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"
)
elif current_state_type == "SNAPSHOTS_VIEW":
active_date = state_data.get("query_date", "выбранную дату")
active_state_context = (
f"\n[ТЕКУЩИЙ РЕЖИМ: ПРОСМОТР СНАПШОТОВ СКУД]\n"
f"- Сейчас на экране отображаются снапшоты за {active_date}.\n"
f"- Ты можешь форматировать, фильтровать или анализировать этот текущий срез.\n"
f"- Если оператор запрашивает ДРУГУЮ дату или день недели (например: 'за вчера', 'а за 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"
# 4. Сборка полного системного сообщения
return (
f"{base_system_prompt}\n\n"
f"СТРОГИЕ ПРАВИЛА ВЫЗОВА ИНСТРУМЕНТОВ:\n"
f"1. Для системного промпта:\n"
f" - 'добавь X.Y Текст' -> СРАЗУ вызывай db_prompt_node_edit(action='ADD', section_id=X, item_id=Y, content='Текст').\n"
f" - 'удали X.Y' -> СРАЗУ вызывай db_prompt_node_edit(action='DELETE', section_id=X, item_id=Y, content='').\n"
f" - 'покажи системный промпт' -> СРАЗУ вызывай db_get_system_prompt().\n"
f"2. Для задач:\n"
f" - 'удали задачу N' -> СРАЗУ вызывай db_tasks_edit(action='DELETE', task_id='N').\n"
f" - 'возьми в работу N' -> СРАЗУ вызывай db_tasks_edit(action='UPDATE', task_id='N', status='IN_PROGRESS').\n"
f" - 'заверши N' / 'готово N' -> СРАЗУ вызывай db_tasks_edit(action='UPDATE', task_id='N', status='COMPLETED').\n"
f"3. Запрещено задавать вопросы и писать подтверждения текстом — СРАЗУ вызывай соответствующий инструмент!\n\n"
f"[СИСТЕМНЫЙ КАЛЕНДАРЬ СЕРВЕРА]\n"
f"- Пользователь: {user_info}\n"
f"- {calendar_context}\n"
f"{active_state_context}"
)
@@ -0,0 +1,98 @@
"""
===============================================================================
FILE: modules/ai_engine/handlers/prompt_handler.py
PROJECT: SCUD Orion AI (Unified Architecture)
MODULE: modules / ai_engine / handlers
ROLE: Изолированная обработка команд управления системным промптом.
===============================================================================
"""
from typing import Dict, Any, Tuple, Optional
from services.prompts.service import get_active_system_prompt, create_prompt_preview
from modules.web_api.llm.db.db_chat import db_save_chat_message, db_get_chat_history
from modules.web_api.llm.db.db_prompts import db_set_session_state
# ANCHOR[PROMPT_HANDLER_DISPATCH]
def handle_prompt_call(
fn_name: str,
fn_args: Dict[str, Any],
session_id: str
) -> Tuple[str, list, Optional[Dict[str, Any]]]:
"""Обрабатывает вызовы просмотра и изменения системного промпта."""
# 1. Просмотр промпта с кнопкой быстрого перехода в редактор
if fn_name == "db_get_system_prompt":
active_prompt = get_active_system_prompt()
reply_text = f"Актуальный системный промпт:\n\n{active_prompt}"
# Сохраняем состояние сессии для возможности мгновенного редактирования и подтверждения
db_set_session_state(session_id, "PROMPT_PREVIEW", {
"draft_text": active_prompt,
"action": "MANUAL_EDIT",
"idle_turns": 0
})
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=1)
return reply_text, db_get_chat_history(session_id), {
"type": "PROMPT_PREVIEW",
"raw_draft": active_prompt,
"baseline_prompt": active_prompt,
"buttons": [
{"label": "✏️ Редактировать", "value": "action:open_editor", "style": "secondary"},
{"label": "Готово", "value": "нет, спасибо", "style": "secondary"}
]
}
# 2. Предпросмотр точечных или пакетных изменений (ADD / EDIT / DELETE / BATCH_DELETE)
action = str(fn_args.get("action", "ADD")).upper()
nodes_list = fn_args.get("nodes_list", [])
delete_nodes_tuples = []
if nodes_list:
for n_str in nodes_list:
parts = str(n_str).strip().split(".")
if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
delete_nodes_tuples.append((int(parts[0]), int(parts[1])))
sec_id = None
itm_id = None
try:
if fn_args.get("section_id") is not None:
sec_id = int(str(fn_args.get("section_id")).strip())
if fn_args.get("item_id") is not None:
itm_id = int(str(fn_args.get("item_id")).strip())
except Exception:
pass
content = str(fn_args.get("content", "")).strip()
merged_prompt, diff_html, baseline_prompt = create_prompt_preview(
action=action,
section_id=sec_id,
item_id=itm_id,
content=content,
delete_nodes=delete_nodes_tuples if delete_nodes_tuples else None
)
db_set_session_state(session_id, "PROMPT_PREVIEW", {
"draft_text": merged_prompt,
"action": "MANUAL_EDIT",
"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,
"buttons": [
{"label": "Подтвердить", "value": "подтверждаю", "style": "primary"},
{"label": "Отменить", "value": "отмена", "style": "danger"},
{"label": "✏️ Редактировать", "value": "action:open_editor", "style": "secondary"}
]
}
@@ -0,0 +1,91 @@
"""
===============================================================================
FILE: modules/ai_engine/handlers/snapshot_handler.py
PROJECT: SCUD Orion AI (Unified Architecture)
MODULE: modules / ai_engine / handlers
ROLE: Изолированная обработка запросов просмотра и удаления срезов СКУД.
AI-CONTEXT-ANCHORS:
- ANCHOR[SNAPSHOT_HANDLER_DISPATCH]: Обработка db_get_snapshots и db_delete_snapshots.
===============================================================================
"""
from typing import Dict, Any, Tuple, Optional, List
from services.snapshots.service import get_snapshots_registry, delete_snapshots_safely
from modules.web_api.llm.core.calendar_utils import parse_relative_date_ru
from modules.web_api.llm.db.db_chat import db_save_chat_message, db_get_chat_history
from modules.web_api.llm.db.db_prompts import db_set_session_state
# ANCHOR[SNAPSHOT_HANDLER_DISPATCH]
def handle_snapshots_call(
fn_name: str,
fn_args: Dict[str, Any],
session_id: str,
user_message: str,
state_data: Dict[str, Any]
) -> Tuple[str, list, Optional[Dict[str, Any]]]:
"""Обрабатывает запросы реестра и безопасного удаления срезов."""
# 1. Получение срезов
if fn_name == "db_get_snapshots":
date_param = fn_args.get("date_str")
if not date_param and user_message:
date_param = parse_relative_date_ru(user_message)
snapshots_res = get_snapshots_registry(date_str=date_param)
query_date = snapshots_res.get("query_date", "выбранную дату")
db_set_session_state(session_id, "SNAPSHOTS_VIEW", snapshots_res)
reply_text = f"Реестр срезов СКУД за {query_date}:"
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": snapshots_res
}
# 2. Удаление срезов (двухфазное подтверждение)
raw_id = fn_args.get("snapshot_id") or fn_args.get("day_str")
raw_ids = fn_args.get("snapshot_ids") or []
is_confirmed = fn_args.get("confirmed", False)
if raw_id and not raw_ids:
if isinstance(raw_id, str) and "," in raw_id:
raw_ids = [s.strip() for s in raw_id.split(",")]
else:
raw_ids = [raw_id]
safe_ids = [s.strip() for s in raw_ids if s and not str(s).strip().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
if not is_confirmed:
query_date = state_data.get("query_date", "")
db_set_session_state(session_id, "SNAPSHOT_DELETE_CONFIRM", {
"snapshot_ids": safe_ids,
"query_date": query_date,
"idle_turns": 0
})
ids_str = ", ".join(safe_ids)
reply_text = f"Вы действительно хотите удалить дневные снапшоты: {ids_str}?"
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=1)
return reply_text, db_get_chat_history(session_id), {
"type": "SNAPSHOT_DELETE_CONFIRM",
"buttons": [
{"label": f"Удалить ({len(safe_ids)} шт.)", "value": f"подтверждаю удаление снапшотов {ids_str}", "style": "danger"},
{"label": "Отмена", "value": "отмена", "style": "secondary"}
]
}
else:
delete_snapshots_safely(snapshot_ids=safe_ids)
query_date = state_data.get("query_date", "")
updated_data = get_snapshots_registry(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_data
}
@@ -0,0 +1,97 @@
"""
===============================================================================
FILE: modules/ai_engine/handlers/task_handler.py
PROJECT: SCUD Orion AI (Unified Architecture)
MODULE: modules / ai_engine / handlers
ROLE: Изолированная обработка вызовов инструментов задач (get, edit, delete, export).
AI-CONTEXT-ANCHORS:
- ANCHOR[TASK_HANDLER_DISPATCH]: Обработка вызовов db_get_tasks и db_tasks_edit.
===============================================================================
"""
from typing import Dict, Any, Tuple, Optional
from services.tasks.service import get_tasks, execute_task_action
from modules.web_api.llm.db.db_chat import db_save_chat_message, db_get_chat_history
from modules.web_api.llm.db.db_prompts import db_set_session_state
# ANCHOR[TASK_HANDLER_DISPATCH]
def handle_tasks_call(
fn_name: str,
fn_args: Dict[str, Any],
user_id: int,
session_id: str
) -> Tuple[str, list, Optional[Dict[str, Any]]]:
"""Обрабатывает нативные tool-вызовы по задачам."""
# 1. Просмотр задач
if fn_name == "db_get_tasks":
raw_tasks = get_tasks(user_id, status=fn_args.get("status"))
reply_text = "Вот интерактивный список ваших текущих задач:"
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=1)
return reply_text, db_get_chat_history(session_id), {
"type": "TASK_INTERACTIVE_CARD",
"tasks": raw_tasks
}
# 2. Модификация / Удаление / Экспорт
action = (fn_args.get("action") or "UPDATE").upper()
if fn_name == "db_add_task": action = "ADD"
elif fn_name == "db_delete_task": action = "DELETE"
elif fn_name == "db_update_task_status": action = "UPDATE"
# Двухфазное подтверждение удаления
if action == "DELETE":
task_id_raw = str(fn_args.get("task_id", "")).replace("#", "").replace("TASK-", "").strip()
db_set_session_state(session_id, "TASK_DELETE_CONFIRM", {"task_id": task_id_raw, "idle_turns": 0})
reply_text = f"Вы действительно хотите удалить задачу #{task_id_raw}?"
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=1)
return reply_text, db_get_chat_history(session_id), {
"type": "TASK_DELETE_CONFIRM",
"buttons": [
{"label": f"Удалить #{task_id_raw}", "value": f"подтверждаю удаление задачи {task_id_raw}", "style": "danger"},
{"label": "Отмена", "value": "отмена", "style": "secondary"}
]
}
# Экспорт в Markdown
elif action == "EXPORT":
export_res = execute_task_action(
user_id=user_id,
action="EXPORT",
filename=fn_args.get("filename"),
status=fn_args.get("status")
)
reply_text = export_res.get("message", "Отчет по задачам успешно экспортирован.")
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=0)
action_payload = None
if export_res.get("status") == "success":
action_payload = {
"type": "FILE_DOWNLOAD_CARD",
"filename": export_res.get("filename"),
"download_url": export_res.get("download_url"),
"tasks_count": export_res.get("tasks_count")
}
return reply_text, db_get_chat_history(session_id), action_payload
# Создание и обновление
else:
res = execute_task_action(
user_id=user_id,
action=action,
task_id=fn_args.get("task_id"),
title=fn_args.get("title"),
priority=fn_args.get("priority", "MEDIUM"),
status=fn_args.get("status"),
module=fn_args.get("module", "general"),
due_date=fn_args.get("due_date")
)
raw_tasks = get_tasks(user_id)
reply_text = res.get("message", "Операция над задачами выполнена.")
db_save_chat_message(session_id, "assistant", reply_text, is_ephemeral=1)
return reply_text, db_get_chat_history(session_id), {
"type": "TASK_INTERACTIVE_CARD",
"tasks": raw_tasks
}
+146 -65
View File
@@ -1,87 +1,168 @@
"""
===============================================================================
FILE: modules/web_api/llm/core/fast_path.py
ROLE: Детерминированный технический конвейер (НЕ для ИИ-размышлений).
Выполняет только кнопки подтверждения для удаления задач, снапшотов и промптов.
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.
===============================================================================
"""
import json
import logging
from typing import Dict, Any, Tuple, Optional
from llm.db_tools import (
db_delete_task,
db_get_tasks,
db_delete_snapshots,
db_get_snapshots,
db_add_system_prompt,
db_apply_prompt_node_action
)
from .context_manager import close_tool_session_and_cleanup, save_tool_interaction, save_dialog_interaction
# Прямые вызовы чистых доменных сервисов
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.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.core.context_manager import close_tool_session_and_cleanup
logger = logging.getLogger("FAST_PATH")
# ANCHOR[FAST_PATH_MAIN]
def handle_fast_path_intercept(
session_id: str,
user_message: str,
full_user_content: str,
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.
"""
if not session_state:
return None
state_type = session_state.get("state_type")
msg = user_message.strip().lower()
state_data = session_state.get("data_json") or {}
if not isinstance(state_data, dict):
state_data = {}
# ⭐️ Управление системным промптом (Превью)
msg_lower = user_message.strip().lower()
# -------------------------------------------------------------------------
# 1. ПОДТВЕРЖДЕНИЕ ПРЕВЬЮ СИСТЕМНОГО ПРОМПТА
# -------------------------------------------------------------------------
if state_type == "PROMPT_PREVIEW":
state_data = session_state.get("data_json") or {}
draft_text = state_data.get("draft_text", "")
action = state_data.get("action")
sec_id = state_data.get("section_id")
itm_id = state_data.get("item_id")
content = state_data.get("content", "")
is_confirm = msg_lower in ["подтверждаю", "да", "сохраняй", "применить", "ок", "подтвердить"]
is_cancel = msg_lower in ["отмена", "отменить", "нет", "отклонить"]
if msg in ["подтверждаю", "подтвердить", "да", "сохранить", "применить", "ок", "хорошо"]:
if action == "MANUAL_EDIT" or not action or sec_id is None:
if draft_text:
db_add_system_prompt("main_agent", draft_text)
if is_confirm:
action = state_data.get("action", "MANUAL_EDIT")
draft_text = state_data.get("draft_text", "")
# Применение изменений
if action == "MANUAL_EDIT" and draft_text:
save_full_prompt_draft(draft_text, prompt_name="main_agent")
else:
db_apply_prompt_node_action(action=action, section_id=sec_id, item_id=itm_id, content=content)
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, "PROMPT_APPLIED_SUCCESSFULLY")
reply_text = "✅ Изменения системного промпта успешно применены в базе данных."
save_tool_interaction(session_id, full_user_content, reply_text)
return reply_text, [], None
# Закрытие сессии и зачистка
close_tool_session_and_cleanup(session_id, close_reason="PROMPT_APPLIED_SUCCESSFULLY")
db_clear_session_state(session_id)
elif msg in ["отмена", "отменить", "отклонить"]:
close_tool_session_and_cleanup(session_id, "PROMPT_PREVIEW_CANCELLED")
reply_text = "❌ Изменения системного промпта отменены."
save_dialog_interaction(session_id, full_user_content, reply_text)
return reply_text, [], None
# Удаление снапшотов
elif state_type == "SNAPSHOT_DELETE_CONFIRM":
state_data = session_state.get("data_json", {})
ids = state_data.get("snapshot_ids", [])
query_date = state_data.get("query_date", "")
if msg.startswith("подтверждаю удаление снапшотов"):
db_delete_snapshots(snapshot_ids=ids)
close_tool_session_and_cleanup(session_id, "SNAPSHOTS_DELETED")
updated_data = db_get_snapshots(session_id=session_id, date_str=query_date)
return f"✅ Удалено снапшотов: {len(ids)}", [], {"type": "SNAPSHOTS_CARD", "data": updated_data}
elif msg == "отмена":
close_tool_session_and_cleanup(session_id, "SNAPSHOT_CANCELLED")
updated_data = db_get_snapshots(session_id=session_id, date_str=query_date)
return "Удаление отменено.", [], {"type": "SNAPSHOTS_CARD", "data": updated_data}
# Удаление задач
elif state_type == "TASK_DELETE_CONFIRM":
task_id = session_state.get("data_json", {}).get("task_id")
if msg.startswith("подтверждаю удаление задачи"):
db_delete_task(1, str(task_id))
close_tool_session_and_cleanup(session_id, "TASK_DELETED")
return f"Задача #{task_id} удалена.", [], {"type": "TASK_INTERACTIVE_CARD", "tasks": db_get_tasks(1)}
tool_action = db_get_tool_action("db_confirm_prompt_preview")
reply = tool_action.get("success_template", "✅ Системный промпт успешно сохранен и применен в базе данных.") if tool_action else "✅ Системный промпт успешно сохранен и применен в базе данных."
elif msg == "отмена":
close_tool_session_and_cleanup(session_id, "TASK_CANCELLED")
return "Удаление отменено.", [], {"type": "TASK_INTERACTIVE_CARD", "tasks": db_get_tasks(1)}
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
elif is_cancel:
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 "❌ Изменения системного промпта отменены."
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. ПОДТВЕРЖДЕНИЕ УДАЛЕНИЯ СНАПШОТОВ СКУД
# -------------------------------------------------------------------------
elif state_type == "SNAPSHOT_DELETE_CONFIRM":
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", "")
# Безопасное удаление через сервис
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")
db_clear_session_state(session_id)
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")
db_clear_session_state(session_id)
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
return None
-74
View File
@@ -1,74 +0,0 @@
"""
===============================================================================
FILE: modules/web_api/llm/core/prompt_merger.py
PROJECT: SCUD Orion AI (Unified Architecture)
MODULE: web_api / llm / core
ROLE: Алгоритмы парсинга, предпросмотра, слияния и визуального выделения правок.
===============================================================================
"""
# ANCHOR[PROMPT_MERGER_IMPORTS]
import re
import logging
logger = logging.getLogger("PROMPT_MERGER")
# ANCHOR[PROMPT_MERGE_ENGINE]
def build_prompt_preview_merge(current_prompt: str, user_message: str, proposed_text: str) -> str:
"""
Вычисляет результирующий чистый текст промпта для записи в БД.
"""
user_msg_lower = user_message.lower()
# 1. Сценарий удаления пункта
if any(w in user_msg_lower for w in ["удали", "стереть", "убрать", "вырежи", "удалить"]):
target_num_match = re.search(r'\d+(\.\d+)*', user_message)
target_num = target_num_match.group(0) if target_num_match else ""
lines = current_prompt.splitlines()
if target_num:
new_lines = [line for line in lines if not line.strip().startswith(f"{target_num}.")]
else:
new_lines = lines
return "\n".join(new_lines)
# 2. Сценарий добавления или замены пункта
if proposed_text:
if len(proposed_text) < 500:
clean_item = proposed_text.strip()
for prefix in ["добавь пункт", "добавить пункт", "вставь пункт", "добавь"]:
if prefix in clean_item.lower():
clean_item = re.sub(prefix, "", clean_item, flags=re.IGNORECASE).strip(" .:")
lines = current_prompt.splitlines()
new_lines = []
inserted = False
for line in lines:
new_lines.append(line)
if "3.3." in line and not inserted:
item_str = clean_item if re.match(r'^\d+\.\d+\.', clean_item) else f"3.4. {clean_item}"
new_lines.append(f" {item_str}")
inserted = True
if not inserted:
new_lines.append(f" {clean_item}")
return "\n".join(new_lines)
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)
-112
View File
@@ -1,112 +0,0 @@
"""
===============================================================================
FILE: modules/web_api/llm/db/db_snapshots.py
PROJECT: SCUD Orion AI (Unified Architecture)
MODULE: web_api / llm / db
ROLE: Выборка, фильтрация и пакетное удаление срезов логов СКУД в SQLite.
AI-CONTEXT-ANCHORS:
- ANCHOR[DB_GET_SNAPSHOTS]: Выборка снапшотов с нормализацией дат.
- ANCHOR[DB_DEL_SNAPSHOTS]: Удаление снапшотов по ID, списку ID или за дату.
===============================================================================
"""
import json
import re
from typing import Dict, Any, Optional, List
from .connection import get_db_connection
from .db_prompts import db_set_session_state
from ..core.calendar_utils import parse_relative_date_ru
# ANCHOR[DB_GET_SNAPSHOTS]
def db_get_snapshots(session_id: str = "web_session_main", date_str: Optional[str] = None, original_user_message: str = "") -> Dict[str, Any]:
conn = get_db_connection()
cursor = conn.cursor()
query = """
SELECT
snapshot_id,
log_date,
snapshot_time,
COUNT(*) as record_count
FROM scud_logs
"""
params = []
clean_date = date_str.strip() if date_str else ""
if not clean_date or not re.search(r'\d{2}\.\d{2}\.\d{4}', clean_date):
if original_user_message:
clean_date = parse_relative_date_ru(original_user_message)
if clean_date and re.search(r'\d{2}\.\d{2}\.\d{4}', clean_date):
iso_date = clean_date
compact_date = clean_date.replace(".", "")
if "." in clean_date:
parts = clean_date.split(".")
if len(parts) == 3:
iso_date = f"{parts[2]}-{parts[1]}-{parts[0]}"
compact_date = f"{parts[2]}{parts[1]}{parts[0]}"
query += """
WHERE log_date = ?
OR snapshot_time LIKE ?
OR snapshot_id LIKE ?
OR snapshot_id LIKE ?
"""
params.extend([clean_date, f"{iso_date}%", f"{compact_date}-%", f"Y{compact_date}-%"])
query += """
GROUP BY snapshot_id, log_date, snapshot_time
ORDER BY snapshot_time DESC, snapshot_id DESC
LIMIT 50
"""
cursor.execute(query, params)
rows = cursor.fetchall()
snapshots = [dict(r) for r in rows]
result_data = {
"query_date": clean_date or "все",
"snapshots_count": len(snapshots),
"snapshots": snapshots
}
db_set_session_state(session_id=session_id, state_type="SNAPSHOTS_VIEW", data=result_data)
conn.close()
return result_data
# ANCHOR[DB_DEL_SNAPSHOTS]
def db_delete_snapshots(
snapshot_id: Optional[str] = None,
snapshot_ids: Optional[List[str]] = None,
day_str: Optional[str] = None
) -> Dict[str, Any]:
"""Удаляет один или группу дневных снапшотов с защитой итоговых Y-снапшотов."""
conn = get_db_connection()
cursor = conn.cursor()
deleted = 0
if snapshot_ids and isinstance(snapshot_ids, list):
# Исключаем любые итоговые снапшоты, начинающиеся с Y
safe_ids = [s.strip() for s in snapshot_ids if s and not str(s).strip().startswith("Y")]
if safe_ids:
placeholders = ",".join(["?"] * len(safe_ids))
cursor.execute(f"DELETE FROM scud_logs WHERE snapshot_id IN ({placeholders})", safe_ids)
deleted = cursor.rowcount
elif snapshot_id:
clean_id = str(snapshot_id).strip()
if clean_id.startswith("Y"):
conn.close()
return {"status": "error", "message": f"Итоговый срез [{clean_id}] защищен от удаления."}
cursor.execute("DELETE FROM scud_logs WHERE snapshot_id = ?", (clean_id,))
deleted = cursor.rowcount
elif day_str:
cursor.execute("DELETE FROM scud_logs WHERE (log_date = ? OR snapshot_id LIKE ?) AND snapshot_id NOT LIKE 'Y%'", (day_str, f"%{day_str.replace('.', '')}%"))
deleted = cursor.rowcount
else:
conn.close()
return {"status": "error", "message": "Не указаны идентификаторы для удаления."}
conn.commit()
conn.close()
return {"status": "success", "deleted_records": deleted, "message": f"Успешно удалено записей: {deleted}"}
-297
View File
@@ -1,297 +0,0 @@
"""
===============================================================================
FILE: modules/web_api/llm/db/db_tasks.py
PROJECT: SCUD Orion AI (Unified Architecture)
MODULE: web_api / llm / db
ROLE: Комплексное управление задачами, единый диспетчер db_tasks_edit
и генерация структурированных отчетов в Markdown.
===============================================================================
"""
import uuid
import logging
import os
import re
from datetime import datetime
from typing import List, Dict, Any, Optional
from .connection import get_db_connection
logger = logging.getLogger("DB_TASKS")
def normalize_task_id(task_id_input: str) -> str:
"""Нормализует идентификатор задачи к формату TASK-XX."""
if not task_id_input:
return ""
clean_id = str(task_id_input).strip().upper().replace("TASK-", "").replace("TASK", "").replace("#", "")
if clean_id.isdigit():
num = int(clean_id)
return f"TASK-{(num):02d}" if num < 100 else f"TASK-{(num):03d}"
return f"TASK-{clean_id}"
def db_get_tasks(user_id: int, status: Optional[str] = None) -> List[Dict[str, Any]]:
"""Получает список всех задач пользователя."""
conn = get_db_connection()
cursor = conn.cursor()
if status and status.upper() != "ALL":
target_status = status.upper()
if target_status in ["PROGRESS", "В РАБОТЕ"]: target_status = "IN_PROGRESS"
elif target_status in ["DONE", "ГОТОВО"]: target_status = "COMPLETED"
elif target_status in ["PLANNED", "ПЛАНЫ"]: target_status = "BACKLOG"
cursor.execute("""
SELECT id, task_id, module, title, priority, status, due_date, created_at
FROM tasks
WHERE user_id = ? AND (status = ? OR (status = 'BACKLOG' AND ? = 'PLANNED'))
ORDER BY id DESC
""", (user_id, target_status, target_status))
else:
cursor.execute("""
SELECT id, task_id, module, title, priority, status, due_date, created_at
FROM tasks
WHERE user_id = ?
ORDER BY id DESC
""", (user_id,))
rows = cursor.fetchall()
conn.close()
return [dict(r) for r in rows]
def db_add_task(
user_id: int,
module: str,
title: str,
priority: str = "MEDIUM",
due_date: Optional[str] = None,
status: str = "BACKLOG"
) -> Dict[str, Any]:
"""Добавление новой задачи со статусом по умолчанию BACKLOG (В планах)."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT MAX(id) FROM tasks")
max_id = cursor.fetchone()[0] or 0
new_task_id = f"TASK-{(max_id + 1):02d}"
target_status = status.upper() if status else "BACKLOG"
if target_status in ["PROGRESS", "В РАБОТЕ"]: target_status = "IN_PROGRESS"
elif target_status in ["DONE", "ГОТОВО"]: target_status = "COMPLETED"
elif target_status in ["PLANNED", "ПЛАНЫ", "BACKLOG"]: target_status = "BACKLOG"
cursor.execute("""
INSERT INTO tasks (task_id, module, title, priority, status, due_date, user_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (new_task_id, module or "general", title.strip(), priority.upper(), target_status, due_date, user_id))
conn.commit()
conn.close()
return {"status": "success", "task_id": new_task_id, "message": f"Задача #{max_id + 1} создана и добавлена в планы"}
def db_update_task_status(user_id: int, task_id: str, status: str = "COMPLETED", due_date: Optional[str] = None) -> Dict[str, Any]:
"""Быстрое обновление статуса задачи."""
return db_update_task_details(user_id=user_id, task_id=task_id, status=status, due_date=due_date)
def db_update_task_details(
user_id: int,
task_id: str,
title: Optional[str] = None,
priority: Optional[str] = None,
status: Optional[str] = None,
due_date: Optional[str] = None
) -> Dict[str, Any]:
"""Комплексное обновление любых параметров задачи."""
conn = get_db_connection()
cursor = conn.cursor()
clean_num = re.sub(r'\D', '', str(task_id))
formatted_id = normalize_task_id(task_id)
updates = []
params = []
if title is not None and title.strip():
updates.append("title = ?")
params.append(title.strip())
if priority is not None and priority.strip():
updates.append("priority = ?")
params.append(priority.strip().upper())
if status is not None and status.strip():
target_status = status.strip().upper()
if target_status in ["PROGRESS", "В РАБОТЕ"]: target_status = "IN_PROGRESS"
elif target_status in ["DONE", "ГОТОВО"]: target_status = "COMPLETED"
elif target_status in ["PLANNED", "ПЛАНЫ"]: target_status = "BACKLOG"
updates.append("status = ?")
params.append(target_status)
if due_date is not None:
updates.append("due_date = ?")
params.append(due_date.strip() if due_date.strip() else None)
if not updates:
conn.close()
return {"status": "success", "message": "Нет данных для обновления"}
params.extend([clean_num, formatted_id, f"%{task_id.strip()}", user_id])
sql = f"""
UPDATE tasks
SET {', '.join(updates)}
WHERE (id = ? OR UPPER(task_id) = ? OR task_id LIKE ?) AND user_id = ?
"""
cursor.execute(sql, params)
if cursor.rowcount == 0:
conn.close()
return {"error": f"Задача {task_id} не найдена или принадлежит другому пользователю"}
conn.commit()
conn.close()
return {"status": "success", "message": f"Задача #{task_id} успешно обновлена"}
def db_delete_task(user_id: int, task_id: str) -> Dict[str, Any]:
"""Удаление задачи."""
conn = get_db_connection()
cursor = conn.cursor()
clean_num = re.sub(r'\D', '', str(task_id))
formatted_id = normalize_task_id(task_id)
cursor.execute("""
DELETE FROM tasks
WHERE (id = ? OR UPPER(task_id) = ? OR task_id LIKE ?) AND user_id = ?
""", (clean_num, formatted_id, f"%{task_id.strip()}", user_id))
if cursor.rowcount == 0:
conn.close()
return {"error": f"Задача {task_id} не найдена"}
conn.commit()
conn.close()
return {"status": "success", "message": f"Задача #{task_id} удалена"}
def db_export_tasks_markdown(user_id: int, filename: Optional[str] = None, status_filter: Optional[str] = None) -> Dict[str, Any]:
"""Генерирует Markdown-отчет задач с сохранением в output/web/tasks_export/{uuid}/."""
tasks = db_get_tasks(user_id)
if not tasks:
return {"status": "error", "message": "Список задач пуст, экспорт отменен"}
# Фильтрация по статусу
if status_filter and status_filter.upper() != "ALL":
tgt = status_filter.upper()
if tgt in ["COMPLETED", "DONE", "ВЫПОЛНЕННЫЕ"]:
tasks = [t for t in tasks if str(t.get("status", "")).upper() in ["COMPLETED", "DONE"]]
elif tgt in ["IN_PROGRESS", "PROGRESS", "В РАБОТЕ"]:
tasks = [t for t in tasks if str(t.get("status", "")).upper() in ["IN_PROGRESS", "PROGRESS"]]
elif tgt in ["BACKLOG", "PLANNED", "В ПЛАНАХ"]:
tasks = [t for t in tasks if str(t.get("status", "")).upper() in ["BACKLOG", "PLANNED"]]
if not tasks:
return {"status": "error", "message": f"Нет задач с фильтром '{status_filter}' для экспорта"}
# Имя файла
target_filename = filename.strip() if (filename and filename.strip()) else "ROADMAP.md"
if not target_filename.endswith(".md"):
target_filename = f"{target_filename}.md"
now_dt = datetime.now()
now_str = now_dt.strftime("%Y-%m-%d %H:%M")
modules: Dict[str, List[Dict[str, Any]]] = {}
for t in tasks:
mod = t.get("module") or "general"
modules.setdefault(mod, []).append(t)
md_lines = [
"# 🗺️ Дорожная карта задач проекта (ROADMAP)\n",
f"> **Сформировано:** {now_str} | **Всего задач:** {len(tasks)}\n",
"---\n"
]
for mod_name, mod_tasks in sorted(modules.items()):
md_lines.append(f"## Модуль `{mod_name}`\n")
for t in sorted(mod_tasks, key=lambda x: x.get("id", 0)):
status = str(t.get("status", "BACKLOG")).upper()
is_done = status in ["COMPLETED", "DONE"]
is_progress = status in ["IN_PROGRESS", "PROGRESS"]
check_box = "[x]" if is_done else "[ ]"
t_id = t.get("id")
title = t.get("title", "Без названия")
prio = t.get("priority", "MEDIUM")
due = f" *(срок: {t['due_date']})*" if t.get("due_date") else ""
status_tag = " `[В РАБОТЕ]`" if is_progress else (" `[ЗАВЕРШЕНО]`" if is_done else "")
md_lines.append(f"- {check_box} **#{t_id}** [{prio}]{status_tag} {title}{due}")
md_lines.append("\n---\n")
content = "\n".join(md_lines)
# Точный путь к корню scud_ai/output/web/tasks_export/
current_file_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.abspath(os.path.join(current_file_dir, "../../../../"))
tool_dir = os.path.join(root_dir, "output", "web", "tasks_export")
os.makedirs(tool_dir, exist_ok=True)
try:
from routers.files import purge_old_tool_sessions
purge_old_tool_sessions(tool_dir)
except Exception:
pass
session_token = uuid.uuid4().hex[:8]
session_dir = os.path.join(tool_dir, session_token)
os.makedirs(session_dir, exist_ok=True)
filepath = os.path.join(session_dir, target_filename)
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
logger.info(f"Файл успешно создан: {filepath}")
return {
"status": "success",
"filename": target_filename,
"filepath": filepath,
"download_url": f"/api/v1/files/download/tasks_export/{session_token}/{target_filename}",
"tasks_count": len(tasks),
"message": f"Отчет успешно сформирован в файл `{target_filename}` (всего задач: {len(tasks)})."
}
def db_tasks_edit(
user_id: int,
action: str,
task_id: Optional[str] = None,
title: Optional[str] = None,
priority: Optional[str] = "MEDIUM",
status: Optional[str] = None,
module: Optional[str] = "general",
due_date: Optional[str] = None,
filename: Optional[str] = "ROADMAP.md"
) -> Dict[str, Any]:
"""Единый консолидированный диспетчер операций над задачами."""
act = action.strip().upper()
if act == "ADD":
if not title:
return {"status": "error", "message": "Для создания задачи требуется указать title"}
return db_add_task(
user_id=user_id,
module=module or "general",
title=title,
priority=priority or "MEDIUM",
due_date=due_date,
status=status or "BACKLOG"
)
elif act == "UPDATE":
if not task_id:
return {"status": "error", "message": "Для обновления требуется указать task_id"}
return db_update_task_details(user_id=user_id, task_id=str(task_id), title=title, priority=priority, status=status, due_date=due_date)
elif act == "DELETE":
if not task_id:
return {"status": "error", "message": "Для удаления требуется указать task_id"}
return db_delete_task(user_id=user_id, task_id=str(task_id))
elif act == "EXPORT":
return db_export_tasks_markdown(user_id=user_id, filename=filename or "ROADMAP.md")
return {"status": "error", "message": f"Неизвестное действие action='{action}'"}
+41 -8
View File
@@ -11,6 +11,7 @@ AI-CONTEXT-ANCHORS:
"""
# ANCHOR[FACADE_EXPORTS]
from typing import Optional, List, Dict, Any
from core.connection import DB_PATH, get_connection as get_db_connection
# Домен: Задачи
@@ -28,16 +29,10 @@ from services.tasks.repository import normalize_task_id
from services.prompts.service import (
get_active_system_prompt as db_get_active_system_prompt,
apply_prompt_action as db_apply_prompt_node_action,
save_full_prompt_draft as db_add_system_prompt,
save_full_prompt_draft,
create_prompt_preview
)
# Домен: Снапшоты СКУД
from services.snapshots.service import (
get_snapshots_registry as db_get_snapshots,
delete_snapshots_safely as db_delete_snapshots
)
# Домен: База знаний
from services.knowledge.service import (
get_rules as db_get_rules,
@@ -60,4 +55,42 @@ from .db.db_prompts import (
db_get_anomalies,
db_get_reference
)
from .core.calendar_utils import get_dynamic_calendar_context as db_get_current_server_time
from .core.calendar_utils import get_dynamic_calendar_context as db_get_current_server_time
def db_add_system_prompt(name_or_text: str, draft_text: str = None) -> None:
"""Совместимая обертка для сохранения системного промпта."""
if draft_text is not None:
save_full_prompt_draft(draft_text, prompt_name=name_or_text)
else:
save_full_prompt_draft(name_or_text, prompt_name="main_agent")
def db_get_snapshots(session_id: str = "web_session_main", date_str: str = None, original_user_message: str = "") -> Dict[str, Any]:
"""Совместимый фасад выборки снапшотов с сохранением стейта сессии."""
from services.snapshots.service import get_snapshots_registry
from .core.calendar_utils import parse_relative_date_ru
clean_date = (date_str or "").strip()
if not clean_date and original_user_message:
clean_date = parse_relative_date_ru(original_user_message)
res = get_snapshots_registry(date_str=clean_date if clean_date else None)
db_set_session_state(session_id=session_id, state_type="SNAPSHOTS_VIEW", data=res)
return res
def db_delete_snapshots(snapshot_id: str = None, snapshot_ids: List[str] = None, day_str: str = None) -> Dict[str, Any]:
"""Совместимый фасад безопасного удаления снапшотов."""
from services.snapshots.service import delete_snapshots_safely
target_ids = []
if snapshot_ids:
target_ids.extend(snapshot_ids)
if snapshot_id:
if isinstance(snapshot_id, str) and "," in snapshot_id:
target_ids.extend([s.strip() for s in snapshot_id.split(",")])
else:
target_ids.append(snapshot_id)
return delete_snapshots_safely(snapshot_ids=target_ids)
+25 -13
View File
@@ -19,32 +19,37 @@ TOOLS_SCHEMA = [
"function": {
"name": "db_prompt_node_edit",
"description": (
"Управление элементами системного промпта (добавление, изменение, удаление). "
"Если пользователь пишет 'удали 2.9' или 'удали пункт 2.9', вызывай action='DELETE', section_id=2, item_id=9. "
"Действие action может быть ADD, EDIT или DELETE."
"Управление элементами системного промпта (добавление, изменение, удаление пунктов).\n"
"Поддерживает как одиночные пункты (section_id, item_id), так и список пунктов для удаления (nodes_list=['1.8', '3.4']).\n"
"При любом запросе на удаление или добавление пунктов системного промпта ТЫ ОБЯЗАН вызвать этот инструмент."
),
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["ADD", "EDIT", "DELETE"],
"description": "Действие: ADD (добавить), EDIT (изменить), DELETE (удалить)"
"enum": ["ADD", "EDIT", "DELETE", "BATCH_DELETE"],
"description": "Тип действия"
},
"section_id": {
"type": "integer",
"description": "Номер раздела из точки (например, 2 из 2.9)"
"description": "Номер раздела (например 1)"
},
"item_id": {
"type": "integer",
"description": "Номер пункта из точки (например, 9 из 2.9)"
"description": "Номер пункта (например 8)"
},
"nodes_list": {
"type": "array",
"items": {"type": "string"},
"description": "Список пунктов для удаления/изменения, например ['1.8', '3.4']"
},
"content": {
"type": "string",
"description": "Текст пункта (для DELETE пустая строка)"
"description": "Текст пункта"
}
},
"required": ["action", "section_id", "item_id"]
"required": ["action"]
}
}
},
@@ -81,7 +86,14 @@ TOOLS_SCHEMA = [
"type": "function",
"function": {
"name": "db_tasks_edit",
"description": "Единый инструмент управления задачами: создание (ADD), изменение статуса/дедлайна (UPDATE), удаление (DELETE) и экспорт задач в Markdown-файл (EXPORT).",
"description": (
"Единый инструмент управления задачами: создание (ADD), изменение статуса/срока/названия (UPDATE), удаление (DELETE) и экспорт (EXPORT).\n"
"СТРОГИЕ ПРАВИЛА ВЫЗОВА:\n"
"- На любые фразы вида 'удали задачу N', 'удалить N', 'убери задачу N' ТЫ ОБЯЗАН СРАЗУ вызвать инструмент с action='DELETE', task_id='N'.\n"
"- На фразы 'возьми в работу N' вызывай action='UPDATE', task_id='N', status='IN_PROGRESS'.\n"
"- На фразы 'заверши N', 'готово N' вызывай action='UPDATE', task_id='N', status='COMPLETED'.\n"
"- КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО писать текстовые вопросы или запрашивать подтверждения словами! ТЫ ОБЯЗАН СРАЗУ вызвать инструмент."
),
"parameters": {
"type": "object",
"properties": {
@@ -92,7 +104,7 @@ TOOLS_SCHEMA = [
},
"task_id": {
"type": "string",
"description": "Номер задачи (для UPDATE и DELETE, например: '35')"
"description": "Номер задачи (например: '37')"
},
"title": {
"type": "string",
@@ -100,8 +112,8 @@ TOOLS_SCHEMA = [
},
"status": {
"type": "string",
"enum": ["IN_PROGRESS", "COMPLETED", "PLANNED"],
"description": "Статус задачи"
"enum": ["IN_PROGRESS", "COMPLETED", "BACKLOG"],
"description": "Новый статус задачи: IN_PROGRESS (В работу), COMPLETED (Завершено), BACKLOG (В планы)"
},
"priority": {
"type": "string",
+6
View File
@@ -16,6 +16,12 @@ CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
if CURRENT_DIR not in sys.path:
sys.path.insert(0, CURRENT_DIR)
ROOT_DIR = os.path.abspath(os.path.join(CURRENT_DIR, "../../"))
for p in [ROOT_DIR, CURRENT_DIR]:
if p not in sys.path:
sys.path.insert(0, p)
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, JSONResponse
+9 -14
View File
@@ -5,8 +5,6 @@ ROLE: Аутентификация, валидация JWT-токенов и у
===============================================================================
"""
# ANCHOR[AUTH_ROUTER_IMPORTS]
import sqlite3
import logging
from datetime import datetime, timedelta
from typing import Dict, Any, Optional
@@ -17,7 +15,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from llm.db_tools import DB_PATH
from core.connection import get_connection
JWT_SECRET = "scud_jwt_secret_key_2026_orion_ai_super_secure"
ALGORITHM = "HS256"
@@ -27,11 +25,10 @@ security = HTTPBearer()
router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
# ANCHOR[AUTH_DB_HELPERS]
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
return get_connection(row_factory=True)
def create_access_token(user_id: int, username: str, is_admin: bool) -> str:
payload = {
@@ -42,6 +39,7 @@ def create_access_token(user_id: int, username: str, is_admin: bool) -> str:
}
return jwt.encode(payload, JWT_SECRET, algorithm=ALGORITHM)
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> Dict[str, Any]:
try:
token = credentials.credentials
@@ -58,21 +56,20 @@ def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(securit
headers={"WWW-Authenticate": "Bearer"},
)
# ANCHOR[AUTH_SCHEMAS]
class AuthRequest(BaseModel):
username: str
password: str
class ChangePasswordRequest(BaseModel):
old_password: str
new_password: str
# ANCHOR[AUTH_ENDPOINTS]
@router.post("/login")
def login(req: AuthRequest):
username = req.username.strip().lower()
logging.info(f"===> Попытка входа для пользователя: {username}")
conn = get_db()
cursor = conn.cursor()
cursor.execute("SELECT id, username, password_hash, is_admin FROM users WHERE username = ?", (username,))
@@ -80,15 +77,14 @@ def login(req: AuthRequest):
conn.close()
if not user or not pwd_context.verify(req.password, user["password_hash"]):
logging.warning(f"===> Ошибка: Неверный логин или пароль для {username}")
raise HTTPException(status_code=401, detail="Неверное имя пользователя или пароль")
is_admin = bool(user["is_admin"]) or (user["username"] == "puh")
token = create_access_token(user["id"], user["username"], is_admin)
logging.info(f"===> УСПЕХ: Авторизован пользователь {username}")
return {"status": "success", "token": token, "username": user["username"], "is_admin": is_admin}
@router.post("/change-password")
def change_password(req: ChangePasswordRequest, current_user: Dict[str, Any] = Depends(get_current_user)):
if not req.new_password or len(req.new_password) < 4:
@@ -108,5 +104,4 @@ def change_password(req: ChangePasswordRequest, current_user: Dict[str, Any] = D
conn.commit()
conn.close()
logging.info(f"Пароль успешно изменен для пользователя ID: {current_user['id']}")
return {"status": "success", "message": "Пароль успешно изменен"}
+1 -1
View File
@@ -13,7 +13,7 @@ from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException
from pydantic import BaseModel
from .auth import get_current_user
from llm.agent import process_chat_message
from modules.ai_engine.agent import process_chat_message
from llm.file_parser import extract_text_from_file
from llm.db_tools import db_set_session_state, db_get_session_state
+8 -7
View File
@@ -3,19 +3,20 @@
FILE: modules/web_api/routers/tasks.py
PROJECT: SCUD Orion AI (Unified Architecture)
MODULE: web_api / routers
ROLE: REST API эндпоинты реестра задач (получение, создание и обновление).
ROLE: REST API эндпоинты реестра задач.
===============================================================================
"""
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import Optional, Dict, Any
from routers.auth import get_current_user
from llm.db_tools import db_get_tasks, db_add_task, db_update_task_details
from services.tasks.service import get_tasks, add_task, update_task_details
router = APIRouter(prefix="/api/v1/tasks", tags=["Tasks"])
class TaskCreateRequest(BaseModel):
title: str
priority: Optional[str] = "MEDIUM"
@@ -23,6 +24,7 @@ class TaskCreateRequest(BaseModel):
due_date: Optional[str] = None
status: Optional[str] = "BACKLOG"
class TaskUpdateRequest(BaseModel):
title: Optional[str] = None
priority: Optional[str] = None
@@ -31,7 +33,6 @@ class TaskUpdateRequest(BaseModel):
def resolve_user_id(current_user: Dict[str, Any]) -> int:
"""Извлекает корректный ID пользователя из JWT payload или ставит дефолтный 1."""
if not current_user:
return 1
return current_user.get("id") or current_user.get("user_id") or 1
@@ -40,13 +41,13 @@ def resolve_user_id(current_user: Dict[str, Any]) -> int:
@router.get("")
async def get_tasks_endpoint(status: Optional[str] = None, current_user = Depends(get_current_user)):
user_id = resolve_user_id(current_user)
return {"tasks": db_get_tasks(user_id=user_id, status=status)}
return {"tasks": get_tasks(user_id=user_id, status=status)}
@router.post("")
async def create_task_endpoint(req: TaskCreateRequest, current_user = Depends(get_current_user)):
user_id = resolve_user_id(current_user)
res = db_add_task(
res = add_task(
user_id=user_id,
module=req.module,
title=req.title,
@@ -62,7 +63,7 @@ async def create_task_endpoint(req: TaskCreateRequest, current_user = Depends(ge
@router.patch("/{task_id}")
async def update_task_endpoint(task_id: str, req: TaskUpdateRequest, current_user = Depends(get_current_user)):
user_id = resolve_user_id(current_user)
res = db_update_task_details(
res = update_task_details(
user_id=user_id,
task_id=task_id,
title=req.title,