cite-agent 1.0.4__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.

Files changed (42) hide show
  1. cite_agent/__init__.py +1 -1
  2. cite_agent/account_client.py +19 -46
  3. cite_agent/agent_backend_only.py +30 -4
  4. cite_agent/cli.py +24 -26
  5. cite_agent/cli_conversational.py +294 -0
  6. cite_agent/enhanced_ai_agent.py +2776 -118
  7. cite_agent/setup_config.py +5 -21
  8. cite_agent/streaming_ui.py +252 -0
  9. {cite_agent-1.0.4.dist-info → cite_agent-1.0.5.dist-info}/METADATA +4 -3
  10. cite_agent-1.0.5.dist-info/RECORD +50 -0
  11. {cite_agent-1.0.4.dist-info → cite_agent-1.0.5.dist-info}/top_level.txt +1 -0
  12. src/__init__.py +1 -0
  13. src/services/__init__.py +132 -0
  14. src/services/auth_service/__init__.py +3 -0
  15. src/services/auth_service/auth_manager.py +33 -0
  16. src/services/graph/__init__.py +1 -0
  17. src/services/graph/knowledge_graph.py +194 -0
  18. src/services/llm_service/__init__.py +5 -0
  19. src/services/llm_service/llm_manager.py +495 -0
  20. src/services/paper_service/__init__.py +5 -0
  21. src/services/paper_service/openalex.py +231 -0
  22. src/services/performance_service/__init__.py +1 -0
  23. src/services/performance_service/rust_performance.py +395 -0
  24. src/services/research_service/__init__.py +23 -0
  25. src/services/research_service/chatbot.py +2056 -0
  26. src/services/research_service/citation_manager.py +436 -0
  27. src/services/research_service/context_manager.py +1441 -0
  28. src/services/research_service/conversation_manager.py +597 -0
  29. src/services/research_service/critical_paper_detector.py +577 -0
  30. src/services/research_service/enhanced_research.py +121 -0
  31. src/services/research_service/enhanced_synthesizer.py +375 -0
  32. src/services/research_service/query_generator.py +777 -0
  33. src/services/research_service/synthesizer.py +1273 -0
  34. src/services/search_service/__init__.py +5 -0
  35. src/services/search_service/indexer.py +186 -0
  36. src/services/search_service/search_engine.py +342 -0
  37. src/services/simple_enhanced_main.py +287 -0
  38. cite_agent/__distribution__.py +0 -7
  39. cite_agent-1.0.4.dist-info/RECORD +0 -23
  40. {cite_agent-1.0.4.dist-info → cite_agent-1.0.5.dist-info}/WHEEL +0 -0
  41. {cite_agent-1.0.4.dist-info → cite_agent-1.0.5.dist-info}/entry_points.txt +0 -0
  42. {cite_agent-1.0.4.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__ = "1.0.4"
10
+ __version__ = "0.9.0b1"
11
11
  __author__ = "Nocturnal Archive Team"
12
12
  __email__ = "contact@nocturnal.dev"
13
13
 
@@ -63,28 +63,14 @@ class AccountClient:
63
63
  )
64
64
  self.timeout = timeout
65
65
 
66
- def provision(self, email: str, password: str, is_new_user: bool = False) -> AccountCredentials:
67
- """
68
- Provision account credentials (login or register).
69
-
70
- Args:
71
- email: User email address
72
- password: User password
73
- is_new_user: True for registration, False for login
74
-
75
- Returns:
76
- AccountCredentials object
77
-
78
- Raises:
79
- AccountProvisioningError: If authentication fails
80
- """
66
+ def provision(self, email: str, password: str) -> AccountCredentials:
81
67
  if self.base_url:
82
- payload = self._request_credentials(email, password, is_new_user=is_new_user)
68
+ payload = self._request_credentials(email, password)
83
69
  return AccountCredentials.from_payload(email=email, payload=payload)
84
70
  return self._generate_offline_credentials(email, password)
85
71
 
86
72
  # -- internal helpers -------------------------------------------------
87
- def _request_credentials(self, email: str, password: str, is_new_user: bool = False) -> Dict[str, Any]:
73
+ def _request_credentials(self, email: str, password: str) -> Dict[str, Any]:
88
74
  try: # pragma: no cover - requires network
89
75
  import requests # type: ignore
90
76
  except Exception as exc: # pragma: no cover - executed when requests missing
@@ -92,42 +78,29 @@ class AccountClient:
92
78
  "The 'requests' package is required for control-plane authentication"
93
79
  ) from exc
94
80
 
95
- # Choose endpoint based on is_new_user flag
96
- if is_new_user:
97
- endpoint = self.base_url.rstrip("/") + "/api/auth/register"
98
- operation = "registration"
99
- else:
100
- endpoint = self.base_url.rstrip("/") + "/api/auth/login"
101
- operation = "login"
102
-
81
+ # Try login first
82
+ login_endpoint = self.base_url.rstrip("/") + "/api/auth/login"
103
83
  body = {"email": email, "password": password}
104
84
 
105
85
  try:
106
- response = requests.post(endpoint, json=body, timeout=self.timeout)
86
+ response = requests.post(login_endpoint, json=body, timeout=self.timeout)
107
87
  except Exception as exc: # pragma: no cover - network failure
108
- raise AccountProvisioningError(f"Failed to reach control plane for {operation}") from exc
88
+ raise AccountProvisioningError("Failed to reach control plane") from exc
109
89
 
110
- # Handle errors
90
+ # If login fails with 401 (user doesn't exist), try registration
91
+ if response.status_code == 401:
92
+ register_endpoint = self.base_url.rstrip("/") + "/api/auth/register"
93
+ try:
94
+ response = requests.post(register_endpoint, json=body, timeout=self.timeout)
95
+ except Exception as exc:
96
+ raise AccountProvisioningError("Failed to register account") from exc
97
+
98
+ # If still failing, raise error
111
99
  if response.status_code >= 400:
112
100
  detail = self._extract_error_detail(response)
113
-
114
- # Provide helpful error messages
115
- if response.status_code == 401 and not is_new_user:
116
- raise AccountProvisioningError(
117
- f"Login failed: Invalid email or password. If you're a new user, please register first."
118
- )
119
- elif response.status_code == 409:
120
- raise AccountProvisioningError(
121
- f"Registration failed: This email is already registered. Please log in instead."
122
- )
123
- elif response.status_code == 400 and "academic" in detail.lower():
124
- raise AccountProvisioningError(
125
- f"Registration requires an academic email address (.edu, .ac.uk, etc.)"
126
- )
127
- else:
128
- raise AccountProvisioningError(
129
- f"{operation.capitalize()} failed (status {response.status_code}): {detail}"
130
- )
101
+ raise AccountProvisioningError(
102
+ f"Authentication failed (status {response.status_code}): {detail}"
103
+ )
131
104
 
132
105
  try:
133
106
  payload = response.json()
@@ -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
- raise RuntimeError(
70
- "Not authenticated. Please run 'cite-agent --setup' first."
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
- self.console.print("\n[warning]👋 Hey there, looks like this machine hasn't met Nocturnal yet.[/warning]")
67
- self.console.print("[banner]Let's get you signed in — this only takes a minute.[/banner]")
68
- try:
69
- if not config.interactive_setup():
70
- self.console.print("[error]❌ Setup was cancelled. Exiting without starting the agent.[/error]")
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
- try:
150
- updater = NocturnalUpdater()
151
- update_info = updater.check_for_updates()
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("Cite-Agent v1.0.4")
446
- print("AI Research Assistant - Backend-Only Distribution")
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())