diff --git a/Dockerfile b/Dockerfile index 1863f11..9a8210f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM python:3.10 -RUN python -m pip install git+https://github.com/Rapptz/discord.py aiohttp websocket-client +RUN python -m pip install git+https://github.com/Rapptz/discord.py aiohttp websocket-client numpy ENV PYTHONUNBUFFERED 1 WORKDIR /app COPY ./ ./ diff --git a/other_ext/kamaz_ai.py b/other_ext/kamaz_ai.py new file mode 100644 index 0000000..3b0422d --- /dev/null +++ b/other_ext/kamaz_ai.py @@ -0,0 +1,267 @@ +from discord.ext import tasks +import discord +import traceback +import asyncio +import aiohttp +import os + +import numpy as np +from collections import Counter + +#ai sloop +class KamazAI: + 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): + self.historical_reports = reports_list + self.report_actions = {} + for report in reports_list: + self.report_actions[report["id"]] = report["actions"] + + 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. Числовые поля (нормированное евклидово расстояние -> сходство) + 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 + + async def predict(self, new_report): + # Вычисляем сходство со всеми историческими заявками + similarities = [] + for hist in self.historical_reports: + sim = await self.similarity(new_report, hist, self.numeric_stats) + similarities.append((hist['id'], sim)) + + # Сортируем по убыванию сходства + 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 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': 'inspect', 'confidence': 1.0}) + + return { + 'suggestions': suggestions, + 'similar_reports': similar_report_ids + } + +class Extension: + core = None + 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.post(f"{os.getenv('BACKEND_URL')}/api/discord/report/s", ssl = False) as response: + self.reports_list = await response.json() + self.kamazai = KamazAI(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 + except: + return await message.reply(content=f'KamazAI не может получить индификатор репорта') + + async with aiohttp.ClientSession(cookies={ + "secretkey":os.getenv("BACKEND_SECRETKEY")}) as session: + async with session.post(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", []) + s = [] + for ss in suggestions: + s.append(f'{ss["action"]} c вероятностью {ss['confidence']}') + + content = f'KamazAI решил что с этим репортом надо сделать: ' + ",".join(s) + "\nОцените решение камаза где :thumbsup: - норм или :thumbsdown: - не очень" + response = await message.reply(content=content) + try: + await response.add_reaction(':thumbsup:') + await response.add_reaction(':thumbsdown:') + 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(load(report_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()) + + \ No newline at end of file diff --git a/other_ext/webhook_helper.py b/other_ext/webhook_helper.py index 3a3ec4a..c380fcd 100644 --- a/other_ext/webhook_helper.py +++ b/other_ext/webhook_helper.py @@ -57,6 +57,15 @@ class Extension: await message.add_reaction(emoji) except Exception as err: print(f"Cannot add reaction on webhook, error: {err}") + + try: + #kamazai + kwargs = {'message': message} + await self.core.loaded_extensions['kamaz_ai'](**kwargs) + except: + print("Cannot call kamazAI") + traceback.print_exc() + return @core.listen()