cite-agent 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cite-agent might be problematic. Click here for more details.
- cite_agent/__distribution__.py +7 -0
- cite_agent/__init__.py +66 -0
- cite_agent/account_client.py +130 -0
- cite_agent/agent_backend_only.py +172 -0
- cite_agent/ascii_plotting.py +296 -0
- cite_agent/auth.py +281 -0
- cite_agent/backend_only_client.py +83 -0
- cite_agent/cli.py +512 -0
- cite_agent/cli_enhanced.py +207 -0
- cite_agent/dashboard.py +339 -0
- cite_agent/enhanced_ai_agent.py +172 -0
- cite_agent/rate_limiter.py +298 -0
- cite_agent/setup_config.py +417 -0
- cite_agent/telemetry.py +85 -0
- cite_agent/ui.py +175 -0
- cite_agent/updater.py +187 -0
- cite_agent/web_search.py +203 -0
- cite_agent-1.0.0.dist-info/METADATA +234 -0
- cite_agent-1.0.0.dist-info/RECORD +23 -0
- cite_agent-1.0.0.dist-info/WHEEL +5 -0
- cite_agent-1.0.0.dist-info/entry_points.txt +3 -0
- cite_agent-1.0.0.dist-info/licenses/LICENSE +21 -0
- cite_agent-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend-Only Agent (Distribution Version)
|
|
3
|
+
All LLM queries go through centralized backend API.
|
|
4
|
+
Local API keys are not supported.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import requests
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ChatRequest:
|
|
15
|
+
question: str
|
|
16
|
+
user_id: str = "default"
|
|
17
|
+
conversation_id: str = "default"
|
|
18
|
+
context: Dict[str, Any] = None
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ChatResponse:
|
|
22
|
+
response: str
|
|
23
|
+
citations: list = None
|
|
24
|
+
tools_used: list = None
|
|
25
|
+
model: str = "backend"
|
|
26
|
+
timestamp: str = None
|
|
27
|
+
|
|
28
|
+
def __post_init__(self):
|
|
29
|
+
if self.timestamp is None:
|
|
30
|
+
self.timestamp = datetime.now(timezone.utc).isoformat()
|
|
31
|
+
if self.citations is None:
|
|
32
|
+
self.citations = []
|
|
33
|
+
if self.tools_used is None:
|
|
34
|
+
self.tools_used = []
|
|
35
|
+
|
|
36
|
+
class EnhancedNocturnalAgent:
|
|
37
|
+
"""
|
|
38
|
+
Backend-only agent for distribution.
|
|
39
|
+
Proxies all requests to centralized API.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self):
|
|
43
|
+
self.backend_url = (
|
|
44
|
+
os.getenv("NOCTURNAL_CONTROL_PLANE_URL")
|
|
45
|
+
or "https://cite-agent-api-720dfadd602c.herokuapp.com"
|
|
46
|
+
)
|
|
47
|
+
self.auth_token = None
|
|
48
|
+
self._load_auth()
|
|
49
|
+
|
|
50
|
+
def _load_auth(self):
|
|
51
|
+
"""Load authentication token from config"""
|
|
52
|
+
# Try environment first
|
|
53
|
+
self.auth_token = os.getenv("NOCTURNAL_AUTH_TOKEN")
|
|
54
|
+
|
|
55
|
+
# Try config file
|
|
56
|
+
if not self.auth_token:
|
|
57
|
+
from pathlib import Path
|
|
58
|
+
config_file = Path.home() / ".nocturnal_archive" / "config.env"
|
|
59
|
+
if config_file.exists():
|
|
60
|
+
with open(config_file) as f:
|
|
61
|
+
for line in f:
|
|
62
|
+
if line.startswith("NOCTURNAL_AUTH_TOKEN="):
|
|
63
|
+
self.auth_token = line.split("=", 1)[1].strip()
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
async def initialize(self):
|
|
67
|
+
"""Initialize agent"""
|
|
68
|
+
if not self.auth_token:
|
|
69
|
+
raise RuntimeError(
|
|
70
|
+
"Not authenticated. Please run 'cite-agent --setup' first."
|
|
71
|
+
)
|
|
72
|
+
print(f"✅ Connected to backend: {self.backend_url}")
|
|
73
|
+
|
|
74
|
+
async def chat(self, request: ChatRequest) -> ChatResponse:
|
|
75
|
+
"""
|
|
76
|
+
Send chat request to backend API.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
request: Chat request with question and context
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Chat response with answer and citations
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
RuntimeError: If authentication fails or backend unavailable
|
|
86
|
+
"""
|
|
87
|
+
if not self.auth_token:
|
|
88
|
+
raise RuntimeError(
|
|
89
|
+
"Not authenticated. Run 'cite-agent --setup' first."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
response = requests.post(
|
|
94
|
+
f"{self.backend_url}/api/query",
|
|
95
|
+
headers={
|
|
96
|
+
"Authorization": f"Bearer {self.auth_token}",
|
|
97
|
+
"Content-Type": "application/json"
|
|
98
|
+
},
|
|
99
|
+
json={
|
|
100
|
+
"query": request.question,
|
|
101
|
+
"context": request.context or {},
|
|
102
|
+
"user_id": request.user_id,
|
|
103
|
+
"conversation_id": request.conversation_id,
|
|
104
|
+
},
|
|
105
|
+
timeout=60
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if response.status_code == 401:
|
|
109
|
+
raise RuntimeError(
|
|
110
|
+
"Authentication expired. Run 'cite-agent --setup' to log in again."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if response.status_code == 429:
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
"Daily quota exceeded (25,000 tokens). Resets tomorrow."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if response.status_code >= 400:
|
|
119
|
+
error_detail = response.json().get("detail", response.text)
|
|
120
|
+
raise RuntimeError(f"Backend error: {error_detail}")
|
|
121
|
+
|
|
122
|
+
data = response.json()
|
|
123
|
+
|
|
124
|
+
return ChatResponse(
|
|
125
|
+
response=data.get("response", data.get("answer", "")),
|
|
126
|
+
citations=data.get("citations", []),
|
|
127
|
+
tools_used=data.get("tools_used", []),
|
|
128
|
+
model=data.get("model", "backend"),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
except requests.RequestException as e:
|
|
132
|
+
raise RuntimeError(
|
|
133
|
+
f"Backend connection failed: {e}. Check your internet connection."
|
|
134
|
+
) from e
|
|
135
|
+
|
|
136
|
+
async def close(self):
|
|
137
|
+
"""Cleanup"""
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
def get_health_status(self) -> Dict[str, Any]:
|
|
141
|
+
"""Get backend health status"""
|
|
142
|
+
try:
|
|
143
|
+
response = requests.get(
|
|
144
|
+
f"{self.backend_url}/api/health/",
|
|
145
|
+
timeout=5
|
|
146
|
+
)
|
|
147
|
+
return response.json()
|
|
148
|
+
except:
|
|
149
|
+
return {"status": "unavailable"}
|
|
150
|
+
|
|
151
|
+
def check_quota(self) -> Dict[str, Any]:
|
|
152
|
+
"""Check remaining daily quota"""
|
|
153
|
+
if not self.auth_token:
|
|
154
|
+
raise RuntimeError("Not authenticated")
|
|
155
|
+
|
|
156
|
+
response = requests.get(
|
|
157
|
+
f"{self.backend_url}/api/auth/me",
|
|
158
|
+
headers={"Authorization": f"Bearer {self.auth_token}"},
|
|
159
|
+
timeout=10
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if response.status_code == 401:
|
|
163
|
+
raise RuntimeError("Authentication expired")
|
|
164
|
+
|
|
165
|
+
response.raise_for_status()
|
|
166
|
+
data = response.json()
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"tokens_used": data.get("tokens_used_today", 0),
|
|
170
|
+
"tokens_remaining": data.get("tokens_remaining", 0),
|
|
171
|
+
"daily_limit": 25000,
|
|
172
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rate Limiting Configuration based on Groq API limits
|
|
3
|
+
Implements per-user rate limiting with soft degradation
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Dict, Optional
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class RateLimitConfig:
|
|
15
|
+
"""Rate limit configuration for different tiers"""
|
|
16
|
+
# Groq limits for llama-3.3-70b-versatile (Free tier)
|
|
17
|
+
rpm: int # Requests per minute
|
|
18
|
+
rpd: int # Requests per day
|
|
19
|
+
tpm: int # Tokens per minute
|
|
20
|
+
tpd: int # Tokens per day
|
|
21
|
+
|
|
22
|
+
# Our API limits (on top of Groq)
|
|
23
|
+
archive_api_per_day: int
|
|
24
|
+
finsight_api_per_day: int
|
|
25
|
+
web_search_per_day: int # -1 = unlimited
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Rate limit tiers
|
|
29
|
+
RATE_LIMITS = {
|
|
30
|
+
'free': RateLimitConfig(
|
|
31
|
+
# Groq limits (llama-3.3-70b-versatile)
|
|
32
|
+
rpm=30,
|
|
33
|
+
rpd=1000,
|
|
34
|
+
tpm=12000,
|
|
35
|
+
tpd=100000,
|
|
36
|
+
# Our limits
|
|
37
|
+
archive_api_per_day=10,
|
|
38
|
+
finsight_api_per_day=20,
|
|
39
|
+
web_search_per_day=-1 # unlimited
|
|
40
|
+
),
|
|
41
|
+
'basic': RateLimitConfig(
|
|
42
|
+
# Groq limits (same, user pays for our value-add)
|
|
43
|
+
rpm=30,
|
|
44
|
+
rpd=1000,
|
|
45
|
+
tpm=12000,
|
|
46
|
+
tpd=100000,
|
|
47
|
+
# Our limits (300 NTD/month)
|
|
48
|
+
archive_api_per_day=25,
|
|
49
|
+
finsight_api_per_day=50,
|
|
50
|
+
web_search_per_day=-1 # unlimited
|
|
51
|
+
),
|
|
52
|
+
'pro': RateLimitConfig(
|
|
53
|
+
# Groq limits (same)
|
|
54
|
+
rpm=30,
|
|
55
|
+
rpd=1000,
|
|
56
|
+
tpm=12000,
|
|
57
|
+
tpd=100000,
|
|
58
|
+
# Our limits (600 NTD/month)
|
|
59
|
+
archive_api_per_day=-1, # unlimited
|
|
60
|
+
finsight_api_per_day=-1, # unlimited
|
|
61
|
+
web_search_per_day=-1 # unlimited
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class RateLimiter:
|
|
67
|
+
"""
|
|
68
|
+
Track and enforce rate limits per user
|
|
69
|
+
Implements soft degradation when limits are hit
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, user_id: str, tier: str = 'basic', storage_dir: Optional[Path] = None):
|
|
73
|
+
self.user_id = user_id
|
|
74
|
+
self.tier = tier
|
|
75
|
+
self.config = RATE_LIMITS.get(tier, RATE_LIMITS['basic'])
|
|
76
|
+
|
|
77
|
+
# Storage for rate limit tracking
|
|
78
|
+
self.storage_dir = storage_dir or Path.home() / ".nocturnal_archive" / "rate_limits"
|
|
79
|
+
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
self.storage_file = self.storage_dir / f"{user_id}_limits.json"
|
|
81
|
+
|
|
82
|
+
# Load existing limits
|
|
83
|
+
self.limits = self._load_limits()
|
|
84
|
+
|
|
85
|
+
def _load_limits(self) -> Dict:
|
|
86
|
+
"""Load rate limit data from storage"""
|
|
87
|
+
if self.storage_file.exists():
|
|
88
|
+
try:
|
|
89
|
+
with open(self.storage_file, 'r') as f:
|
|
90
|
+
data = json.load(f)
|
|
91
|
+
# Check if data is from today
|
|
92
|
+
if data.get('date') == datetime.now().strftime('%Y-%m-%d'):
|
|
93
|
+
return data
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
# Return fresh limits
|
|
98
|
+
return {
|
|
99
|
+
'date': datetime.now().strftime('%Y-%m-%d'),
|
|
100
|
+
'groq_requests': 0,
|
|
101
|
+
'groq_tokens': 0,
|
|
102
|
+
'archive_api': 0,
|
|
103
|
+
'finsight_api': 0,
|
|
104
|
+
'web_search': 0,
|
|
105
|
+
'last_request_time': None
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
def _save_limits(self):
|
|
109
|
+
"""Save rate limit data to storage"""
|
|
110
|
+
try:
|
|
111
|
+
with open(self.storage_file, 'w') as f:
|
|
112
|
+
json.dump(self.limits, f, indent=2)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
print(f"Warning: Could not save rate limits: {e}")
|
|
115
|
+
|
|
116
|
+
def _reset_if_needed(self):
|
|
117
|
+
"""Reset limits if it's a new day"""
|
|
118
|
+
current_date = datetime.now().strftime('%Y-%m-%d')
|
|
119
|
+
if self.limits.get('date') != current_date:
|
|
120
|
+
self.limits = {
|
|
121
|
+
'date': current_date,
|
|
122
|
+
'groq_requests': 0,
|
|
123
|
+
'groq_tokens': 0,
|
|
124
|
+
'archive_api': 0,
|
|
125
|
+
'finsight_api': 0,
|
|
126
|
+
'web_search': 0,
|
|
127
|
+
'last_request_time': None
|
|
128
|
+
}
|
|
129
|
+
self._save_limits()
|
|
130
|
+
|
|
131
|
+
def can_make_request(self, api_name: str = 'groq', tokens: int = 0) -> tuple[bool, Optional[str]]:
|
|
132
|
+
"""
|
|
133
|
+
Check if user can make a request
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
(can_proceed, error_message)
|
|
137
|
+
"""
|
|
138
|
+
self._reset_if_needed()
|
|
139
|
+
|
|
140
|
+
if api_name == 'groq':
|
|
141
|
+
# Check Groq limits
|
|
142
|
+
if self.limits['groq_requests'] >= self.config.rpd:
|
|
143
|
+
return False, f"Daily Groq request limit reached ({self.config.rpd} requests/day)"
|
|
144
|
+
|
|
145
|
+
if self.limits['groq_tokens'] + tokens > self.config.tpd:
|
|
146
|
+
return False, f"Daily Groq token limit reached ({self.config.tpd} tokens/day)"
|
|
147
|
+
|
|
148
|
+
# Check RPM (requests per minute)
|
|
149
|
+
if self.limits.get('last_request_time'):
|
|
150
|
+
last_time = datetime.fromisoformat(self.limits['last_request_time'])
|
|
151
|
+
if datetime.now() - last_time < timedelta(seconds=2): # Simple RPM approximation
|
|
152
|
+
return False, "Rate limit: Please wait a moment before making another request"
|
|
153
|
+
|
|
154
|
+
elif api_name == 'archive_api':
|
|
155
|
+
if self.config.archive_api_per_day == -1:
|
|
156
|
+
return True, None # Unlimited
|
|
157
|
+
if self.limits['archive_api'] >= self.config.archive_api_per_day:
|
|
158
|
+
return False, f"Daily Archive API limit reached ({self.config.archive_api_per_day} queries/day)"
|
|
159
|
+
|
|
160
|
+
elif api_name == 'finsight_api':
|
|
161
|
+
if self.config.finsight_api_per_day == -1:
|
|
162
|
+
return True, None # Unlimited
|
|
163
|
+
if self.limits['finsight_api'] >= self.config.finsight_api_per_day:
|
|
164
|
+
return False, f"Daily FinSight API limit reached ({self.config.finsight_api_per_day} queries/day)"
|
|
165
|
+
|
|
166
|
+
elif api_name == 'web_search':
|
|
167
|
+
if self.config.web_search_per_day == -1:
|
|
168
|
+
return True, None # Unlimited
|
|
169
|
+
if self.limits['web_search'] >= self.config.web_search_per_day:
|
|
170
|
+
return False, f"Daily web search limit reached"
|
|
171
|
+
|
|
172
|
+
return True, None
|
|
173
|
+
|
|
174
|
+
def record_request(self, api_name: str = 'groq', tokens: int = 0):
|
|
175
|
+
"""Record a request"""
|
|
176
|
+
self._reset_if_needed()
|
|
177
|
+
|
|
178
|
+
if api_name == 'groq':
|
|
179
|
+
self.limits['groq_requests'] += 1
|
|
180
|
+
self.limits['groq_tokens'] += tokens
|
|
181
|
+
self.limits['last_request_time'] = datetime.now().isoformat()
|
|
182
|
+
elif api_name == 'archive_api':
|
|
183
|
+
self.limits['archive_api'] += 1
|
|
184
|
+
elif api_name == 'finsight_api':
|
|
185
|
+
self.limits['finsight_api'] += 1
|
|
186
|
+
elif api_name == 'web_search':
|
|
187
|
+
self.limits['web_search'] += 1
|
|
188
|
+
|
|
189
|
+
self._save_limits()
|
|
190
|
+
|
|
191
|
+
def get_remaining(self, api_name: str = 'groq') -> int:
|
|
192
|
+
"""Get remaining requests for an API"""
|
|
193
|
+
self._reset_if_needed()
|
|
194
|
+
|
|
195
|
+
if api_name == 'groq':
|
|
196
|
+
return max(0, self.config.rpd - self.limits['groq_requests'])
|
|
197
|
+
elif api_name == 'archive_api':
|
|
198
|
+
if self.config.archive_api_per_day == -1:
|
|
199
|
+
return -1 # Unlimited
|
|
200
|
+
return max(0, self.config.archive_api_per_day - self.limits['archive_api'])
|
|
201
|
+
elif api_name == 'finsight_api':
|
|
202
|
+
if self.config.finsight_api_per_day == -1:
|
|
203
|
+
return -1 # Unlimited
|
|
204
|
+
return max(0, self.config.finsight_api_per_day - self.limits['finsight_api'])
|
|
205
|
+
elif api_name == 'web_search':
|
|
206
|
+
return -1 # Unlimited for now
|
|
207
|
+
|
|
208
|
+
return 0
|
|
209
|
+
|
|
210
|
+
def get_status_message(self) -> str:
|
|
211
|
+
"""Get human-readable status of limits"""
|
|
212
|
+
self._reset_if_needed()
|
|
213
|
+
|
|
214
|
+
lines = []
|
|
215
|
+
lines.append(f"**Rate Limit Status** (Tier: {self.tier.upper()})")
|
|
216
|
+
lines.append("")
|
|
217
|
+
|
|
218
|
+
# Groq limits
|
|
219
|
+
groq_remaining = self.get_remaining('groq')
|
|
220
|
+
lines.append(f"• Groq Requests: {self.limits['groq_requests']}/{self.config.rpd} used ({groq_remaining} remaining)")
|
|
221
|
+
lines.append(f"• Groq Tokens: {self.limits['groq_tokens']}/{self.config.tpd} used")
|
|
222
|
+
lines.append("")
|
|
223
|
+
|
|
224
|
+
# API limits
|
|
225
|
+
archive_remaining = self.get_remaining('archive_api')
|
|
226
|
+
if self.config.archive_api_per_day == -1:
|
|
227
|
+
lines.append("• Archive API: Unlimited ✓")
|
|
228
|
+
else:
|
|
229
|
+
lines.append(f"• Archive API: {self.limits['archive_api']}/{self.config.archive_api_per_day} used ({archive_remaining} remaining)")
|
|
230
|
+
|
|
231
|
+
finsight_remaining = self.get_remaining('finsight_api')
|
|
232
|
+
if self.config.finsight_api_per_day == -1:
|
|
233
|
+
lines.append("• FinSight API: Unlimited ✓")
|
|
234
|
+
else:
|
|
235
|
+
lines.append(f"• FinSight API: {self.limits['finsight_api']}/{self.config.finsight_api_per_day} used ({finsight_remaining} remaining)")
|
|
236
|
+
|
|
237
|
+
lines.append("• Web Search: Unlimited ✓")
|
|
238
|
+
|
|
239
|
+
return "\n".join(lines)
|
|
240
|
+
|
|
241
|
+
def get_available_capabilities(self) -> list[str]:
|
|
242
|
+
"""Get list of what's still available when rate limited"""
|
|
243
|
+
capabilities = []
|
|
244
|
+
|
|
245
|
+
if self.can_make_request('web_search')[0]:
|
|
246
|
+
capabilities.append("Web searches (unlimited)")
|
|
247
|
+
|
|
248
|
+
if self.can_make_request('archive_api')[0]:
|
|
249
|
+
archive_remaining = self.get_remaining('archive_api')
|
|
250
|
+
if archive_remaining == -1:
|
|
251
|
+
capabilities.append("Academic paper search (unlimited)")
|
|
252
|
+
else:
|
|
253
|
+
capabilities.append(f"Academic paper search ({archive_remaining} remaining today)")
|
|
254
|
+
|
|
255
|
+
if self.can_make_request('finsight_api')[0]:
|
|
256
|
+
finsight_remaining = self.get_remaining('finsight_api')
|
|
257
|
+
if finsight_remaining == -1:
|
|
258
|
+
capabilities.append("Financial data queries (unlimited)")
|
|
259
|
+
else:
|
|
260
|
+
capabilities.append(f"Financial data queries ({finsight_remaining} remaining today)")
|
|
261
|
+
|
|
262
|
+
# Always available
|
|
263
|
+
capabilities.append("Local data analysis (unlimited)")
|
|
264
|
+
capabilities.append("File operations and conversation")
|
|
265
|
+
|
|
266
|
+
return capabilities
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# Quick test function
|
|
270
|
+
def test_rate_limiter():
|
|
271
|
+
"""Test the rate limiter"""
|
|
272
|
+
limiter = RateLimiter("test_user", "basic")
|
|
273
|
+
|
|
274
|
+
print("Testing Rate Limiter")
|
|
275
|
+
print("=" * 70)
|
|
276
|
+
print(limiter.get_status_message())
|
|
277
|
+
print("\n")
|
|
278
|
+
|
|
279
|
+
# Test making requests
|
|
280
|
+
print("Making 5 Groq requests...")
|
|
281
|
+
for i in range(5):
|
|
282
|
+
can_proceed, error = limiter.can_make_request('groq', tokens=100)
|
|
283
|
+
if can_proceed:
|
|
284
|
+
limiter.record_request('groq', tokens=100)
|
|
285
|
+
print(f" Request {i+1}: ✓")
|
|
286
|
+
else:
|
|
287
|
+
print(f" Request {i+1}: ✗ {error}")
|
|
288
|
+
|
|
289
|
+
print("\n")
|
|
290
|
+
print(limiter.get_status_message())
|
|
291
|
+
|
|
292
|
+
print("\n\nAvailable capabilities:")
|
|
293
|
+
for cap in limiter.get_available_capabilities():
|
|
294
|
+
print(f" • {cap}")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == "__main__":
|
|
298
|
+
test_rate_limiter()
|