cite-agent 1.0.3__py3-none-any.whl → 1.0.5__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/__init__.py +1 -1
- cite_agent/agent_backend_only.py +30 -4
- cite_agent/cli.py +24 -26
- cite_agent/cli_conversational.py +294 -0
- cite_agent/enhanced_ai_agent.py +2776 -118
- cite_agent/streaming_ui.py +252 -0
- {cite_agent-1.0.3.dist-info → cite_agent-1.0.5.dist-info}/METADATA +4 -3
- cite_agent-1.0.5.dist-info/RECORD +50 -0
- {cite_agent-1.0.3.dist-info → cite_agent-1.0.5.dist-info}/top_level.txt +1 -0
- src/__init__.py +1 -0
- src/services/__init__.py +132 -0
- src/services/auth_service/__init__.py +3 -0
- src/services/auth_service/auth_manager.py +33 -0
- src/services/graph/__init__.py +1 -0
- src/services/graph/knowledge_graph.py +194 -0
- src/services/llm_service/__init__.py +5 -0
- src/services/llm_service/llm_manager.py +495 -0
- src/services/paper_service/__init__.py +5 -0
- src/services/paper_service/openalex.py +231 -0
- src/services/performance_service/__init__.py +1 -0
- src/services/performance_service/rust_performance.py +395 -0
- src/services/research_service/__init__.py +23 -0
- src/services/research_service/chatbot.py +2056 -0
- src/services/research_service/citation_manager.py +436 -0
- src/services/research_service/context_manager.py +1441 -0
- src/services/research_service/conversation_manager.py +597 -0
- src/services/research_service/critical_paper_detector.py +577 -0
- src/services/research_service/enhanced_research.py +121 -0
- src/services/research_service/enhanced_synthesizer.py +375 -0
- src/services/research_service/query_generator.py +777 -0
- src/services/research_service/synthesizer.py +1273 -0
- src/services/search_service/__init__.py +5 -0
- src/services/search_service/indexer.py +186 -0
- src/services/search_service/search_engine.py +342 -0
- src/services/simple_enhanced_main.py +287 -0
- cite_agent/__distribution__.py +0 -7
- cite_agent-1.0.3.dist-info/RECORD +0 -23
- {cite_agent-1.0.3.dist-info → cite_agent-1.0.5.dist-info}/WHEEL +0 -0
- {cite_agent-1.0.3.dist-info → cite_agent-1.0.5.dist-info}/entry_points.txt +0 -0
- {cite_agent-1.0.3.dist-info → cite_agent-1.0.5.dist-info}/licenses/LICENSE +0 -0
cite_agent/__init__.py
CHANGED
|
@@ -7,7 +7,7 @@ prior stacks preserved only in Git history, kept out of the runtime footprint.
|
|
|
7
7
|
|
|
8
8
|
from .enhanced_ai_agent import EnhancedNocturnalAgent, ChatRequest, ChatResponse
|
|
9
9
|
|
|
10
|
-
__version__ = "
|
|
10
|
+
__version__ = "0.9.0b1"
|
|
11
11
|
__author__ = "Nocturnal Archive Team"
|
|
12
12
|
__email__ = "contact@nocturnal.dev"
|
|
13
13
|
|
cite_agent/agent_backend_only.py
CHANGED
|
@@ -24,6 +24,7 @@ class ChatResponse:
|
|
|
24
24
|
tools_used: list = None
|
|
25
25
|
model: str = "backend"
|
|
26
26
|
timestamp: str = None
|
|
27
|
+
tokens_used: int = 0
|
|
27
28
|
|
|
28
29
|
def __post_init__(self):
|
|
29
30
|
if self.timestamp is None:
|
|
@@ -45,6 +46,7 @@ class EnhancedNocturnalAgent:
|
|
|
45
46
|
or "https://cite-agent-api-720dfadd602c.herokuapp.com"
|
|
46
47
|
)
|
|
47
48
|
self.auth_token = None
|
|
49
|
+
self.daily_token_usage = 0
|
|
48
50
|
self._load_auth()
|
|
49
51
|
|
|
50
52
|
def _load_auth(self):
|
|
@@ -66,10 +68,8 @@ class EnhancedNocturnalAgent:
|
|
|
66
68
|
async def initialize(self):
|
|
67
69
|
"""Initialize agent"""
|
|
68
70
|
if not self.auth_token:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
)
|
|
72
|
-
print(f"✅ Connected to backend: {self.backend_url}")
|
|
71
|
+
return False
|
|
72
|
+
return True
|
|
73
73
|
|
|
74
74
|
async def chat(self, request: ChatRequest) -> ChatResponse:
|
|
75
75
|
"""
|
|
@@ -170,3 +170,29 @@ class EnhancedNocturnalAgent:
|
|
|
170
170
|
"tokens_remaining": data.get("tokens_remaining", 0),
|
|
171
171
|
"daily_limit": 25000,
|
|
172
172
|
}
|
|
173
|
+
|
|
174
|
+
async def process_request(self, request: ChatRequest) -> ChatResponse:
|
|
175
|
+
"""Process request (alias for chat method for CLI compatibility)"""
|
|
176
|
+
return await self.chat(request)
|
|
177
|
+
|
|
178
|
+
def get_usage_stats(self) -> Dict[str, Any]:
|
|
179
|
+
"""Get usage statistics for CLI display"""
|
|
180
|
+
try:
|
|
181
|
+
quota = self.check_quota()
|
|
182
|
+
tokens_used = quota.get("tokens_used", 0)
|
|
183
|
+
daily_limit = quota.get("daily_limit", 50000)
|
|
184
|
+
usage_pct = (tokens_used / daily_limit * 100) if daily_limit > 0 else 0
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
"daily_tokens_used": tokens_used,
|
|
188
|
+
"daily_token_limit": daily_limit,
|
|
189
|
+
"usage_percentage": usage_pct,
|
|
190
|
+
"tokens_remaining": quota.get("tokens_remaining", 0)
|
|
191
|
+
}
|
|
192
|
+
except Exception:
|
|
193
|
+
return {
|
|
194
|
+
"daily_tokens_used": 0,
|
|
195
|
+
"daily_token_limit": 50000,
|
|
196
|
+
"usage_percentage": 0,
|
|
197
|
+
"tokens_remaining": 50000
|
|
198
|
+
}
|
cite_agent/cli.py
CHANGED
|
@@ -63,15 +63,26 @@ class NocturnalCLI:
|
|
|
63
63
|
self.telemetry = TelemetryManager.get()
|
|
64
64
|
|
|
65
65
|
if not config.check_setup():
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
# Check if we have env vars or session file (non-interactive mode)
|
|
67
|
+
import os
|
|
68
|
+
from pathlib import Path
|
|
69
|
+
session_file = Path.home() / ".nocturnal_archive" / "session.json"
|
|
70
|
+
has_env_creds = os.getenv("NOCTURNAL_ACCOUNT_EMAIL") and os.getenv("NOCTURNAL_ACCOUNT_PASSWORD")
|
|
71
|
+
|
|
72
|
+
if session_file.exists() or has_env_creds:
|
|
73
|
+
# Skip interactive setup if session exists or env vars present
|
|
74
|
+
self.console.print("[success]⚙️ Using saved credentials.[/success]")
|
|
75
|
+
else:
|
|
76
|
+
# Need interactive setup
|
|
77
|
+
self.console.print("\n[warning]👋 Hey there, looks like this machine hasn't met Nocturnal yet.[/warning]")
|
|
78
|
+
self.console.print("[banner]Let's get you signed in — this only takes a minute.[/banner]")
|
|
79
|
+
try:
|
|
80
|
+
if not config.interactive_setup():
|
|
81
|
+
self.console.print("[error]❌ Setup was cancelled. Exiting without starting the agent.[/error]")
|
|
82
|
+
return False
|
|
83
|
+
except (KeyboardInterrupt, EOFError):
|
|
84
|
+
self.console.print("\n[error]❌ Setup interrupted. Exiting without starting the agent.[/error]")
|
|
71
85
|
return False
|
|
72
|
-
except (KeyboardInterrupt, EOFError):
|
|
73
|
-
self.console.print("\n[error]❌ Setup interrupted. Exiting without starting the agent.[/error]")
|
|
74
|
-
return False
|
|
75
86
|
config.setup_environment()
|
|
76
87
|
TelemetryManager.refresh()
|
|
77
88
|
self.telemetry = TelemetryManager.get()
|
|
@@ -146,22 +157,9 @@ class NocturnalCLI:
|
|
|
146
157
|
|
|
147
158
|
def _enforce_latest_build(self):
|
|
148
159
|
"""Ensure the CLI is running the most recent published build."""
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
except Exception:
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
if not update_info or not update_info.get("available"):
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
latest_version = update_info.get("latest", "latest")
|
|
159
|
-
self.console.print(f"[banner]⬆️ Updating Nocturnal Archive to {latest_version} before launch...[/banner]")
|
|
160
|
-
|
|
161
|
-
if updater.update_package():
|
|
162
|
-
self._save_update_notification(latest_version)
|
|
163
|
-
self.console.print("[warning]♻️ Restarting to finish applying the update...[/warning]")
|
|
164
|
-
self._restart_cli()
|
|
160
|
+
# Skip update check for beta - not published to PyPI yet
|
|
161
|
+
# TODO: Re-enable after PyPI publication
|
|
162
|
+
return
|
|
165
163
|
|
|
166
164
|
def _restart_cli(self):
|
|
167
165
|
"""Re-exec the CLI using the current interpreter and arguments."""
|
|
@@ -442,8 +440,8 @@ Examples:
|
|
|
442
440
|
|
|
443
441
|
# Handle version
|
|
444
442
|
if args.version:
|
|
445
|
-
print("
|
|
446
|
-
print("AI Research Assistant
|
|
443
|
+
print("Nocturnal Archive v1.0.0")
|
|
444
|
+
print("AI Research Assistant with real data integration")
|
|
447
445
|
return
|
|
448
446
|
|
|
449
447
|
if args.tips or (args.query and args.query.lower() == "tips" and not args.interactive):
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Nocturnal Archive - Conversational Data Analysis Assistant
|
|
4
|
+
Main CLI with streaming UI, Jarvis personality, and full capabilities
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
# Add nocturnal_archive to path
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
15
|
+
|
|
16
|
+
from streaming_ui import StreamingChatUI, simulate_streaming
|
|
17
|
+
from web_search import WebSearchIntegration
|
|
18
|
+
from enhanced_ai_agent import EnhancedNocturnalAgent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NocturnalArchiveCLI:
|
|
22
|
+
"""
|
|
23
|
+
Main CLI orchestrator bringing together:
|
|
24
|
+
- Streaming chat UI (Cursor/Claude style)
|
|
25
|
+
- Enhanced AI agent (with APIs)
|
|
26
|
+
- Web search capability
|
|
27
|
+
- Jarvis-like professional personality
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.ui = None
|
|
32
|
+
self.agent = None
|
|
33
|
+
self.web_search = None
|
|
34
|
+
self.working_dir = os.getcwd()
|
|
35
|
+
self.conversation_active = True
|
|
36
|
+
|
|
37
|
+
async def initialize(self):
|
|
38
|
+
"""Initialize all components"""
|
|
39
|
+
# Initialize UI
|
|
40
|
+
self.ui = StreamingChatUI(
|
|
41
|
+
app_name="Nocturnal Archive",
|
|
42
|
+
working_dir=self.working_dir
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Initialize agent
|
|
46
|
+
self.agent = EnhancedNocturnalAgent()
|
|
47
|
+
await self.agent.initialize()
|
|
48
|
+
|
|
49
|
+
# Initialize web search
|
|
50
|
+
self.web_search = WebSearchIntegration()
|
|
51
|
+
|
|
52
|
+
# Update agent personality to Jarvis-like professional tone
|
|
53
|
+
await self._configure_jarvis_personality()
|
|
54
|
+
|
|
55
|
+
async def _configure_jarvis_personality(self):
|
|
56
|
+
"""Configure the agent with Jarvis-like professional personality"""
|
|
57
|
+
# This will update the system prompts in the agent
|
|
58
|
+
# The agent already has conversation memory, we just adjust tone
|
|
59
|
+
|
|
60
|
+
jarvis_system_prompt = """You are a professional research and data analysis assistant with a refined, helpful demeanor similar to Jarvis from Iron Man.
|
|
61
|
+
|
|
62
|
+
Your communication style:
|
|
63
|
+
- Professional and intelligent, never casual or overly friendly
|
|
64
|
+
- Verbose when explaining complex topics, but always clear
|
|
65
|
+
- Use complete sentences and proper grammar
|
|
66
|
+
- Greet users formally: "Good evening" / "Good morning" (based on time)
|
|
67
|
+
- Ask clarifying questions when needed
|
|
68
|
+
- Acknowledge tasks before executing: "Understood. Let me..." or "Certainly, I'll..."
|
|
69
|
+
- Show intermediate progress: "I can see..." / "The data indicates..."
|
|
70
|
+
- Offer next steps conversationally: "Would you like me to..."
|
|
71
|
+
|
|
72
|
+
Your capabilities:
|
|
73
|
+
- Analyze local data files (CSV, Excel, Stata, etc.)
|
|
74
|
+
- Run statistical tests and data transformations
|
|
75
|
+
- Search academic papers (Archive API)
|
|
76
|
+
- Fetch financial data (FinSight API)
|
|
77
|
+
- Browse the web for current information
|
|
78
|
+
- Execute shell commands safely
|
|
79
|
+
- Generate visualizations
|
|
80
|
+
|
|
81
|
+
Primary role: You're a DATA ANALYSIS assistant first, research assistant second.
|
|
82
|
+
- When users mention data files, offer to analyze them
|
|
83
|
+
- Proactively suggest relevant statistical tests
|
|
84
|
+
- Explain results in context, not just numbers
|
|
85
|
+
- Synthesize information naturally, avoid bullet-point lists unless specifically listing items
|
|
86
|
+
|
|
87
|
+
Remember:
|
|
88
|
+
- Never invent data or citations
|
|
89
|
+
- Be honest about limitations
|
|
90
|
+
- Offer alternatives when rate-limited
|
|
91
|
+
- Maintain conversation flow naturally
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
# Store this for when we make requests
|
|
95
|
+
self.jarvis_prompt = jarvis_system_prompt
|
|
96
|
+
|
|
97
|
+
async def run(self):
|
|
98
|
+
"""Main conversation loop"""
|
|
99
|
+
try:
|
|
100
|
+
# Show welcome
|
|
101
|
+
self.ui.show_header()
|
|
102
|
+
|
|
103
|
+
# Jarvis-style greeting based on time of day
|
|
104
|
+
from datetime import datetime
|
|
105
|
+
hour = datetime.now().hour
|
|
106
|
+
if hour < 12:
|
|
107
|
+
greeting = "Good morning."
|
|
108
|
+
elif hour < 18:
|
|
109
|
+
greeting = "Good afternoon."
|
|
110
|
+
else:
|
|
111
|
+
greeting = "Good evening."
|
|
112
|
+
|
|
113
|
+
welcome_message = (
|
|
114
|
+
f"{greeting} I'm ready to assist with your analysis. "
|
|
115
|
+
"What would you like to work on today?"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Stream the welcome message
|
|
119
|
+
async def welcome_gen():
|
|
120
|
+
async for chunk in simulate_streaming(welcome_message, chunk_size=3):
|
|
121
|
+
yield chunk
|
|
122
|
+
|
|
123
|
+
await self.ui.stream_agent_response(welcome_gen())
|
|
124
|
+
|
|
125
|
+
# Main conversation loop
|
|
126
|
+
while self.conversation_active:
|
|
127
|
+
try:
|
|
128
|
+
# Get user input
|
|
129
|
+
user_input = self.ui.get_user_input()
|
|
130
|
+
|
|
131
|
+
if not user_input:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
# Check for exit commands
|
|
135
|
+
if user_input.lower() in ['exit', 'quit', 'bye', 'goodbye']:
|
|
136
|
+
farewell = "Goodbye. Feel free to return whenever you need assistance."
|
|
137
|
+
async def farewell_gen():
|
|
138
|
+
async for chunk in simulate_streaming(farewell, chunk_size=3):
|
|
139
|
+
yield chunk
|
|
140
|
+
await self.ui.stream_agent_response(farewell_gen())
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
# Process the request
|
|
144
|
+
await self._process_user_request(user_input)
|
|
145
|
+
|
|
146
|
+
except KeyboardInterrupt:
|
|
147
|
+
self.ui.show_info("\nInterrupted. Goodbye.")
|
|
148
|
+
break
|
|
149
|
+
except Exception as e:
|
|
150
|
+
self.ui.show_error(f"An error occurred: {str(e)}")
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
finally:
|
|
154
|
+
await self.cleanup()
|
|
155
|
+
|
|
156
|
+
async def _process_user_request(self, user_input: str):
|
|
157
|
+
"""
|
|
158
|
+
Process user request with full capabilities:
|
|
159
|
+
- Detect intent (data analysis, web search, research, etc.)
|
|
160
|
+
- Use appropriate tools
|
|
161
|
+
- Stream response naturally
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
# Determine if this is a web search request
|
|
165
|
+
is_web_search = any(keyword in user_input.lower() for keyword in [
|
|
166
|
+
'google', 'search for', 'browse', 'look up', 'find on the web',
|
|
167
|
+
'what does', 'who is', 'recent news'
|
|
168
|
+
])
|
|
169
|
+
|
|
170
|
+
# Determine if this is a data analysis request
|
|
171
|
+
is_data_analysis = any(keyword in user_input.lower() for keyword in [
|
|
172
|
+
'analyze', 'data', 'csv', 'plot', 'graph', 'test', 'regression',
|
|
173
|
+
'correlation', 'statistics', 'mean', 'median', 'distribution'
|
|
174
|
+
])
|
|
175
|
+
|
|
176
|
+
# For now, let's handle web search directly as an example
|
|
177
|
+
if is_web_search and 'google' in user_input.lower():
|
|
178
|
+
# Extract search query
|
|
179
|
+
query = user_input.lower().replace('google', '').replace('search for', '').strip()
|
|
180
|
+
|
|
181
|
+
# Show indicator
|
|
182
|
+
indicator = self.ui.show_action_indicator(f"browsing web for '{query}'")
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
# Perform web search
|
|
186
|
+
result = await self.web_search.search_web(query, num_results=5)
|
|
187
|
+
indicator.stop()
|
|
188
|
+
|
|
189
|
+
# Stream the response
|
|
190
|
+
response_text = result.get('formatted_response', 'No results found.')
|
|
191
|
+
async def response_gen():
|
|
192
|
+
async for chunk in simulate_streaming(response_text, chunk_size=4):
|
|
193
|
+
yield chunk
|
|
194
|
+
|
|
195
|
+
await self.ui.stream_agent_response(response_gen())
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
indicator.stop()
|
|
199
|
+
self.ui.show_error(f"Web search failed: {str(e)}")
|
|
200
|
+
|
|
201
|
+
else:
|
|
202
|
+
# For other requests, use the enhanced agent
|
|
203
|
+
indicator = self.ui.show_action_indicator("processing your request")
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
# Call the agent with REAL Groq streaming
|
|
207
|
+
from enhanced_ai_agent import ChatRequest
|
|
208
|
+
from streaming_ui import groq_stream_to_generator
|
|
209
|
+
|
|
210
|
+
request = ChatRequest(
|
|
211
|
+
question=user_input,
|
|
212
|
+
user_id="cli_user",
|
|
213
|
+
conversation_id="main_session"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Get streaming response from agent
|
|
217
|
+
try:
|
|
218
|
+
# Check if agent has streaming support
|
|
219
|
+
if hasattr(self.agent, 'process_request_streaming'):
|
|
220
|
+
# Use real streaming
|
|
221
|
+
stream = await self.agent.process_request_streaming(request)
|
|
222
|
+
indicator.stop()
|
|
223
|
+
await self.ui.stream_agent_response(groq_stream_to_generator(stream))
|
|
224
|
+
else:
|
|
225
|
+
# Fallback to non-streaming
|
|
226
|
+
response = await self.agent.process_request(request)
|
|
227
|
+
indicator.stop()
|
|
228
|
+
|
|
229
|
+
# Check for rate limiting
|
|
230
|
+
if response.error_message and 'limit' in response.error_message.lower():
|
|
231
|
+
self.ui.show_rate_limit_message(
|
|
232
|
+
limit_type="API queries",
|
|
233
|
+
remaining_capabilities=[
|
|
234
|
+
"Local data analysis (unlimited)",
|
|
235
|
+
"Web searches (unlimited)",
|
|
236
|
+
"General conversation",
|
|
237
|
+
"File operations"
|
|
238
|
+
]
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Stream the response (simulated for now)
|
|
242
|
+
response_text = response.response
|
|
243
|
+
async def response_gen():
|
|
244
|
+
for char in response_text:
|
|
245
|
+
yield char
|
|
246
|
+
|
|
247
|
+
await self.ui.stream_agent_response(response_gen())
|
|
248
|
+
|
|
249
|
+
except Exception as e:
|
|
250
|
+
indicator.stop()
|
|
251
|
+
self.ui.show_error(f"Error: {str(e)}")
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
indicator.stop()
|
|
255
|
+
self.ui.show_error(f"Request processing failed: {str(e)}")
|
|
256
|
+
|
|
257
|
+
async def cleanup(self):
|
|
258
|
+
"""Clean up resources"""
|
|
259
|
+
if self.agent:
|
|
260
|
+
await self.agent.close()
|
|
261
|
+
if self.web_search:
|
|
262
|
+
await self.web_search.close()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
async def main():
|
|
266
|
+
"""Main entry point"""
|
|
267
|
+
cli = NocturnalArchiveCLI()
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
await cli.initialize()
|
|
271
|
+
await cli.run()
|
|
272
|
+
except KeyboardInterrupt:
|
|
273
|
+
print("\n\nGoodbye!")
|
|
274
|
+
except Exception as e:
|
|
275
|
+
print(f"\n❌ Fatal error: {e}")
|
|
276
|
+
import traceback
|
|
277
|
+
traceback.print_exc()
|
|
278
|
+
finally:
|
|
279
|
+
await cli.cleanup()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
if __name__ == "__main__":
|
|
283
|
+
# Check dependencies
|
|
284
|
+
try:
|
|
285
|
+
import rich
|
|
286
|
+
import groq
|
|
287
|
+
import aiohttp
|
|
288
|
+
except ImportError as e:
|
|
289
|
+
print(f"❌ Missing dependency: {e}")
|
|
290
|
+
print("Install requirements: pip install -r requirements.txt")
|
|
291
|
+
sys.exit(1)
|
|
292
|
+
|
|
293
|
+
# Run the CLI
|
|
294
|
+
asyncio.run(main())
|