from discord.ext import tasks import discord import traceback import asyncio import aiohttp import os import numpy as np import math from collections import Counter import re from typing import List, Dict, Any, Optional, Union class AnyFieldIsNotFilled(Exception): pass #ai slooop for ai slooop EPSILON = 1e-6 PERMISSION_LEVEL_MAP = { 'DONT_HAVE': 0, 'NONE': 0, None: 0, 'FREE': 1, 'VIP': 2, 'MODERATOR': 3, 'ADMIN': 4, 'ROOT': 4 } MAX_PERMISSION_LEVEL = 4 class KamazAI_v1_3: def __init__(self, report_list): self.k_neighbors = 5 self.weights = { 'steam_identity': 0.35, 'reason': 0.15, 'permissions': 0.10, 'derived_stats': 0.30, 'server': 0.10 } self.historical_reports: List[Dict[str, Any]] = [] self.report_actions: Dict[int, List[str]] = {} self.derived_stats: Optional[Dict[str, Dict[str, float]]] = None self.derived_fields = [ 'a_kd', 'a_kpm', 'a_dpm', 'r_kd', 'r_kpm', 'r_dpm', 'kd_diff', 'a_session_min', 'r_session_min' ] self.initialize(report_list) # ------------------------------------------------------------------ # Семантическое сравнение причин # ------------------------------------------------------------------ @staticmethod def _reason_similarity(reason1: str, reason2: str) -> float: """Jaccard-сходство по множествам слов (регистронезависимо).""" # Извлекаем буквенно-цифровые токены tokens1 = set(re.findall(r'\w+', reason1.lower())) tokens2 = set(re.findall(r'\w+', reason2.lower())) if not tokens1 or not tokens2: return 1.0 if reason1 == reason2 else 0.0 intersection = tokens1 & tokens2 union = tokens1 | tokens2 return len(intersection) / len(union) # ------------------------------------------------------------------ # Вспомогательные методы # ------------------------------------------------------------------ @staticmethod def _parse_permission_level(value: Union[str, int, None]) -> int: if isinstance(value, str): return PERMISSION_LEVEL_MAP.get(value.upper(), 0) if value is None: return 0 try: return int(value) except (ValueError, TypeError): return 0 @staticmethod def _compute_derived_features(report: Dict[str, Any]) -> Dict[str, float]: a_min = max((report['a_seconds'] if report['a_seconds'] else 0) / 60.0, EPSILON) r_min = max((report['r_seconds'] if report['r_seconds'] else 0) / 60.0, EPSILON) a_kd = (report['a_kills'] if report['a_kills'] else 0) / max((report['a_deads'] if report['a_deads'] else 0), EPSILON) r_kd = (report['r_kills'] if report['r_kills'] else 0) / max((report['r_deads'] if report['r_deads'] else 0), EPSILON) kd_diff = r_kd - a_kd return { 'a_kd': a_kd, 'a_kpm': (report['a_kills'] if report['a_kills'] else 0) / a_min, 'a_dpm': (report['a_deads'] if report['a_deads'] else 0) / a_min, 'r_kd': r_kd, 'r_kpm': (report['r_kills'] if report['r_kills'] else 0) / r_min, 'r_dpm': (report['r_deads'] if report['r_deads'] else 0) / r_min, 'kd_diff': kd_diff, 'a_session_min': a_min, 'r_session_min': r_min } def _compute_derived_stats(self) -> Dict[str, Dict[str, float]]: stats = {f: {'min': float('inf'), 'max': float('-inf')} for f in self.derived_fields} for r in self.historical_reports: feats = self._compute_derived_features(r) for f in self.derived_fields: val = feats[f] if val < stats[f]['min']: stats[f]['min'] = val if val > stats[f]['max']: stats[f]['max'] = val return stats def _normalize_derived(self, feats: Dict[str, float]) -> Dict[str, float]: norm = {} for f in self.derived_fields: min_val = self.derived_stats[f]['min'] max_val = self.derived_stats[f]['max'] if max_val - min_val < EPSILON: norm[f] = 0.0 else: norm[f] = (feats[f] - min_val) / (max_val - min_val) return norm def _similarity(self, r1: Dict[str, Any], r2: Dict[str, Any]) -> float: score = 0.0 # 1. Совпадение связки игроков #if r1['a_steam2'] == r2['a_steam2'] and r1['r_steam2'] == r2['r_steam2']: # identity_score = 1.0 #elif r1['a_steam2'] == r2['a_steam2'] or r1['r_steam2'] == r2['r_steam2']: # identity_score = 0.5 #else: # identity_score = 0.0 #score += self.weights['steam_identity'] * identity_score # 2. Причина – теперь семантически, через Jaccard reason_score = self._reason_similarity(r1['reasons'], r2['reasons']) score += self.weights['reason'] * reason_score # 3. Права a_lvl1 = self._parse_permission_level(r1['a_permition']) a_lvl2 = self._parse_permission_level(r2['a_permition']) r_lvl1 = self._parse_permission_level(r1['r_permition']) r_lvl2 = self._parse_permission_level(r2['r_permition']) dist_a = abs(a_lvl1 - a_lvl2) / MAX_PERMISSION_LEVEL dist_r = abs(r_lvl1 - r_lvl2) / MAX_PERMISSION_LEVEL perm_sim = 1.0 - (dist_a + dist_r) / 2.0 score += self.weights['permissions'] * perm_sim # 4. Производные признаки f1 = self._normalize_derived(self._compute_derived_features(r1)) f2 = self._normalize_derived(self._compute_derived_features(r2)) dist_sq = sum((f1[f] - f2[f]) ** 2 for f in self.derived_fields) eucl_dist = math.sqrt(dist_sq / len(self.derived_fields)) derived_sim = 1.0 - min(eucl_dist, 1.0) score += self.weights['derived_stats'] * derived_sim # 5. Сервер #srv_score = 1.0 if r1['srv'] == r2['srv'] else 0.0 #score += self.weights['server'] * srv_score return score # ------------------------------------------------------------------ # Публичные асинхронные методы # ------------------------------------------------------------------ def initialize(self, reports: List[Dict[str, Any]]) -> None: self.historical_reports = [] self.report_actions.clear() for report in reports: #if report.get("type", "IN_GAME") != "IN_GAME": # continue self.historical_reports.append(report) try: self.report_actions[report["id"]] = report["actions"] except: print("Cannot build action", report) self.derived_stats = self._compute_derived_stats() async def predict(self, new_report: Dict[str, Any]) -> Dict[str, Any]: if not self.historical_reports or self.derived_stats is None: raise RuntimeError("System not initialized. Call 'await initialize()' first.") similarities = [] for hist in self.historical_reports: sim = self._similarity(new_report, hist) similarities.append((hist['id'], sim)) similarities.sort(key=lambda x: x[1], reverse=True) top_k = similarities[:self.k_neighbors] action_counter = Counter() similar_ids = [] for rid, sim in top_k: if sim > 0: similar_ids.append(rid) if rid in self.report_actions: for act in self.report_actions[rid]: action_counter[act] += 1 total = sum(action_counter.values()) suggestions = [] if total > 0: for action, count in action_counter.most_common(): suggestions.append({ 'action': action, 'confidence': round(count / total, 4) }) else: suggestions.append({'action': 'none', 'confidence': 1.0}) return { 'suggestions': suggestions, 'similar_reports': similar_ids } async def reload_actions(self, new_actions: List[Dict[str, Any]]) -> None: for act in new_actions: rid = act['report_id'] self.report_actions.setdefault(rid, []).append(act['action']) #depricated class KamazAI_v1_2: """ Асинхронная система рекомендации действий модератора на основе исторических жалоб. Использование: system = ReportDecisionSystem() await system.initialize(reports, actions) result = await system.predict(new_report) """ def __init__(self, report_list): self.k_neighbors = 5 self.weights = { 'steam_identity': 0.35, 'reason': 0.15, 'permissions': 0.10, 'derived_stats': 0.30, 'server': 0.10 } self.historical_reports: List[Dict[str, Any]] = [] self.report_actions: Dict[int, List[str]] = {} self.derived_stats: Optional[Dict[str, Dict[str, float]]] = None self.derived_fields = [ 'a_kd', 'a_kpm', 'a_dpm', 'r_kd', 'r_kpm', 'r_dpm', 'kd_diff', 'a_session_min', 'r_session_min' ] self.initialize(report_list) # ---------- Вспомогательные методы ---------- @staticmethod def _compute_derived_features(report: Dict[str, Any]) -> Dict[str, float]: """Вычисляет производные признаки для одной записи.""" try: a_min = max(report['a_seconds'] / 60.0, EPSILON) except: a_min = 1 try: r_min = max(report['r_seconds'] / 60.0, EPSILON) except: r_min = 1 try: a_kd = report['a_kills'] / max(report['a_deads'], EPSILON) except: a_kd = 1 try: r_kd = report['r_kills'] / max(report['r_deads'], EPSILON) except: r_kd = 1 kd_diff = r_kd - a_kd try: return { 'a_kd': a_kd, 'a_kpm': report['a_kills'] / a_min, 'a_dpm': report['a_deads'] / a_min, 'r_kd': r_kd, 'r_kpm': report['r_kills'] / r_min, 'r_dpm': report['r_deads'] / r_min, 'kd_diff': kd_diff, 'a_session_min': a_min, 'r_session_min': r_min } except: raise AnyFieldIsNotFilled def _compute_derived_stats(self) -> Dict[str, Dict[str, float]]: """Вычисляет min/max для всех производных признаков по историческим данным.""" stats = {f: {'min': float('inf'), 'max': float('-inf')} for f in self.derived_fields} for r in self.historical_reports: try: feats = self._compute_derived_features(r) for f in self.derived_fields: val = feats[f] if val < stats[f]['min']: stats[f]['min'] = val if val > stats[f]['max']: stats[f]['max'] = val except AnyFieldIsNotFilled: continue return stats def _normalize_derived(self, feats: Dict[str, float]) -> Dict[str, float]: """Нормализует производные признаки в [0,1].""" norm = {} for f in self.derived_fields: min_val = self.derived_stats[f]['min'] max_val = self.derived_stats[f]['max'] if max_val - min_val < EPSILON: norm[f] = 0.0 else: norm[f] = (feats[f] - min_val) / (max_val - min_val) return norm def _similarity(self, r1: Dict[str, Any], r2: Dict[str, Any]) -> float: """Вычисляет взвешенное сходство (0..1) между двумя отчётами.""" score = 0.0 # 1. Совпадение пары Steam #if r1['a_steam2'] == r2['a_steam2'] and r1['r_steam2'] == r2['r_steam2']: # identity_score = 1.0 #elif r1['a_steam2'] == r2['a_steam2'] or r1['r_steam2'] == r2['r_steam2']: # identity_score = 0.5 #else: # identity_score = 0.0 #score += self.weights['steam_identity'] * identity_score # 2. Причина reason_score = 1.0 if r1['reasons'] == r2['reasons'] else 0.0 score += self.weights['reason'] * reason_score # 3. Права perm_score = 0.0 if r1['a_permition'] == r2['a_permition']: perm_score += 0.5 if r1['r_permition'] == r2['r_permition']: perm_score += 0.5 score += self.weights['permissions'] * perm_score # 4. Производные признаки f1 = self._normalize_derived(self._compute_derived_features(r1)) f2 = self._normalize_derived(self._compute_derived_features(r2)) dist_sq = sum((f1[f] - f2[f]) ** 2 for f in self.derived_fields) eucl_dist = math.sqrt(dist_sq / len(self.derived_fields)) derived_sim = 1.0 - min(eucl_dist, 1.0) score += self.weights['derived_stats'] * derived_sim # 5. Сервер #srv_score = 1.0 if r1['srv'] == r2['srv'] else 0.0 #score += self.weights['server'] * srv_score return score # ---------- Публичные асинхронные методы ---------- def initialize(self, reports: List[Dict[str, Any]]) -> None: """ Загружает исторические данные и вычисляет нормализацию. :param reports: список записей из user_reports :param actions: список записей из user_reports_action """ self.historical_reports = reports self.report_actions.clear() for report in self.historical_reports: try: self.report_actions[report["id"]] = report["actions"] except: print("Cannot build action", report) self.derived_stats = self._compute_derived_stats() async def predict(self, new_report: Dict[str, Any]) -> Dict[str, Any]: """ Возвращает рекомендации для новой жалобы. :param new_report: словарь с полями нового репорта (без id) :return: словарь с ключами 'suggestions' и 'similar_reports' """ if not self.historical_reports or self.derived_stats is None: raise RuntimeError("System not initialized. Call 'await initialize()' first.") # Расчёт сходства со всеми историческими записями similarities = [] for hist in self.historical_reports: try: sim = self._similarity(new_report, hist) similarities.append((hist['id'], sim)) except AnyFieldIsNotFilled: pass similarities.sort(key=lambda x: x[1], reverse=True) top_k = similarities[:self.k_neighbors] # Сбор действий action_counter = Counter() similar_ids = [] for rid, sim in top_k: if sim > 0: similar_ids.append(rid) if rid in self.report_actions: for act in self.report_actions[rid]: action_counter[act] += 1 total = sum(action_counter.values()) suggestions = [] if total > 0: for action, count in action_counter.most_common(): suggestions.append({ 'action': action, 'confidence': round(count / total, 4) }) else: suggestions.append({'action': 'none', 'confidence': 1.0}) return { 'suggestions': suggestions, 'similar_reports': similar_ids } async def reload_actions(self, new_actions: List[Dict[str, Any]]) -> None: """Обновить только действия, не пересчитывая нормализацию (если поступили новые решения).""" for act in new_actions: rid = act['report_id'] self.report_actions.setdefault(rid, []).append(act['action']) #depricated class KamazAI_v1_1: WEIGHTS = { 'steam_identity': 0.4, 'reason': 0.2, 'permissions': 0.1, 'numerics': 0.2, 'server': 0.1 } K_NEIGHBORS = 5 ''' { "id":2, "a_nickname":"Роботяга", "a_permition":"DONT_HAVE", "a_kills":6, "a_deads":5, "a_seconds":392, "r_nickname":"zombiskell", "r_permition":"DONT_HAVE", "r_kills":10, "r_deads":10, "r_seconds":2022, "reasons":"Читы", "utime":1717502378, "srv":"srv9", "online":18, "type":"IN_GAME", "actions":["inspect"], "serverName":"Норильск 2019", "a_steam":{ "steam3":"[U:1:1338527208]", "steam2":"STEAM_0:0:669263604", "steam64":"76561199298792936", "community_url":"https://steamcommunity.com/profiles/76561199298792936", "account_id":1338527208 }, "r_steam":{ "steam3":"[U:1:1554861952]", "steam2":"STEAM_0:0:777430976", "steam64":"76561199515127680", "community_url":"https://steamcommunity.com/profiles/76561199515127680", "account_id":1554861952 } } ''' def __init__(self, reports_list): print("Reports list size: ", len(reports_list)) self.historical_reports = reports_list self.report_actions = {} for report in reports_list: try: self.report_actions[report["id"]] = report["actions"] except: print("Cannot build action", report) self.numeric_stats = self.normalize_numerics(reports_list) ''' def init_data(reports: list, actions: list): """Загрузка данных при старте или через специальный endpoint.""" global historical_reports, report_actions historical_reports = reports report_actions.clear() for act in actions: rid = act['report_id'] report_actions.setdefault(rid, []).append(act['action']) ''' def normalize_numerics(self, reports): """Вычисляет min/max для всех числовых полей (обучающая выборка).""" fields = ['a_kills', 'a_deads', 'a_seconds', 'r_kills', 'r_deads', 'r_seconds', 'online'] stats = {} for f in fields: values = [r[f] for r in reports if r[f] is not None] stats[f] = {'min': min(values), 'max': max(values)} return stats async def similarity(self, report1, report2, num_stats): """Возвращает взвешенное сходство (0..1).""" score = 0.0 # 1. Совпадение пары Steam-аккаунтов if False: identity_score = 0.0 if report1['a_steam2'] == report2['a_steam2'] and report1['r_steam2'] == report2['r_steam2']: identity_score = 1.0 elif report1['a_steam2'] == report2['a_steam2'] or report1['r_steam2'] == report2['r_steam2']: identity_score = 0.5 score += self.WEIGHTS['steam_identity'] * identity_score # 2. Причина (точное совпадение) reason_score = 1.0 if report1['reasons'] == report2['reasons'] else 0.0 score += self.WEIGHTS['reason'] * reason_score # 3. Права perm_score = 0.0 if report1['a_permition'] == report2['a_permition']: perm_score += 0.5 if report1['r_permition'] == report2['r_permition']: perm_score += 0.5 score += self.WEIGHTS['permissions'] * perm_score # 4. Числовые поля (нормированное евклидово расстояние -> сходство) #todo kd num_fields = ['a_kills', 'a_deads', 'a_seconds', 'r_kills', 'r_deads', 'r_seconds', 'online'] dist_sq = 0.0 for f in num_fields: min_val = num_stats[f]['min'] max_val = num_stats[f]['max'] if max_val == min_val: norm_diff = 0.0 else: try: norm_diff = (report1[f] - report2[f]) / (max_val - min_val) except: norm_diff = 0.0 dist_sq += norm_diff ** 2 eucl_dist = np.sqrt(dist_sq / len(num_fields)) # Превращаем расстояние в сходство (1 - нормализованное расстояние) num_similarity = 1.0 - min(eucl_dist, 1.0) score += self.WEIGHTS['numerics'] * num_similarity # 5. Сервер if False: srv_score = 1.0 if report1['srv'] == report2['srv'] else 0.0 score += self.WEIGHTS['server'] * srv_score return [score, { "reason_score": reason_score, "perm_score": perm_score, "num_similarity": num_similarity}] async def predict(self, new_report): # Вычисляем сходство со всеми историческими заявками similarities = [] for hist in self.historical_reports: s_container = await self.similarity(new_report, hist, self.numeric_stats) similarities.append((hist['id'], s_container[0], s_container[1])) # Сортируем по убыванию сходства similarities.sort(key=lambda x: x[1], reverse=True) top_k = similarities[:self.K_NEIGHBORS] #print(top_k) # Собираем все действия, назначенные на эти заявки action_counter = Counter() similar_report_ids = [] for rid, sim, score_rate in top_k: #print(rid, sim) if sim > 0: # можно задать порог, чтобы отсечь шум similar_report_ids.append(rid) if rid in self.report_actions: for act in self.report_actions[rid]: action_counter[act] += 1 total = sum(action_counter.values()) suggestions = [] if total > 0: for action, count in action_counter.most_common(): suggestions.append({ 'action': action, 'confidence': round(count / total, 4) }) else: # Если ни одного похожего – предложение "inspect" по умолчанию suggestions.append({'action': 'none', 'confidence': 1.0}) return { 'suggestions': suggestions, 'similar_reports': similar_report_ids } class Extension: core = None actions = { 'ban': ["забанить игрока", 4], 'inspect': ["проверить профиль стима и решить что делать дальше", 1], 'kick': ["кикнуть игрока", 2], 'ban30': ["легкий бан на 30 минут", 3], 'ban120': ["бан на пару часов", 3], 'noreason': ["не вводить причину", -1], 'author_kick': ["кикнуть автора репорта", 2], 'mute': ["замьютить игрока", 1], 'unban': ["разбанить игрока если тот в бане, ебанутое решение", -1], 'author_inspect': ['глянуть профиль автора репорта', 1], 'none': ['ничего не делать, лучше не лезть', 0] } def __init__(self, core): self.core = core self.kamazai = None self.reports_list = [] async def task(self, timeout = 15): if os.getenv('BACKEND_URL') and os.getenv("BACKEND_SECRETKEY"): pass else: print("Cannot init kamazAI, BACKEND_URL or BACKEND_SECRETKEY is missing in env") return await self.core.wait_until_ready() while True: await self.updater() await asyncio.sleep(timeout) async def updater(self): try: if self.kamazai == None: print("Sync report list") async with aiohttp.ClientSession(cookies={ "secretkey":os.getenv("BACKEND_SECRETKEY")}) as session: async with session.get(f"{os.getenv('BACKEND_URL')}/api/discord/report/s", ssl = False) as response: self.reports_list = await response.json() self.kamazai = KamazAI_v1(self.reports_list) print("KamazAI Enabled") except: traceback.print_exc() async def __call__(self, message: discord.Message = None): if self.kamazai == None: print("call kamazAi but he is not init") return await message.reply(content=f'KamazAI не иницилизирован') try: report_id = message.embeds[0].color.value #print("Found report id", report_id) except: return await message.reply(content=f'KamazAI не может получить индификатор репорта') async with aiohttp.ClientSession(cookies={ "secretkey":os.getenv("BACKEND_SECRETKEY")}) as session: async with session.get(f"{os.getenv('BACKEND_URL')}/api/discord/report/{report_id}", ssl = False) as response: report = await response.json() result = await self.kamazai.predict(report) suggestions = result.get("suggestions", [{"confidence":1, "action": "none"}]) embed: discord.Embed = discord.Embed(description="Решил что с этим репортом надо сделать") embed.set_author(name="Камаз AI", icon_url="https://media.istockphoto.com/id/532124854/ru/%D0%B2%D0%B5%D0%BA%D1%82%D0%BE%D1%80%D0%BD%D0%B0%D1%8F/%D0%B5%D0%B2%D1%80%D0%B5%D0%B9%D1%81%D0%BA%D0%B8%D0%B9-%D1%80%D0%BE%D0%B1%D0%BE%D1%82-%D0%BF%D0%B5%D1%80%D1%81%D0%BE%D0%BD%D0%B0%D0%B6%D0%B0.jpg?s=170667a&w=0&k=20&c=3n1zIaQ0zd36upFONeofKPVioEf5JfFnDs6gShydAvw=") for ss in suggestions: if ss["action"] == "noreason": continue embed.add_field( name=f'{round(ss["confidence"] * 100)}%', value=f'{self.actions.get(ss["action"], [ss["action"], 100])[0]}', inline=False) same_reports = [] same_reports.append(str(report_id)) for s in result.get("similar_reports", []): same_reports.append(str(s)) embed.set_footer(text=",".join(same_reports)) response = await message.reply(embed=embed) try: await response.add_reaction('👍') await response.add_reaction('👎') except: pass return response #self.reports_list.append(report) #self.kamazai = KamazAI(self.reports_list) if __name__ == "__main__": async def run(): print("run") from json import load with open("/Users/gsd/Downloads/reports.json", "r", encoding="utf8") as report_list: kamazAi = KamazAI_v1_3(load(report_list)) perm_list = [] for r in kamazAi.historical_reports: if r['a_permition'] not in perm_list: perm_list.append(r['a_permition']) if r['r_permition'] not in perm_list: perm_list.append(r['r_permition']) print(perm_list) test_report1 = {"id":23059,"a_nickname":"серийный чувак","a_permition":"DONT_HAVE","a_kills":29,"a_deads":38,"a_seconds":3237,"r_nickname":"Корабль Бомж 1","r_permition":"DONT_HAVE","r_kills":44,"r_deads":31,"r_seconds":3598,"reasons":"Читы","utime":1780842262,"srv":"srv9","online":18,"type":"IN_GAME","actions":[],"serverName":"Норильск 2019","a_steam":{"steam3":"[U:1:1675616295]","steam2":"STEAM_0:1:837808147","steam64":"76561199635882023","community_url":"https://steamcommunity.com/profiles/76561199635882023","account_id":1675616295},"r_steam":{"steam3":"[U:1:1546493291]","steam2":"STEAM_0:1:773246645","steam64":"76561199506759019","community_url":"https://steamcommunity.com/profiles/76561199506759019","account_id":1546493291}} test_report2 = {"id":23048,"a_nickname":"Евгений Задротов","a_permition":"FREE","a_kills":63,"a_deads":68,"a_seconds":5031,"r_nickname":"Kapusta_KvSH","r_permition":"VIP","r_kills":11,"r_deads":2,"r_seconds":436,"reasons":"VIP абуз","utime":1780837315,"srv":"srv5","online":21,"type":"IN_GAME","actions":[],"serverName":"Завод Ultimate","a_steam":{"steam3":"[U:1:1623272121]","steam2":"STEAM_0:1:811636060","steam64":"76561199583537849","community_url":"https://steamcommunity.com/profiles/76561199583537849","account_id":1623272121},"r_steam":{"steam3":"[U:1:196806785]","steam2":"STEAM_0:1:98403392","steam64":"76561198157072513","community_url":"https://steamcommunity.com/profiles/76561198157072513","account_id":196806785}} print(test_report1) print(await kamazAi.predict(test_report1)) print() print(test_report2) print(await kamazAi.predict(test_report2)) import asyncio asyncio.run(run())