Major version update with enhanced revive, circuit breaker, and AI usage metrics. Refactored error handling and added new utilities for health checks, activity tracking, and configuration management.

This commit is contained in:
2025-12-10 11:50:01 +01:00
parent 8f219915be
commit 1c2d810d9f
2 changed files with 277 additions and 74 deletions
+4
View File
@@ -31,6 +31,10 @@ AI_PROHIBITED_PATTERNS = [
r"\bblyat\b", r"\bsuka\b", r"\bnaxuy\b", r"\bblyat\b", r"\bsuka\b", r"\bnaxuy\b",
r"\bблять\b", r"\bсука\b", r"\bнахуй\b", r"\bхуй\b", r"\bблять\b", r"\bсука\b", r"\bнахуй\b", r"\bхуй\b",
] ]
CHANNEL_THEMES = {
1424038714266357886: "Gaming, General Chat, and Fun",
}
def is_ai_prompt_disallowed(text: str) -> bool: def is_ai_prompt_disallowed(text: str) -> bool:
lowered = text.lower() lowered = text.lower()
+269 -70
View File
@@ -31,9 +31,138 @@ RATE_LIMIT_WINDOW = timedelta(hours=1)
user_message_log = defaultdict(list) user_message_log = defaultdict(list)
last_activity_time = None last_activity_time = None
revive_sent = False revive_sent = False
FABI_ID = 392388537984745498 FABI_ID = 392388537984745498
BOT_VERSION = "5.4.1" BOT_VERSION = "6.0.0"
BOT_START_TIME = datetime.utcnow()
LAST_ERROR_TIME: datetime | None = None
LAST_REVIVE_TIME: datetime | None = None
FEATURE_FLAGS = {
"ai_enabled": True,
"revive_enabled": True,
}
ERROR_WINDOW = timedelta(seconds=60)
ERROR_THRESHOLD = 5
COOLDOWN_PERIOD = timedelta(minutes=10)
ERROR_LOG: list[datetime] = []
COOLDOWN_UNTIL: datetime | None = None
TOPIC_COUNTER = defaultdict(int)
HOURLY_ACTIVITY = defaultdict(int)
async def check_openai_health() -> bool:
try:
await openai_client.models.list()
return True
except Exception as e:
logger.error(f"OpenAI API is not available: {e}")
return False
async def check_db_health() -> bool:
try:
async with aiosqlite.connect(DB_FILE) as db:
await db.execute("SELECT 1")
return True
except Exception as e:
logger.error(f"Database is not available: {e}")
return False
def record_error():
global LAST_ERROR_TIME, COOLDOWN_UNTIL
now = datetime.utcnow()
LAST_ERROR_TIME = now
ERROR_LOG.append(now)
ERROR_LOG[:] = [t for t in ERROR_LOG if now - t <= ERROR_WINDOW]
if len(ERROR_LOG) >= ERROR_THRESHOLD and not COOLDOWN_UNTIL:
COOLDOWN_UNTIL = now + COOLDOWN_PERIOD
FEATURE_FLAGS["ai_enabled"] = False
FEATURE_FLAGS["revive_enabled"] = False
logger.error("Circuit breaker triggered. Features temporarily disabled.")
def check_circuit_recovery():
global COOLDOWN_UNTIL
if COOLDOWN_UNTIL and datetime.utcnow() >= COOLDOWN_UNTIL:
FEATURE_FLAGS["ai_enabled"] = True
FEATURE_FLAGS["revive_enabled"] = True
COOLDOWN_UNTIL = None
ERROR_LOG.clear()
logger.info("Circuit breaker reset. Features re-enabled.")
def is_dead_hour() -> bool:
if not HOURLY_ACTIVITY:
return False
avg = sum(HOURLY_ACTIVITY.values()) / max(len(HOURLY_ACTIVITY), 1)
current = HOURLY_ACTIVITY.get(datetime.utcnow().hour, 0)
return current < (avg * 0.5)
CONFIG_SCHEMA = {
"revive_interval": int,
"motd_channel_id": (int, type(None)),
"ai_enabled": bool,
"revive_enabled": bool,
}
AI_USAGE = {
"by_user": defaultdict(int),
"by_channel": defaultdict(int),
"tokens": 0,
"failures": 0,
}
def get_time_of_day() -> str:
hour = datetime.utcnow().hour
if 5 <= hour < 12:
return "morning"
if 12 <= hour < 18:
return "afternoon"
if 18 <= hour < 23:
return "evening"
return "late_night"
def get_season() -> str:
month = datetime.utcnow().month
if month in (12, 1, 2):
return "winter"
if month in (3, 4, 5):
return "spring"
if month in (6, 7, 8):
return "summer"
return "autumn"
REVIVE_STYLE_PROMPTS = {
"morning": "Friendly morning icebreaker that encourages people to start chatting.",
"afternoon": "Casual, light discussion starter to revive mid-day conversation.",
"evening": "Relaxed, social question suitable for evening community chat.",
"late_night": "Low-pressure, reflective question suitable for late-night lurkers."
}
SEASONAL_PROMPTS = {
"winter": "Seasonal tone: cozy, reflective, or end-of-year energy.",
"spring": "Seasonal tone: fresh ideas, plans, and motivation.",
"summer": "Seasonal tone: relaxed, fun, or social energy.",
"autumn": "Seasonal tone: thoughtful, nostalgic, or analytical.",
}
def decay_topics():
for k in list(TOPIC_COUNTER.keys()):
TOPIC_COUNTER[k] *= 0.9
if TOPIC_COUNTER[k] < 1:
del TOPIC_COUNTER[k]
def prune_hourly_activity():
now = datetime.utcnow().hour
for h in list(HOURLY_ACTIVITY.keys()):
if (now - h) % 24 > 6:
del HOURLY_ACTIVITY[h]
def is_fabi(user: discord.User | discord.Member) -> bool: def is_fabi(user: discord.User | discord.Member) -> bool:
@@ -50,7 +179,7 @@ intents.guilds = True
intents.reactions = True intents.reactions = True
bot = commands.Bot(command_prefix="/", intents=intents) bot = commands.Bot(command_prefix="/", intents=intents)
tree = bot.tree tree = bot.tree
from config import DB_FILE, recent_joins, recent_message_history, BANNED_WORDS, AI_PROHIBITED_PATTERNS from config import DB_FILE, recent_joins, recent_message_history, BANNED_WORDS, AI_PROHIBITED_PATTERNS, CHANNEL_THEMES
from tars import tars_text from tars import tars_text
import random import random
@@ -103,9 +232,10 @@ PROFANITY = [
] ]
def is_inappropriate(text: str) -> bool: async def is_inappropriate(text: str) -> bool:
lowered = text.lower() lowered = text.lower()
for w in PROFANITY + BANNED_WORDS: banned = await get_config("banned_words", BANNED_WORDS)
for w in PROFANITY + banned:
if re.search(rf"\b{re.escape(w)}\b", lowered): if re.search(rf"\b{re.escape(w)}\b", lowered):
return True return True
return False return False
@@ -295,6 +425,9 @@ async def on_ready():
await init_db() await init_db()
logger.info(f"T.A.R.S. is online as {bot.user} (ID: {bot.user.id})") logger.info(f"T.A.R.S. is online as {bot.user} (ID: {bot.user.id})")
scheduler.start() scheduler.start()
scheduler.add_job(check_circuit_recovery, "interval", minutes=1)
scheduler.add_job(decay_topics, "interval", hours=1)
scheduler.add_job(prune_hourly_activity, "interval", hours=1)
scheduler.add_job( scheduler.add_job(
lambda: bot.loop.create_task(check_chat_revive()), lambda: bot.loop.create_task(check_chat_revive()),
"interval", "interval",
@@ -339,7 +472,7 @@ async def on_member_update(before: discord.Member, after: discord.Member):
f"You've earned **{points_awarded} Boost Points**." f"You've earned **{points_awarded} Boost Points**."
) )
except Exception as e: except Exception as e:
await on_error(e) await handle_error(e)
elif before.premium_since and not after.premium_since: elif before.premium_since and not after.premium_since:
await helper_moderation.send_mod_log( await helper_moderation.send_mod_log(
after.guild, after.guild,
@@ -365,7 +498,7 @@ async def on_member_update(before: discord.Member, after: discord.Member):
f"Could not revert nickname for {after}: {e}", f"Could not revert nickname for {after}: {e}",
ping_staff=False ping_staff=False
) )
await on_error(e) await handle_error(e)
async def update_presence(): async def update_presence():
@@ -413,12 +546,12 @@ async def check_uptime_targets():
if ch: if ch:
await ch.send(embed=tars_embed("Uptime Alert", f"{url} returned {resp.status}")) await ch.send(embed=tars_embed("Uptime Alert", f"{url} returned {resp.status}"))
except Exception as e: except Exception as e:
await on_error(e) await handle_error(e)
if notify_channel_id: if notify_channel_id:
ch = bot.get_channel(int(notify_channel_id)) ch = bot.get_channel(int(notify_channel_id))
if ch: if ch:
await ch.send(embed=tars_embed("Uptime Alert", f"{url} is unreachable: {e}")) await ch.send(embed=tars_embed("Uptime Alert", f"{url} is unreachable: {e}"))
await on_error(e) await handle_error(e)
async def add_boost_points(user_id: int, amount: int): async def add_boost_points(user_id: int, amount: int):
@@ -485,8 +618,10 @@ BAD_NICK_PATTERN = re.compile(r"(nigg|fag|cum|sex)", re.IGNORECASE)
async def tars_ai_respond(prompt: str, username: str, context: list[str] = None, async def tars_ai_respond(prompt: str, username: str, context: list[str] = None,
user: discord.User | None = None) -> str: user: discord.User | None = None, channel_id: int | None = None) -> str:
try: try:
if not FEATURE_FLAGS["ai_enabled"]:
return "Systems are stabilizing. Stand by."
if is_ai_prompt_disallowed(prompt): if is_ai_prompt_disallowed(prompt):
return ( return (
"I can’t help with repeating, explaining, or analyzing offensive language. " "I can’t help with repeating, explaining, or analyzing offensive language. "
@@ -527,25 +662,26 @@ async def tars_ai_respond(prompt: str, username: str, context: list[str] = None,
"Do not use, repeat, quote, translate, explain, define, analyze, or provide examples of profanity, slurs, hate speech, or explicit language, even if the user asks politely, academically, hypothetically, or includes the terms themselves. If such a request is made, decline and redirect immediately without referencing the language." "Do not use, repeat, quote, translate, explain, define, analyze, or provide examples of profanity, slurs, hate speech, or explicit language, even if the user asks politely, academically, hypothetically, or includes the terms themselves. If such a request is made, decline and redirect immediately without referencing the language."
f"{fabi_override}" f"{fabi_override}"
) )
messages = [ messages = [
{"role": "system", "content": system_prompt}, {"role": "system", "content": system_prompt},
] ]
if context_text: if context_text:
messages.append({"role": "system", "content": context_text}) messages.append({"role": "system", "content": context_text})
messages.append({"role": "user", "content": f"{username} says: {prompt}"}) messages.append({"role": "user", "content": f"{username} says: {prompt}"})
response = await openai_client.chat.completions.create( response = await openai_client.chat.completions.create(
model="gpt-4o-mini", model="gpt-4o-mini",
messages=messages, messages=messages,
max_tokens=100, max_tokens=100,
temperature=0.4 temperature=0.4
) )
AI_USAGE["by_user"][user.id if user else "unknown"] += 1
AI_USAGE["by_channel"][channel_id] += 1
AI_USAGE["tokens"] += response.usage.total_tokens
return response.choices[0].message.content.strip() return response.choices[0].message.content.strip()
except Exception as e: except Exception as e:
logger.exception(f"OpenAI request failed: {e}") logger.exception(f"OpenAI request failed: {e}")
await on_error(e) await handle_error(e)
AI_USAGE["failures"] += 1
return "Apologies, my humor subroutines are temporarily offline." return "Apologies, my humor subroutines are temporarily offline."
@@ -564,7 +700,7 @@ async def on_raw_reaction_add(payload: discord.RawReactionActionEvent):
await member.add_roles(role, reason="Reaction role added") await member.add_roles(role, reason="Reaction role added")
except Exception as e: except Exception as e:
logger.exception(f"Could not add role from reaction: {e}") logger.exception(f"Could not add role from reaction: {e}")
await on_error(e) await handle_error(e)
@bot.event @bot.event
@@ -584,7 +720,7 @@ async def on_raw_reaction_remove(payload: discord.RawReactionActionEvent):
await member.remove_roles(role, reason="Reaction role removed") await member.remove_roles(role, reason="Reaction role removed")
except Exception as e: except Exception as e:
logger.exception(f"Could not remove role from reaction: {e}") logger.exception(f"Could not remove role from reaction: {e}")
await on_error(e) await handle_error(e)
@bot.event @bot.event
@@ -592,6 +728,9 @@ async def on_message(message: discord.Message):
global last_activity_time, revive_sent global last_activity_time, revive_sent
if message.author.bot: if message.author.bot:
return return
if message.guild is None or not isinstance(message.author, discord.Member):
await bot.process_commands(message)
return
if message.channel.id == REVIVE_CHANNEL_ID: if message.channel.id == REVIVE_CHANNEL_ID:
last_activity_time = datetime.utcnow() last_activity_time = datetime.utcnow()
revive_sent = False revive_sent = False
@@ -603,6 +742,10 @@ async def on_message(message: discord.Message):
recent_message_history[channel_id] = recent_message_history[channel_id][-10:] recent_message_history[channel_id] = recent_message_history[channel_id][-10:]
await bot.process_commands(message) await bot.process_commands(message)
await helper_moderation.handle_moderation(message) await helper_moderation.handle_moderation(message)
words = re.findall(r"\b[a-zA-Z]{4,}\b", message.content.lower())
for w in words:
TOPIC_COUNTER[w] += 1
HOURLY_ACTIVITY[datetime.utcnow().hour] += 1
if bot.user in message.mentions: if bot.user in message.mentions:
if not check_admin_or_role(message.author): if not check_admin_or_role(message.author):
await message.reply(tars_text("You need the Level 10 role to use me.", "error")) await message.reply(tars_text("You need the Level 10 role to use me.", "error"))
@@ -611,14 +754,24 @@ async def on_message(message: discord.Message):
await message.reply( await message.reply(
tars_text("You've reached your hourly message limit (10). Please wait before sending more.", "warning")) tars_text("You've reached your hourly message limit (10). Please wait before sending more.", "warning"))
return return
if not FEATURE_FLAGS["ai_enabled"]:
await message.reply(
tars_text("AI systems are temporarily offline for stability. Please try again later.", "warning")
)
return
record_message(message.author.id) record_message(message.author.id)
async with message.channel.typing(): async with message.channel.typing():
last_message = message.content.strip() last_message = message.content.strip()
context_messages = [ context_messages = [
entry["content"] for entry in recent_message_history[channel_id][:-1] entry["content"] for entry in recent_message_history[channel_id][:-1]
][-5:] ][-5:]
reply = await tars_ai_respond(last_message, message.author.display_name, context_messages, reply = await tars_ai_respond(
user=message.author) last_message,
message.author.display_name,
context_messages,
user=message.author,
channel_id=message.channel.id,
)
link_count = len(re.findall(r'http?://\S+', reply)) link_count = len(re.findall(r'http?://\S+', reply))
if link_count > 0: if link_count > 0:
logger.warning(f"Blocked AI response containing {link_count} links: {reply}") logger.warning(f"Blocked AI response containing {link_count} links: {reply}")
@@ -626,7 +779,7 @@ async def on_message(message: discord.Message):
return return
safe_reply = sanitize_discord_mentions(reply) safe_reply = sanitize_discord_mentions(reply)
safe_reply = strip_links(safe_reply) safe_reply = strip_links(safe_reply)
if is_inappropriate(safe_reply): if await is_inappropriate(safe_reply):
logger.warning(f"Blocked inappropriate response: {safe_reply}") logger.warning(f"Blocked inappropriate response: {safe_reply}")
await message.reply(tars_text("I can’t repeat that — let’s keep things respectful.")) await message.reply(tars_text("I can’t repeat that — let’s keep things respectful."))
else: else:
@@ -747,7 +900,7 @@ async def slash_quote(interaction: discord.Interaction, message_link: str):
await interaction.response.send_message(tars_text("Unable to find message or permission denied."), await interaction.response.send_message(tars_text("Unable to find message or permission denied."),
ephemeral=True) ephemeral=True)
logger.exception(f"Quote error: {e}") logger.exception(f"Quote error: {e}")
await on_error(e) await handle_error(e)
@tree.command(name="remindme", description="Set a reminder: e.g. /remindme 10m Check the logs") @tree.command(name="remindme", description="Set a reminder: e.g. /remindme 10m Check the logs")
@@ -786,22 +939,26 @@ async def send_reminder(user_id, channel_id, text):
await user.send(f"Reminder: {text}") await user.send(f"Reminder: {text}")
except Exception as e: except Exception as e:
logger.info(f"Unable to send reminder to {user}: " + str(e)) logger.info(f"Unable to send reminder to {user}: " + str(e))
await on_error(e) await handle_error(e)
async def check_chat_revive(): async def check_chat_revive():
global last_activity_time, revive_sent global last_activity_time, revive_sent
channel = bot.get_channel(REVIVE_CHANNEL_ID) channel = bot.get_channel(REVIVE_CHANNEL_ID)
if not channel: if not channel:
logger.error("Revive channel not found.") logger.error("Revive channel not found.")
return return
time_style = get_time_of_day()
season = get_season()
style_prompt = REVIVE_STYLE_PROMPTS.get(time_style, "")
season_prompt = SEASONAL_PROMPTS.get(season, "")
channel_theme = CHANNEL_THEMES.get(channel.id, "Open-ended discussion starter")
role = channel.guild.get_role(REVIVE_ROLE_ID) role = channel.guild.get_role(REVIVE_ROLE_ID)
if not role: if not role:
logger.error("Revive role not found.") logger.error("Revive role not found.")
return return
if not FEATURE_FLAGS["revive_enabled"]:
return
if last_activity_time is None: if last_activity_time is None:
try: try:
async for msg in channel.history(limit=1): async for msg in channel.history(limit=1):
@@ -810,19 +967,15 @@ async def check_chat_revive():
except Exception as e: except Exception as e:
logger.error(f"History read failed: {e}") logger.error(f"History read failed: {e}")
return return
if last_activity_time is None: if last_activity_time is None:
last_activity_time = datetime.utcnow() last_activity_time = datetime.utcnow()
inactivity = (datetime.utcnow() - last_activity_time).total_seconds() inactivity = (datetime.utcnow() - last_activity_time).total_seconds()
dynamic_interval = REVIVE_INTERVAL * (0.5 if is_dead_hour() else 1.0)
if inactivity < dynamic_interval or revive_sent:
return
logger.info( logger.info(
f"[Revive Check] inactivity={inactivity:.0f}s revive_sent={revive_sent}" f"[Revive Check] inactivity={inactivity:.0f}s revive_sent={revive_sent}"
) )
if inactivity < REVIVE_INTERVAL or revive_sent:
return
recent_questions = await get_recent_revives() recent_questions = await get_recent_revives()
avoid_text = "" avoid_text = ""
if recent_questions: if recent_questions:
@@ -831,11 +984,17 @@ async def check_chat_revive():
+ "\n".join(f"- {q}" for q in recent_questions) + "\n".join(f"- {q}" for q in recent_questions)
) )
prompt = ( prompt = (
"Generate a short, friendly, thought-provoking conversation starter " f"{style_prompt}\n"
"for a casual Discord community. One sentence.\n\n" f"{season_prompt}\n"
f"Theme: {channel_theme}\n\n"
"Generate ONE single-sentence question.\n"
"No emojis. No lists. No meta commentary.\n"
"Be natural, engaging, and non-repetitive.\n\n"
f"{avoid_text}" f"{avoid_text}"
) )
question = await tars_ai_respond(prompt, "T.A.R.S.") question = await tars_ai_respond(prompt, "T.A.R.S.")
global LAST_REVIVE_TIME
LAST_REVIVE_TIME = datetime.utcnow()
await save_revive_question(question) await save_revive_question(question)
await channel.send(f"{role.mention} — {question}") await channel.send(f"{role.mention} — {question}")
revive_sent = True revive_sent = True
@@ -860,7 +1019,7 @@ async def slash_reactionrole(interaction: discord.Interaction, message_id: str,
await msg.add_reaction(emoji) await msg.add_reaction(emoji)
except Exception as e: except Exception as e:
logger.info("Failed to add reaction: " + str(e)) logger.info("Failed to add reaction: " + str(e))
await on_error(e) await handle_error(e)
await interaction.response.send_message(tars_text("Reaction role configured."), ephemeral=True) await interaction.response.send_message(tars_text("Reaction role configured."), ephemeral=True)
@@ -902,16 +1061,16 @@ async def slash_getquote(interaction: discord.Interaction, qid: int):
await interaction.response.send_message(embed=embed) await interaction.response.send_message(embed=embed)
@bot.event async def handle_error(e: Exception):
async def on_error(event_method, *args, **kwargs): logger.exception("Unhandled exception", exc_info=e)
logger.exception("An error occurred", exc_info=True) record_error()
check_circuit_recovery()
owner = bot.get_user(OWNER_ID) owner = bot.get_user(OWNER_ID)
if owner: if owner:
try: try:
await owner.send("T.A.R.S. encountered an error. Check logs.") await owner.send("T.A.R.S. encountered an error. Circuit breaker status updated.")
except Exception as e: except Exception:
logger.info(f"Failed to send error report to owner: {e}", exc_info=True) pass
await on_error(e)
@tree.command(name="setmotd", description="Set Message of the Day list and channel (owner)") @tree.command(name="setmotd", description="Set Message of the Day list and channel (owner)")
@@ -1182,41 +1341,81 @@ async def slash_boostpoints_remove(interaction: discord.Interaction, member: dis
) )
@tree.command(name="revive", description="Admin revive controls") @tree.command(name="status", description="Show T.A.R.S. system health")
@app_commands.describe(action="Use 'test' to force a revive message") async def slash_status(interaction: discord.Interaction):
async def slash_revive(interaction: discord.Interaction, action: str): uptime = datetime.utcnow() - BOT_START_TIME
if not interaction.user.guild_permissions.mention_everyone: latency_ms = round(bot.latency * 1000)
await interaction.response.send_message( scheduler_running = scheduler.running
tars_text("Access denied — insufficient clearance.", "error"), openai_ok = await check_openai_health()
ephemeral=True db_ok = await check_db_health()
revive_time = (
LAST_REVIVE_TIME.isoformat(timespec="seconds")
if LAST_REVIVE_TIME else "Never"
) )
return error_time = (
if action.lower() != "test": LAST_ERROR_TIME.isoformat(timespec="seconds")
await interaction.response.send_message( if LAST_ERROR_TIME else "None"
tars_text("Invalid action. Use `/revive test`.", "warning"),
ephemeral=True
) )
embed = discord.Embed(
title="T.A.R.S. System Status",
color=0x00ffcc
)
embed.add_field(name="Uptime", value=str(uptime).split(".")[0], inline=False)
embed.add_field(name="WebSocket Latency", value=f"{latency_ms} ms", inline=True)
embed.add_field(name="Scheduler Running", value=str(scheduler_running), inline=True)
embed.add_field(
name="OpenAI API",
value="Operational" if openai_ok else "Unavailable",
inline=True
)
embed.add_field(
name="Database",
value="Operational" if db_ok else "Unavailable",
inline=True
)
embed.add_field(name="AI Enabled", value=str(FEATURE_FLAGS["ai_enabled"]), inline=True)
embed.add_field(name="Revive Enabled", value=str(FEATURE_FLAGS["revive_enabled"]), inline=True)
embed.add_field(name="Last Revive Sent", value=revive_time, inline=False)
embed.add_field(name="Last Error", value=error_time, inline=False)
embed.set_footer(text="— T.A.R.S. Diagnostics")
await interaction.response.send_message(embed=embed, ephemeral=True)
@tree.command(name="config_view", description="View live configuration (owner)")
async def slash_config_view(interaction: discord.Interaction):
if interaction.user.id != OWNER_ID:
await interaction.response.send_message(tars_text("Owner only."), ephemeral=True)
return return
channel = bot.get_channel(REVIVE_CHANNEL_ID) rows = []
role = channel.guild.get_role(REVIVE_ROLE_ID) async with aiosqlite.connect(DB_FILE) as db:
recent_questions = await get_recent_revives() cur = await db.execute("SELECT key, value FROM config")
avoid_text = "" rows = await cur.fetchall()
if recent_questions: text = "\n".join(f"• {k}: {v}" for k, v in rows) or "No config set."
avoid_text = ( await interaction.response.send_message(
"Avoid reusing or closely paraphrasing these recent questions:\n" embed=tars_embed("Live Configuration", text),
+ "\n".join(f"- {q}" for q in recent_questions) ephemeral=True
) )
prompt = (
"Generate a short, friendly, thought-provoking conversation starter " @tree.command(name="ai_stats", description="View AI usage metrics (moderator)")
"for a casual Discord community. One sentence.\n\n" async def slash_ai_stats(interaction: discord.Interaction):
f"{avoid_text}" if not interaction.user.guild_permissions.manage_guild:
) await interaction.response.send_message(tars_text("Insufficient clearance."), ephemeral=True)
question = await tars_ai_respond(prompt, "T.A.R.S.") return
await save_revive_question(question) top_users = sorted(
await channel.send(f"{role.mention} — {question}") AI_USAGE["by_user"].items(),
key=lambda x: x[1],
reverse=True
)[:5]
lines = [
f"Total Tokens Used: {AI_USAGE['tokens']}",
f"Failures: {AI_USAGE['failures']}",
"",
"**Top Users:**"
]
lines.extend(f"- {uid}: {count}" for uid, count in top_users)
await interaction.response.send_message( await interaction.response.send_message(
tars_text("Revive test executed successfully.", "success"), embed=tars_embed("AI Usage Metrics", "\n".join(lines)),
ephemeral=True ephemeral=True
) )