import re import time import asyncio import logging from datetime import datetime, timezone from typing import Dict, Any, List, Optional, Tuple logger = logging.getLogger("AGYUsageMonitor") def make_quota_bar(percent_remaining: float, length: int = 10) -> str: """ Generates an emoji progress bar for remaining quota: >= 60%: 🟩 Green (Safe/High) 20% - 59%: 🟨 Yellow (Medium) < 20%: 🟥 Red (Low) """ percent = max(0.0, min(100.0, float(percent_remaining))) filled = int(round((percent / 100.0) * length)) filled = max(0, min(length, filled)) if percent >= 60: fill_char = "🟩" elif percent >= 20: fill_char = "🟨" else: fill_char = "🟥" empty_char = "⬜" return f"{fill_char * filled}{empty_char * (length - filled)}" def parse_iso_time_remaining(iso_str: str, is_fa: bool = True) -> str: """Calculates relative time remaining until reset timestamp.""" if not iso_str: return "نامشخص" if is_fa else "Unknown" try: clean_iso = iso_str.strip().replace("Z", "+00:00") target_dt = datetime.fromisoformat(clean_iso) now_dt = datetime.now(timezone.utc) diff_sec = int((target_dt - now_dt).total_seconds()) if diff_sec <= 0: return "هماکنون در حال تمدید" if is_fa else "Resetting now" days = diff_sec // 86400 hours = (diff_sec % 86400) // 3600 mins = (diff_sec % 3600) // 60 if is_fa: parts = [] if days > 0: parts.append(f"{days} روز") if hours > 0: parts.append(f"{hours} ساعت") if mins > 0 or not parts: parts.append(f"{mins} دقیقه") return f"{' و '.join(parts)} دیگر" else: parts = [] if days > 0: parts.append(f"{days}d") if hours > 0: parts.append(f"{hours}h") parts.append(f"{mins}m") return f"in {' '.join(parts)}" except Exception as e: return iso_str def parse_agy_usage_output(usage_raw: str, credits_raw: str) -> Dict[str, Any]: """Parses raw stdout from 'agy --print /usage' and 'agy --print /credits'.""" data: Dict[str, Any] = { "gemini_5h": None, "gemini_5h_reset": "", "gemini_weekly": None, "gemini_weekly_reset": "", "claude_5h": None, "claude_5h_reset": "", "claude_weekly": None, "claude_weekly_reset": "", "credits": "0", "raw_text": usage_raw + "\n" + credits_raw, } lines = (usage_raw + "\n" + credits_raw).splitlines() for line in lines: line_clean = line.strip() if not line_clean: continue # Check Gemini 5H if re.search(r"Gemini.*Five Hour Limit Remaining", line_clean, re.I): m = re.search(r"(\d+)%\s+(\S+)", line_clean) if m: data["gemini_5h"] = int(m.group(1)) data["gemini_5h_reset"] = m.group(2) # Check Gemini Weekly elif re.search(r"Gemini.*Weekly Limit Remaining", line_clean, re.I): m = re.search(r"(\d+)%\s+(\S+)", line_clean) if m: data["gemini_weekly"] = int(m.group(1)) data["gemini_weekly_reset"] = m.group(2) # Check Claude/GPT 5H elif re.search(r"(Claude|GPT).*Five Hour Limit Remaining", line_clean, re.I): m = re.search(r"(\d+)%\s+(\S+)", line_clean) if m: data["claude_5h"] = int(m.group(1)) data["claude_5h_reset"] = m.group(2) # Check Claude/GPT Weekly elif re.search(r"(Claude|GPT).*Weekly Limit Remaining", line_clean, re.I): m = re.search(r"(\d+)%\s+(\S+)", line_clean) if m: data["claude_weekly"] = int(m.group(1)) data["claude_weekly_reset"] = m.group(2) # Check credits elif re.search(r"Remaining credits", line_clean, re.I): m = re.search(r"Remaining credits\s+(\d+)", line_clean, re.I) if m: data["credits"] = m.group(1) return data async def fetch_and_render_usage_report(is_fa: bool = True) -> str: """Executes agy /usage and /credits and formats with visual progress bars.""" usage_raw = "" credits_raw = "" try: proc1 = await asyncio.create_subprocess_exec( "agy", "--print", "/usage", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) out1, _ = await proc1.communicate() usage_raw = out1.decode("utf-8", errors="replace").strip() proc2 = await asyncio.create_subprocess_exec( "agy", "--print", "/credits", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) out2, _ = await proc2.communicate() credits_raw = out2.decode("utf-8", errors="replace").strip() except Exception as e: logger.error(f"Failed to query AGY quota: {e}") usage_raw = f"Error: {e}" parsed = parse_agy_usage_output(usage_raw, credits_raw) g_5h = parsed["gemini_5h"] g_wk = parsed["gemini_weekly"] c_5h = parsed["claude_5h"] c_wk = parsed["claude_weekly"] credits_val = parsed["credits"] if g_5h is None and g_wk is None and c_5h is None: # Fallback to pre-formatted raw output if parsing failed if is_fa: return ( "📊 گزارش لحظهای سهمیه و وضعیت مصرف AGY\n\n" f"
{usage_raw}\n\n{credits_raw}\n\n"
"💡 سهمیههای ۵ ساعته و هفتگی به صورت خودکار تمدید میشوند."
)
else:
return (
"📊 Real-time AGY Quota & Usage Report\n\n"
f"{usage_raw}\n\n{credits_raw}\n\n"
"💡 Quotas are reset on 5-hour and 7-day windows."
)
if is_fa:
lines = [
"📈 گزارش وضعیت سهمیه و مصرف هوش مصنوعی (AGY)",
"",
"✨ ۱. مدلهای Gemini (جمینای):",
]
if g_5h is not None:
bar_5h = make_quota_bar(g_5h)
reset_5h = parse_iso_time_remaining(parsed["gemini_5h_reset"], is_fa=True)
lines.extend([
f" ⏳ سهمیه ۵ ساعته:",
f" {bar_5h} {g_5h}% باقیمانده",
f" • 🔄 زمان تمدید: {reset_5h}",
])
if g_wk is not None:
bar_wk = make_quota_bar(g_wk)
reset_wk = parse_iso_time_remaining(parsed["gemini_weekly_reset"], is_fa=True)
lines.extend([
f" 📅 سهمیه هفتگی:",
f" {bar_wk} {g_wk}% باقیمانده",
f" • 🔄 زمان تمدید: {reset_wk}",
])
lines.extend([
"",
"🌟 ۲. مدلهای Claude و GPT:",
]
)
if c_5h is not None:
bar_c5 = make_quota_bar(c_5h)
reset_c5 = parse_iso_time_remaining(parsed["claude_5h_reset"], is_fa=True)
lines.extend([
f" ⏳ سهمیه ۵ ساعته:",
f" {bar_c5} {c_5h}% باقیمانده",
f" • 🔄 زمان تمدید: {reset_c5}",
])
if c_wk is not None:
bar_cwk = make_quota_bar(c_wk)
reset_cwk = parse_iso_time_remaining(parsed["claude_weekly_reset"], is_fa=True)
lines.extend([
f" 📅 سهمیه هفتگی:",
f" {bar_cwk} {c_wk}% باقیمانده",
f" • 🔄 زمان تمدید: {reset_cwk}",
])
lines.extend([
"",
f"🪙 اعتبار مدلها (Model Credits): {credits_val}",
"",
"💡 سهمیههای مصرف به صورت خودکار در بازههای زمانی فوق بازنشانی میشوند."
])
else:
lines = [
"📈 Real-Time AGY Quota & Usage Report",
"",
"✨ 1. Gemini Models:",
]
if g_5h is not None:
bar_5h = make_quota_bar(g_5h)
reset_5h = parse_iso_time_remaining(parsed["gemini_5h_reset"], is_fa=False)
lines.extend([
f" ⏳ 5-Hour Limit:",
f" {bar_5h} {g_5h}% Remaining",
f" • 🔄 Reset: {reset_5h}",
])
if g_wk is not None:
bar_wk = make_quota_bar(g_wk)
reset_wk = parse_iso_time_remaining(parsed["gemini_weekly_reset"], is_fa=False)
lines.extend([
f" 📅 Weekly Limit:",
f" {bar_wk} {g_wk}% Remaining",
f" • 🔄 Reset: {reset_wk}",
])
lines.extend([
"",
"🌟 2. Claude & GPT Models:",
])
if c_5h is not None:
bar_c5 = make_quota_bar(c_5h)
reset_c5 = parse_iso_time_remaining(parsed["claude_5h_reset"], is_fa=False)
lines.extend([
f" ⏳ 5-Hour Limit:",
f" {bar_c5} {c_5h}% Remaining",
f" • 🔄 Reset: {reset_c5}",
])
if c_wk is not None:
bar_cwk = make_quota_bar(c_wk)
reset_cwk = parse_iso_time_remaining(parsed["claude_weekly_reset"], is_fa=False)
lines.extend([
f" 📅 Weekly Limit:",
f" {bar_cwk} {c_wk}% Remaining",
f" • 🔄 Reset: {reset_cwk}",
])
lines.extend([
"",
f"🪙 Model Credits: {credits_val}",
"",
"💡 Quotas automatically reset periodically in the windows shown above."
])
return "\n".join(lines)