3 changed files with 277 additions and 1 deletions
@ -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()) |
|||
|
|||
|
|||
Loading…
Reference in new issue