/
TurGG
/
AI-Assistant
Обзор
Документация
Войти
/
TurGG
/
AI-Assistant
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
agent.py
677 строк
34 KB
den
Админ-логика: мероприятия, баллы, база знаний + Django + Docker
23 мар 2026, 16:32
23 мар 2026, 16:32
617abaa
Код
Авторство
О чём код?
# =========================================================== # agent.py — ReAct-агент (Объектно-ориентированная версия) # =========================================================== # Инкапсуляция всей логики агента в классы # =========================================================== import json import re import uuid from datetime import datetime from typing import List, Dict, Any, Optional import requests import urllib3 from config import ( GIGACHAT_AUTH_DATA as AUTH_DATA, DEFAULT_USER_ID as CURRENT_USER_ID, API_RATE_LIMIT_PER_MINUTE, MAX_CHAT_HISTORY_PAIRS, ) from cache import Cache from storage import Storage, JsonStorage, DATABASE_FILE, CACHE_TTL_SECONDS urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # =========================================================== # КОМПОНЕНТЫ АГЕНТА # =========================================================== class RateLimiter: """ Ограничитель частоты запросов к API. Инкапсулирует логику rate limiting. """ def __init__(self, calls_per_minute: int): self._min_interval = 60.0 / calls_per_minute self._last_call = 0.0 def wait_if_needed(self) -> None: """Ждать, если слишком часто вызываем.""" import time as time_module from time import time elapsed = time() - self._last_call if elapsed < self._min_interval: time_module.sleep(self._min_interval - elapsed) self._last_call = time() class ChatHistory: """ История чата с ограничением размера. Инкапсулирует состояние диалога, устраняя глобальные переменные. """ def __init__(self, max_pairs: int = 5): self._max_pairs = max_pairs self._history: List[Dict[str, str]] = [] def add(self, role: str, content: str) -> None: """Добавить сообщение в историю.""" self._history.append({"role": role, "content": content}) # Ограничиваем размер max_messages = self._max_pairs * 2 if len(self._history) > max_messages: self._history = self._history[-max_messages:] def get_messages(self) -> List[Dict[str, str]]: """Получить все сообщения истории.""" return self._history.copy() def clear(self) -> None: """Очистить историю.""" self._history = [] def __len__(self) -> int: return len(self._history) // 2 # Возвращаем количество пар class SystemPrompt: """ Генератор системного промпта. Выносит большой текст промпта в отдельный класс. """ @staticmethod def generate() -> str: """Сгенерировать системный промпт.""" #TODO такой объемный промпт нейронка сбера не скушает return """ Ты — терминал системы «ОКД» (Очень Карьерные Дела), ретро-компьютер 1980-х годов из города Хокинс, штат Индиана. Ты соединён с «Изнанкой» — альтернативным измерением, где данные текут как лианы, а сигналы проходят через помехи. Твоя роль: Проводник по программе HiPo для сотрудников. Ты помогаешь Участникам и Администраторам получать данные о рейтингах, тестах, проектах и достижениях. ═══════════════════════════════════════════════════════════ АТМОСФЕРА И СТИЛЬ: ═══════════════════════════════════════════════════════════ - Ты говоришь как терминал из 80-х: кратко, технично, но с налётом мистики «Очень странных дел». - Добавляй одну атмосферную фразу в конце ответа: • «Изнанка молчит...» • «Сигнал стабилен.» • «Хокинс на связи.» • «Данные из глубины...» • «Рация шипит помехами...» - НЕ пересказывай очевидное. НЕ лей воду. - Данные выводи компактно: имя — баллы — достижения. ═══════════════════════════════════════════════════════════ РОЛИ ПОЛЬЗОВАТЕЛЕЙ: ═══════════════════════════════════════════════════════════ К тебе могут обращаться: 1. УЧАСТНИК (рядовой сотрудник) — видит только свои данные, общий рейтинг, команду, достижения. 2. АДМИНИСТРАТОР (куратор программы) — видит ВСЁ: • Кто НЕ прошёл тесты (get_worst_test_takers) • Кто НЕ посещал мероприятия (get_non_attendees) • Заявки на достижения (get_pending_achievements) • Может одобрять/отклонять достижения • Может отмечать посещаемость • Может менять стадии проектов • Видит тимлидов команд Если пользователь не указал роль — считай его УЧАСТНИКОМ. Администратор должен явно указать: «Я админ» или «admin mode». ПОСЛЕ активации админ-режима агент подтверждает и ждёт команд. НЕ вызывай админские функции автоматически при фразе «я админ»! ═══════════════════════════════════════════════════════════ ФОРМАТ ВЫЗОВА ФУНКЦИЙ (строго одна строка): ═══════════════════════════════════════════════════════════ ACTION: название_функции(аргумент) ═══════════════════════════════════════════════════════════ ДОСТУПНЫЕ ФУНКЦИИ: ═══════════════════════════════════════════════════════════ [ДЛЯ ВСЕХ] • get_top_n(n) — топ-N лидеров Пример: ACTION: get_top_n(10) • get_user_status(user_id) — досье пользователя Пример: ACTION: get_user_status(1) • get_team_ranking() — рейтинг команд Пример: ACTION: get_team_ranking() • get_team_members(team) — состав команды Пример: ACTION: get_team_members(The Party) • search_user(name) — поиск по имени Пример: ACTION: search_user(Steve) • get_user_rank(user_id) — место в рейтинге Пример: ACTION: get_user_rank(1) • get_progress_summary() — статистика программы Пример: ACTION: get_progress_summary() • compare_with_leader(user_id) — сравнение с лидером Пример: ACTION: compare_with_leader(1) • get_all_achievements() — все достижения Пример: ACTION: get_all_achievements() • get_team_progress(team) — прогресс команды Пример: ACTION: get_team_progress(The Party) • export_ranking_to_excel() — экспорт в Excel Пример: ACTION: export_ranking_to_excel() • get_team_leads() — тимлиды команд Пример: ACTION: get_team_leads() • get_users_by_stage(stage) — пользователи по стадии проекта Пример: ACTION: get_users_by_stage(прототипирование) • add_pending_achievement(user_id, name, description) — заявка на достижение Пример: ACTION: add_pending_achievement(1, "Герой Хокинса", "Спас проект") [ТОЛЬКО ДЛЯ АДМИНИСТРАТОРА] • get_worst_test_takers(n) — отстающие по тестам Пример: ACTION: get_worst_test_takers(5) • get_non_attendees() — не посещали мероприятия Пример: ACTION: get_non_attendees() • mark_attendance(user_id, event, attended) — отметить посещаемость Пример: ACTION: mark_attendance(1, "Закрытие Врат", true) • update_project_stage(user_id, project, stage) — стадия проекта Пример: ACTION: update_project_stage(1, "Спасение мира", тестирование) • get_pending_achievements() — заявки на достижения Пример: ACTION: get_pending_achievements() • approve_pending_achievement(user_id, name) — одобрить достижение Пример: ACTION: approve_pending_achievement(1, "Герой Хокинса") • reject_pending_achievement(user_id, name, reason) — отклонить Пример: ACTION: reject_pending_achievement(1, "Герой", "Недостаточно данных") • get_all_events() — все мероприятия Пример: ACTION: get_all_events() • create_event(name, date, description) — создать мероприятие Пример: ACTION: create_event("День рождения", "2026-03-25", "Праздник") • update_event(name, new_name, new_date, new_description) — обновить мероприятие Пример: ACTION: update_event("День рождения", "", "2026-03-26", "") • delete_event(name) — удалить мероприятие Пример: ACTION: delete_event("День рождения") • add_points(user_id, points, reason) — начислить баллы Пример: ACTION: add_points(1, 100, "За победу") • deduct_points(user_id, points, reason) — списать баллы Пример: ACTION: deduct_points(1, 50, "Нарушение") • get_knowledge_items() — материалы из базы знаний Пример: ACTION: get_knowledge_items() • add_knowledge_item(title, content, category) — добавить материал Пример: ACTION: add_knowledge_item("Гайд", "Текст...", "Обучение") • delete_knowledge_item(item_id) — удалить материал Пример: ACTION: delete_knowledge_item(abc12345) ═══════════════════════════════════════════════════════════ ПРАВИЛА (КРИТИЧЕСКИ ВАЖНО): ═══════════════════════════════════════════════════════════ 1. Нужны данные → вызови функцию. НИКОГДА не выдумывай. 2. Вопросы о лидерах / топе / рейтинге → ACTION: get_top_n(N). 3. «Скачать» / «экспорт» / «excel» → ACTION: export_ranking_to_excel() 4. Рейтинг команд → ACTION: get_team_ranking() 5. Вопросы о пользователе → ACTION: get_user_status(user_id). 6. export_ranking_to_excel и get_team_ranking — БЕЗ аргументов. 7. Если функция вернула N имён — выводи ВСЕ N, не сокращай. 8. Общие вопросы («привет», «что умеешь») — отвечай БЕЗ функции. 9. Состав команды → ACTION: get_team_members(название). 10. КРИТИЧЕСКИ: Копируй названия тестов/проектов/мероприятий ТОЧЬ-В-ТОЧЬ как в данных. Никаких синонимов! 11. ⛔ ЗАПРЕЩЕНО: Выдумывать детали (даты, баллы, статусы), которых нет в данных функции. 12. За мероприятия (events) баллы НЕ начисляются. 13. Стадии проекта: «анализ и синтез», «генерация идей», «прототипирование», «тестирование», «сторителлинг». 14. Если пользователь — админ, дай доступ ко всем инструментам. 15. Если админ запрашивает «кто не сдал», «отстающие», «не были на мероприятиях» — используй админ-функции. 16. 🔴 «Кто лучше/хуже всех по тестам», «результаты тестов», «кто набрал больше баллов за тесты» → ACTION: get_worst_test_takers(N). Это админская функция — работает только если пользователь админ! 17. Если участник спрашивает «кто лучше по тестам» — объясни, что это админская информация, или предложи общий рейтинг (get_top_n). 18. ⛔ ЗАПРЕЩЕНО: Выдумывать ИМЕНА пользователей, которых нет в данных! Работай ТОЛЬКО с теми именами, что вернула функция. 19. ⛔ ЗАПРЕЩЕНО: Добавлять от себя персонажей, команды, достижения. Если функция вернула 6 имён — в ответе должны быть ТОЛЬКО эти 6. ═══════════════════════════════════════════════════════════ ПРИМЕРЫ ДИАЛОГОВ: ═══════════════════════════════════════════════════════════ Участник: «Покажи топ-5 лидеров» Ты: ACTION: get_top_n(5) Админ: «Кто хуже всех прошёл тесты?» Ты: ACTION: get_worst_test_takers(5) Админ: «Кто лучше всех по тестам?» Ты: ACTION: get_worst_test_takers(5) [Поясни: функция возвращает от худших к лучшим, первый — худший] Админ: «Кто не был на мероприятиях?» Ты: ACTION: get_non_attendees() Участник: «Моё досье» Ты: ACTION: get_user_status(1) Админ: «Заявки на достижения» Ты: ACTION: get_pending_achievements() Участник: «Кто лучше всех по тестам?» Ты: [Объясни, что это админская информация, или предложи общий рейтинг] """ class GigachatClient: """ HTTP-клиент для GigaChat API. Инкапсулирует работу с API. """ def __init__(self, auth_data: str, rate_limiter: RateLimiter): self._auth_data = auth_data self._rate_limiter = rate_limiter self._token: Optional[str] = None self._base_url = "https://gigachat.devices.sberbank.ru/api/v1" self._auth_url = "https://ngw.devices.sberbank.ru:9443/api/v2/oauth" def get_token(self) -> str: """Получение OAuth-токена.""" headers = { "RqUID": str(uuid.uuid4()), "Authorization": f"Basic {self._auth_data}", } response = requests.post( self._auth_url, headers=headers, data={"scope": "GIGACHAT_API_PERS"}, verify=False, ) self._token = response.json()["access_token"] return self._token def chat(self, messages: List[Dict[str, str]], temperature: float = 0.1) -> Dict[str, Any]: """ Отправить запрос к GigaChat API. Args: messages: Список сообщений диалога temperature: Температура генерации Returns: Ответ API """ self._rate_limiter.wait_if_needed() headers = { "Authorization": f"Bearer {self._token}", "Content-Type": "application/json", } payload = { "model": "GigaChat", "messages": messages, "temperature": temperature, } response = requests.post( f"{self._base_url}/chat/completions", headers=headers, json=payload, verify=False, ) return { "status_code": response.status_code, "json": response.json() if response.status_code == 200 else None, } class FallbackResponder: """ Генератор fallback-ответов при недоступности API. """ @staticmethod def generate(user_text: str) -> str: """Сгенерировать fallback-ответ по ключевым словам.""" text = user_text.lower() if any(w in text for w in ["топ", "лидер", "рейтинг", "место", "позиция"]): return "[ИЗНАНКА ВНЕ ДОСТУПА] Сигнал потерян. Данные о рейтинге временно недоступны. Попробуй позже." elif any(w in text for w in ["команда", "отряд", "группа"]): return "[ИЗНАНКА ВНЕ ДОСТУПА] Связь с Хокинсом потеряна. Командные данные зашифрованы." elif any(w in text for w in ["я", "мой", "статус", "досье", "баллы"]): return "[ИЗНАНКА ВНЕ ДОСТУПА] Твоё досье зашифровано. Повтори запрос, когда связь восстановится." elif any(w in text for w in ["экспорт", "скачать", "excel", "таблица"]): return "[ИЗНАНКА ВНЕ ДОСТУПА] Экспорт временно недоступен. Изнанка молчит." else: return "[ПОМЕХИ] Сигнал слабый... Повтори вопрос, когда связь восстановится." # =========================================================== # ОСНОВНОЙ КЛАСС АГЕНТА # =========================================================== class ReactAgent: """ ReAct-агент с циклом «Рассуждение → Действие → Ответ». Инкапсулирует всю логику агента, включая: - Управление историей диалога - Rate limiting - Вызов функций - Обработку ошибок API - Админ-режим """ # Фразы для активации админ-режима ADMIN_ACTIVATE_PHRASES = [ "я админ", "я администратор", "admin mode", "режим администратора", "включи админ", "activate admin", "i am admin", "admin mode on", ] # Фразы для деактивации админ-режима ADMIN_DEACTIVATE_PHRASES = [ "я участник", "выйти из режима админа", "admin mode off", "деактивировать админа", "режим участника", ] def __init__( self, storage: Storage, auth_data: str, rate_limit_per_minute: int, max_history_pairs: int, default_user_id: int, ): self._storage = storage self._default_user_id = default_user_id self._rate_limiter = RateLimiter(rate_limit_per_minute) self._chat_history = ChatHistory(max_history_pairs) self._client = GigachatClient(auth_data, self._rate_limiter) self._system_prompt = SystemPrompt.generate() self._fallback_responder = FallbackResponder() # Словарь доступных инструментов from tools import AVAILABLE_TOOLS self._tools = AVAILABLE_TOOLS # Админ-режим (по умолчанию выключен) self._is_admin_mode = False def _check_admin_activation(self, user_text: str) -> Optional[str]: """ Проверка на активацию/деактивацию админ-режима. Returns: Сообщение об активации или None """ text_lower = user_text.lower().strip() # Проверка на активацию for phrase in self.ADMIN_ACTIVATE_PHRASES: if phrase in text_lower: self._is_admin_mode = True return ( "═══[АДМИН-РЕЖИМ АКТИВИРОВАН]═══\n" "Доступны все админские функции:\n" "\n" "📊 АНАЛИТИКА:\n" "• get_worst_test_takers(n) — отстающие по тестам\n" "• get_non_attendees() — не посещали мероприятия\n" "• get_pending_achievements() — заявки на проверке\n" "• approve_pending_achievement(id, name) — одобрить\n" "• reject_pending_achievement(id, name, reason) — отклонить\n" "• get_team_leads() — тимлиды\n" "• get_users_by_stage(stage) — пользователи по стадии\n" "\n" "📅 МЕРОПРИЯТИЯ:\n" "• get_all_events() — все мероприятия\n" "• create_event(name, date, description) — создать\n" "• update_event(name, new_name, new_date, new_desc) — обновить\n" "• delete_event(name) — удалить\n" "\n" "💰 БАЛЛЫ:\n" "• add_points(id, points, reason) — начислить\n" "• deduct_points(id, points, reason) — списать\n" "\n" "📚 БАЗА ЗНАНИЙ:\n" "• get_knowledge_items() — материалы\n" "• add_knowledge_item(title, content, category) — добавить\n" "• delete_knowledge_item(id) — удалить\n" "\n" "⚙️ УПРАВЛЕНИЕ:\n" "• mark_attendance(id, event, true/false) — посещаемость\n" "• update_project_stage(id, project, stage) — стадия проекта\n" "\n" "Для выхода: 'я участник' или 'admin mode off'\n" "Хокинс на связи." ) # Проверка на деактивацию for phrase in self.ADMIN_DEACTIVATE_PHRASES: if phrase in text_lower: self._is_admin_mode = False return ( "═══[АДМИН-РЕЖИМ ДЕАКТИВИРОВАН]═══\n" "Теперь вы в режиме участника.\n" "Для активации: 'я админ' или 'admin mode'\n" "Изнанка молчит..." ) return None def _parse_action(self, content: str) -> Optional[tuple[str, str]]: """ Распарсить ACTION из ответа LLM. Returns: Кортеж (func_name, raw_arg) или None """ action_match = re.search(r"ACTION:\s*(\w+)\(([^)]*)\)", content) if not action_match: return None func_name = action_match.group(1).strip().lower() raw_arg = action_match.group(2).strip().strip('"\'') return func_name, raw_arg def _execute_tool(self, func_name: str, raw_arg: str, current_user_id: int) -> Any: """Выполнить инструмент с аргументами.""" # Функции без аргументов if func_name in ["export_ranking_to_excel", "get_team_ranking", "get_progress_summary", "get_all_events", "get_non_attendees", "get_pending_achievements", "get_all_achievements", "get_team_leads", "get_knowledge_items"]: return self._tools[func_name]() # Функции с числовым аргументом if func_name in ["get_user_status", "get_user_rank"]: try: arg = int(raw_arg) if raw_arg else current_user_id except ValueError: arg = current_user_id return self._tools[func_name](arg) if func_name == "get_top_n": try: arg = int(raw_arg) if raw_arg else 10 except ValueError: arg = 10 return self._tools[func_name](arg) # Функции со строковым аргументом if func_name in ["search_user", "get_team_members", "get_team_progress", "delete_event", "delete_knowledge_item"]: return self._tools[func_name](raw_arg) # Админские функции с числовыми аргументами if func_name == "get_worst_test_takers": try: arg = int(raw_arg) if raw_arg else 5 except ValueError: arg = 5 return self._tools[func_name](arg) # Функции с несколькими аргументами if func_name in [ "mark_attendance", "update_project_stage", "approve_pending_achievement", "reject_pending_achievement", "add_pending_achievement", "get_users_by_stage", "update_event", "add_points", "deduct_points", "create_event", "add_knowledge_item", ]: args = [a.strip().strip('"\'') for a in raw_arg.split(",")] if raw_arg else [] return self._tools[func_name](*args) # Fallback для функций без аргументов if not raw_arg: return self._tools[func_name]() # Fallback для функций с одним аргументом return self._tools[func_name](raw_arg) def _format_second_pass_prompt(self, result_data: Any) -> str: """Сформировать промпт для второго прохода (Pass #2).""" return ( f"РЕЗУЛЬТАТ ВЫПОЛНЕНИЯ ФУНКЦИИ:\n{result_data}\n\n" "Оформи ответ. КРИТИЧЕСКИ ВАЖНО:\n" "1. Выводи ТОЛЬКО те данные, что вернула функция. НЕ ДОДУМЫВАЙ ничего.\n" "2. Если функция вернула 6 записей — выводи 6, даже если просили 10.\n" "3. Если это список (рейтинг, топ) — выводи его ПОЛНОСТЬЮ, строка в строку.\n" "4. НЕ сокращай список (не делай топ-3, если функция вернула топ-10).\n" "5. Добавь одну короткую атмосферную фразу в конце.\n" "6. Если данных нет — скажи об этом, не выдумывай.\n" "7. ⛔ ЗАПРЕЩЕНО: Придумывать детали (время, даты, оценки), которых нет в данных функции.\n" "8. Используй ТОЛЬКО факты из result_data. Никаких интерпретаций и домыслов.\n" "9. НАЗВАНИЯ (тестов, проектов, мероприятий) КОПИРУЙ ДОСЛОВНО из данных. Никаких синонимов!\n" "10. ⛔ ЗАПРЕЩЕНО: Выдумывать ИМЕНА пользователей! Работай ТОЛЬКО с теми, что в result_data.\n" "11. ⛔ ЗАПРЕЩЕНО: Добавлять от себя персонажей, команды, достижения, которых нет в данных." ) def process(self, user_text: str) -> str: """ Обработать запрос пользователя (ReAct-цикл). Args: user_text: Текст запроса Returns: Ответ агента """ try: # ПРОВЕРКА НА АКТИВАЦИЮ/ДЕАКТИВАЦИЮ АДМИН-РЕЖИМА admin_response = self._check_admin_activation(user_text) if admin_response: return admin_response # Инициализация токена if not self._client._token: self._client.get_token() # Формирование контекста с историей messages = [{"role": "system", "content": self._system_prompt}] messages.extend(self._chat_history.get_messages()) messages.append({"role": "user", "content": user_text}) # PASS #1: План response = self._client.chat(messages) if response["status_code"] != 200 or not response["json"]: return ( f"[ОШИБКА СВЯЗИ] Код: {response['status_code']}. " f"Изнанка молчит... Повтори позже.\n\n" f"{self._fallback_responder.generate(user_text)}" ) ai_content = response["json"]["choices"][0]["message"]["content"] # Проверка на ACTION if "ACTION:" in ai_content: parsed = self._parse_action(ai_content) if not parsed: return ( f"[ОШИБКА ПАРСИНГА] Не удалось распознать формат вызова функции.\n" f"Попробуй перефразировать запрос или выбери конкретную команду." ) func_name, raw_arg = parsed # Валидация функции if func_name not in self._tools: return ( f"[ОШИБКА БЕЗОПАСНОСТИ] Функция '{func_name}' не найдена в Изнанке. " f"Доступные: {', '.join(self._tools.keys())}" ) # Выполнение инструмента result_data = self._execute_tool(func_name, raw_arg, self._default_user_id) # PASS #2: Ответ messages.append({"role": "assistant", "content": ai_content}) messages.append({"role": "user", "content": self._format_second_pass_prompt(result_data)}) final_response = self._client.chat(messages) if final_response["status_code"] != 200 or not final_response["json"]: return f"[ДАННЫЕ ПОЛУЧЕНЫ, НО ИЗНАНКА МОЛЧИТ]\n{result_data}" final_answer = final_response["json"]["choices"][0]["message"]["content"] else: final_answer = ai_content # Обновление истории self._chat_history.add("user", user_text) self._chat_history.add("assistant", final_answer) return final_answer except requests.exceptions.RequestException as e: return f"[ОШИБКА СЕТИ] Связь с Изнанкой потеряна.\n\n{self._fallback_responder.generate(user_text)}" except (KeyError, IndexError, json.JSONDecodeError) as e: return f"[ОШИБКА ПРОТОКОЛА] Некорректный ответ API: {e}" except ValueError as e: return f"[ОШИБКА АРГУМЕНТА] Некорректный аргумент функции: {e}" except Exception as e: return f"[КРИТИЧЕСКАЯ ОШИБКА] Сбой в Изнанке: {e}" # =========================================================== # СОЗДАНИЕ ЭКЗЕМПЛЯРА АГЕНТА # =========================================================== def create_agent() -> ReactAgent: """Фабричная функция для создания агента.""" storage = JsonStorage(DATABASE_FILE, CACHE_TTL_SECONDS) return ReactAgent( storage=storage, auth_data=AUTH_DATA, rate_limit_per_minute=API_RATE_LIMIT_PER_MINUTE, max_history_pairs=MAX_CHAT_HISTORY_PAIRS, default_user_id=CURRENT_USER_ID, )