cite-agent 1.0.4__py3-none-any.whl → 1.2.3__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/account_client.py +19 -46
- cite_agent/agent_backend_only.py +30 -4
- cite_agent/cli.py +397 -64
- cite_agent/cli_conversational.py +294 -0
- cite_agent/cli_workflow.py +276 -0
- cite_agent/enhanced_ai_agent.py +3222 -117
- cite_agent/session_manager.py +215 -0
- cite_agent/setup_config.py +5 -21
- cite_agent/streaming_ui.py +252 -0
- cite_agent/updater.py +50 -17
- cite_agent/workflow.py +427 -0
- cite_agent/workflow_integration.py +275 -0
- cite_agent-1.2.3.dist-info/METADATA +442 -0
- cite_agent-1.2.3.dist-info/RECORD +54 -0
- {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.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.4.dist-info/METADATA +0 -234
- cite_agent-1.0.4.dist-info/RECORD +0 -23
- {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/WHEEL +0 -0
- {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/entry_points.txt +0 -0
- {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
User-Friendly Session Manager for Cite-Agent
|
|
4
|
+
Handles session detection, user choices, and authentication flow
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, Dict, Any
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.prompt import Prompt, Confirm
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
from rich.text import Text
|
|
17
|
+
|
|
18
|
+
class SessionManager:
|
|
19
|
+
"""User-friendly session management for Cite-Agent"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.console = Console()
|
|
23
|
+
self.session_file = Path.home() / ".nocturnal_archive" / "session.json"
|
|
24
|
+
self.config_file = Path.home() / ".nocturnal_archive" / "config.env"
|
|
25
|
+
self.session_data: Optional[Dict[str, Any]] = None
|
|
26
|
+
|
|
27
|
+
def detect_existing_session(self) -> bool:
|
|
28
|
+
"""Detect if there's an existing session and load it"""
|
|
29
|
+
if not self.session_file.exists():
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
with open(self.session_file, 'r') as f:
|
|
34
|
+
self.session_data = json.load(f)
|
|
35
|
+
return True
|
|
36
|
+
except Exception as e:
|
|
37
|
+
self.console.print(f"[red]⚠️ Session file corrupted: {e}[/red]")
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
def show_session_info(self):
|
|
41
|
+
"""Display existing session information in a user-friendly way"""
|
|
42
|
+
if not self.session_data:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
email = self.session_data.get('email', 'Unknown')
|
|
46
|
+
user_id = self.session_data.get('user_id', 'Unknown')[:8] + "..."
|
|
47
|
+
expires_at = self.session_data.get('expires_at', 'Unknown')
|
|
48
|
+
daily_limit = self.session_data.get('daily_token_limit', 0)
|
|
49
|
+
|
|
50
|
+
# Create a nice table
|
|
51
|
+
table = Table(title="🔑 Existing Session Found", show_header=True, header_style="bold green")
|
|
52
|
+
table.add_column("Property", style="cyan", width=15)
|
|
53
|
+
table.add_column("Value", style="white")
|
|
54
|
+
|
|
55
|
+
table.add_row("Email", email)
|
|
56
|
+
table.add_row("User ID", user_id)
|
|
57
|
+
table.add_row("Daily Limit", f"{daily_limit:,} queries")
|
|
58
|
+
table.add_row("Expires", expires_at)
|
|
59
|
+
|
|
60
|
+
self.console.print()
|
|
61
|
+
self.console.print(table)
|
|
62
|
+
self.console.print()
|
|
63
|
+
|
|
64
|
+
def ask_session_choice(self) -> str:
|
|
65
|
+
"""Ask user what they want to do with the existing session"""
|
|
66
|
+
self.console.print("[bold cyan]What would you like to do?[/bold cyan]")
|
|
67
|
+
self.console.print()
|
|
68
|
+
|
|
69
|
+
# Create a nice menu
|
|
70
|
+
menu_table = Table(show_header=False, box=None, padding=(0, 1))
|
|
71
|
+
menu_table.add_column("Choice", style="bold green", width=3)
|
|
72
|
+
menu_table.add_column("Action", style="white", width=20)
|
|
73
|
+
menu_table.add_column("Description", style="dim", width=40)
|
|
74
|
+
|
|
75
|
+
menu_table.add_row("1", "Resume", "Continue with this session")
|
|
76
|
+
menu_table.add_row("2", "Switch", "Login with different account")
|
|
77
|
+
menu_table.add_row("3", "Logout", "Clear session and start fresh")
|
|
78
|
+
menu_table.add_row("4", "Help", "Show session management help")
|
|
79
|
+
|
|
80
|
+
self.console.print(menu_table)
|
|
81
|
+
self.console.print()
|
|
82
|
+
|
|
83
|
+
while True:
|
|
84
|
+
choice = Prompt.ask(
|
|
85
|
+
"Choose an option",
|
|
86
|
+
choices=["1", "2", "3", "4", "resume", "switch", "logout", "help"],
|
|
87
|
+
default="1"
|
|
88
|
+
).lower()
|
|
89
|
+
|
|
90
|
+
if choice in ["1", "resume"]:
|
|
91
|
+
return "resume"
|
|
92
|
+
elif choice in ["2", "switch"]:
|
|
93
|
+
return "switch"
|
|
94
|
+
elif choice in ["3", "logout"]:
|
|
95
|
+
return "logout"
|
|
96
|
+
elif choice in ["4", "help"]:
|
|
97
|
+
self.show_help()
|
|
98
|
+
continue
|
|
99
|
+
else:
|
|
100
|
+
self.console.print("[red]Invalid choice. Please try again.[/red]")
|
|
101
|
+
|
|
102
|
+
def show_help(self):
|
|
103
|
+
"""Show help for session management"""
|
|
104
|
+
help_text = """
|
|
105
|
+
[bold cyan]Session Management Help[/bold cyan]
|
|
106
|
+
|
|
107
|
+
[bold green]Resume:[/bold green] Continue with your existing session
|
|
108
|
+
• Use your current login and settings
|
|
109
|
+
• No need to re-authenticate
|
|
110
|
+
• All your data and preferences are preserved
|
|
111
|
+
|
|
112
|
+
[bold yellow]Switch:[/bold yellow] Login with a different account
|
|
113
|
+
• Logout from current session
|
|
114
|
+
• Start fresh with new account
|
|
115
|
+
• Previous session data will be cleared
|
|
116
|
+
|
|
117
|
+
[bold red]Logout:[/bold red] Clear session and start fresh
|
|
118
|
+
• Remove all saved login information
|
|
119
|
+
• Start completely fresh
|
|
120
|
+
• You'll need to login again
|
|
121
|
+
|
|
122
|
+
[bold blue]Session Files:[/bold blue]
|
|
123
|
+
• Session: ~/.nocturnal_archive/session.json
|
|
124
|
+
• Config: ~/.nocturnal_archive/config.env
|
|
125
|
+
|
|
126
|
+
[bold blue]Manual Session Management:[/bold blue]
|
|
127
|
+
• To clear session manually: rm ~/.nocturnal_archive/session.json
|
|
128
|
+
• To clear config: rm ~/.nocturnal_archive/config.env
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
self.console.print(Panel(help_text, title="Help", border_style="blue"))
|
|
132
|
+
self.console.print()
|
|
133
|
+
|
|
134
|
+
def clear_session(self) -> bool:
|
|
135
|
+
"""Clear the existing session"""
|
|
136
|
+
try:
|
|
137
|
+
if self.session_file.exists():
|
|
138
|
+
self.session_file.unlink()
|
|
139
|
+
if self.config_file.exists():
|
|
140
|
+
self.config_file.unlink()
|
|
141
|
+
self.console.print("[green]✅ Session cleared successfully[/green]")
|
|
142
|
+
return True
|
|
143
|
+
except Exception as e:
|
|
144
|
+
self.console.print(f"[red]❌ Error clearing session: {e}[/red]")
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
def handle_session_affirmation(self) -> str:
|
|
148
|
+
"""Main function to handle session affirmation with user-friendly interface"""
|
|
149
|
+
# Check for existing session
|
|
150
|
+
has_session = self.detect_existing_session()
|
|
151
|
+
|
|
152
|
+
if not has_session:
|
|
153
|
+
self.console.print("[yellow]No existing session found. Starting fresh...[/yellow]")
|
|
154
|
+
return "fresh"
|
|
155
|
+
|
|
156
|
+
# Show session information
|
|
157
|
+
self.show_session_info()
|
|
158
|
+
|
|
159
|
+
# Ask user what to do
|
|
160
|
+
choice = self.ask_session_choice()
|
|
161
|
+
|
|
162
|
+
if choice == "resume":
|
|
163
|
+
self.console.print("[green]✅ Resuming existing session...[/green]")
|
|
164
|
+
return "resume"
|
|
165
|
+
elif choice == "switch":
|
|
166
|
+
self.console.print("[yellow]🔄 Switching to different account...[/yellow]")
|
|
167
|
+
if self.clear_session():
|
|
168
|
+
return "fresh"
|
|
169
|
+
else:
|
|
170
|
+
return "error"
|
|
171
|
+
elif choice == "logout":
|
|
172
|
+
self.console.print("[red]🚪 Logging out...[/red]")
|
|
173
|
+
if self.clear_session():
|
|
174
|
+
return "fresh"
|
|
175
|
+
else:
|
|
176
|
+
return "error"
|
|
177
|
+
|
|
178
|
+
return "error"
|
|
179
|
+
|
|
180
|
+
def setup_environment_variables(self):
|
|
181
|
+
"""Set up environment variables for backend mode"""
|
|
182
|
+
# PRODUCTION MODE: Force backend, ensure monetization
|
|
183
|
+
# NEVER load user's .env files in production
|
|
184
|
+
|
|
185
|
+
# Set backend URL if not already set
|
|
186
|
+
if "NOCTURNAL_API_URL" not in os.environ:
|
|
187
|
+
os.environ["NOCTURNAL_API_URL"] = "https://cite-agent-api-720dfadd602c.herokuapp.com/api"
|
|
188
|
+
|
|
189
|
+
# SECURITY: Default to backend mode (USE_LOCAL_KEYS=false)
|
|
190
|
+
# This ensures users MUST authenticate and pay
|
|
191
|
+
if "USE_LOCAL_KEYS" not in os.environ:
|
|
192
|
+
os.environ["USE_LOCAL_KEYS"] = "false"
|
|
193
|
+
|
|
194
|
+
def get_session_status(self) -> Dict[str, Any]:
|
|
195
|
+
"""Get current session status for debugging"""
|
|
196
|
+
return {
|
|
197
|
+
"session_file_exists": self.session_file.exists(),
|
|
198
|
+
"config_file_exists": self.config_file.exists(),
|
|
199
|
+
"session_data": self.session_data,
|
|
200
|
+
"use_local_keys": os.environ.get("USE_LOCAL_KEYS", "not set"),
|
|
201
|
+
"api_url": os.environ.get("NOCTURNAL_API_URL", "not set")
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
def main():
|
|
205
|
+
"""Test the session manager"""
|
|
206
|
+
sm = SessionManager()
|
|
207
|
+
result = sm.handle_session_affirmation()
|
|
208
|
+
print(f"Result: {result}")
|
|
209
|
+
|
|
210
|
+
# Show status
|
|
211
|
+
status = sm.get_session_status()
|
|
212
|
+
print(f"Status: {status}")
|
|
213
|
+
|
|
214
|
+
if __name__ == "__main__":
|
|
215
|
+
main()
|
cite_agent/setup_config.py
CHANGED
|
@@ -63,18 +63,6 @@ class NocturnalConfig:
|
|
|
63
63
|
print("You'll use your institution-issued account to sign in. No invite codes or manual API keys required.")
|
|
64
64
|
print()
|
|
65
65
|
|
|
66
|
-
# Ask if new user or returning user
|
|
67
|
-
print("Are you a new user or returning user?")
|
|
68
|
-
print(" 1. New user (register)")
|
|
69
|
-
print(" 2. Returning user (login)")
|
|
70
|
-
choice = input("Enter choice (1 or 2): ").strip()
|
|
71
|
-
|
|
72
|
-
is_new_user = choice == "1"
|
|
73
|
-
action = "Registration" if is_new_user else "Login"
|
|
74
|
-
|
|
75
|
-
print(f"\n{action}")
|
|
76
|
-
print("-" * 40)
|
|
77
|
-
|
|
78
66
|
email = self._prompt_academic_email()
|
|
79
67
|
if not email:
|
|
80
68
|
return False
|
|
@@ -83,18 +71,14 @@ class NocturnalConfig:
|
|
|
83
71
|
if not password:
|
|
84
72
|
return False
|
|
85
73
|
|
|
86
|
-
if
|
|
74
|
+
if not self._confirm_beta_terms():
|
|
87
75
|
print("❌ Terms must be accepted to continue")
|
|
88
76
|
return False
|
|
89
77
|
|
|
90
78
|
try:
|
|
91
|
-
credentials = self._provision_account(email, password
|
|
92
|
-
if is_new_user:
|
|
93
|
-
print(f"\n✅ Account created successfully for {email}")
|
|
94
|
-
else:
|
|
95
|
-
print(f"\n✅ Logged in successfully as {email}")
|
|
79
|
+
credentials = self._provision_account(email, password)
|
|
96
80
|
except AccountProvisioningError as exc:
|
|
97
|
-
print(f"❌
|
|
81
|
+
print(f"❌ Could not verify your account: {exc}")
|
|
98
82
|
return False
|
|
99
83
|
|
|
100
84
|
print("\n🛡️ Recap of beta limitations:")
|
|
@@ -179,9 +163,9 @@ class NocturnalConfig:
|
|
|
179
163
|
print("❌ Could not confirm password after multiple attempts")
|
|
180
164
|
return None
|
|
181
165
|
|
|
182
|
-
def _provision_account(self, email: str, password: str
|
|
166
|
+
def _provision_account(self, email: str, password: str) -> AccountCredentials:
|
|
183
167
|
client = AccountClient()
|
|
184
|
-
return client.provision(email=email, password=password
|
|
168
|
+
return client.provision(email=email, password=password)
|
|
185
169
|
|
|
186
170
|
def _is_academic_email(self, email: str) -> bool:
|
|
187
171
|
if "@" not in email:
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Streaming Chat UI - Cursor/Claude Style Interface
|
|
4
|
+
Minimal, clean, conversational interface for data analysis assistant
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import asyncio
|
|
10
|
+
from typing import Optional, AsyncGenerator
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.markdown import Markdown
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
from rich.live import Live
|
|
15
|
+
from rich.spinner import Spinner
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StreamingChatUI:
|
|
21
|
+
"""
|
|
22
|
+
Clean, minimal chat interface matching Cursor/Claude aesthetics
|
|
23
|
+
- Simple header (just app name)
|
|
24
|
+
- "You:" / "Agent:" conversation labels
|
|
25
|
+
- Streaming character-by-character output
|
|
26
|
+
- Transient action indicators
|
|
27
|
+
- Markdown rendering for rich text
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, app_name: str = "Nocturnal Archive", working_dir: Optional[str] = None):
|
|
31
|
+
self.app_name = app_name
|
|
32
|
+
self.working_dir = working_dir
|
|
33
|
+
self.console = Console()
|
|
34
|
+
self.typing_speed = 0.015 # ~60 chars/sec
|
|
35
|
+
|
|
36
|
+
def show_header(self):
|
|
37
|
+
"""Display minimal header on startup"""
|
|
38
|
+
self.console.print(f"\n[bold cyan]{self.app_name}[/bold cyan]")
|
|
39
|
+
if self.working_dir:
|
|
40
|
+
self.console.print(f"[dim]Connected to: {self.working_dir}[/dim]")
|
|
41
|
+
self.console.print("─" * 70)
|
|
42
|
+
self.console.print()
|
|
43
|
+
|
|
44
|
+
def show_user_message(self, message: str):
|
|
45
|
+
"""Display user message with 'You:' prefix"""
|
|
46
|
+
self.console.print(f"[bold]You:[/bold] {message}")
|
|
47
|
+
self.console.print()
|
|
48
|
+
|
|
49
|
+
async def stream_agent_response(
|
|
50
|
+
self,
|
|
51
|
+
content_generator: AsyncGenerator[str, None],
|
|
52
|
+
show_markdown: bool = True
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Stream agent response character-by-character
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
content_generator: Async generator yielding text chunks
|
|
59
|
+
show_markdown: Whether to render as markdown (default True)
|
|
60
|
+
"""
|
|
61
|
+
# No prefix for agent - just stream naturally
|
|
62
|
+
buffer = ""
|
|
63
|
+
|
|
64
|
+
async for chunk in content_generator:
|
|
65
|
+
buffer += chunk
|
|
66
|
+
# Stream character by character for natural feel
|
|
67
|
+
for char in chunk:
|
|
68
|
+
self.console.print(char, end="", style="white")
|
|
69
|
+
await asyncio.sleep(self.typing_speed)
|
|
70
|
+
|
|
71
|
+
self.console.print() # Newline after response
|
|
72
|
+
self.console.print() # Extra space for readability
|
|
73
|
+
|
|
74
|
+
return buffer
|
|
75
|
+
|
|
76
|
+
async def stream_markdown_response(self, markdown_text: str):
|
|
77
|
+
"""
|
|
78
|
+
Stream a markdown response with proper formatting
|
|
79
|
+
Used for final rendering after streaming is complete
|
|
80
|
+
"""
|
|
81
|
+
# Render markdown with Rich
|
|
82
|
+
md = Markdown(markdown_text)
|
|
83
|
+
self.console.print(md)
|
|
84
|
+
self.console.print()
|
|
85
|
+
|
|
86
|
+
def show_action_indicator(self, action: str) -> Live:
|
|
87
|
+
"""
|
|
88
|
+
Show a transient action indicator (e.g., [reading file...])
|
|
89
|
+
Returns Live object that should be stopped when action completes
|
|
90
|
+
|
|
91
|
+
Usage:
|
|
92
|
+
indicator = ui.show_action_indicator("analyzing data")
|
|
93
|
+
# ... do work ...
|
|
94
|
+
indicator.stop()
|
|
95
|
+
"""
|
|
96
|
+
spinner = Spinner("dots", text=f"[dim]{action}[/dim]")
|
|
97
|
+
live = Live(spinner, console=self.console, transient=True)
|
|
98
|
+
live.start()
|
|
99
|
+
return live
|
|
100
|
+
|
|
101
|
+
def show_error(self, error_message: str):
|
|
102
|
+
"""Display error message"""
|
|
103
|
+
self.console.print(f"[red]Error:[/red] {error_message}")
|
|
104
|
+
self.console.print()
|
|
105
|
+
|
|
106
|
+
def show_info(self, message: str):
|
|
107
|
+
"""Display info message"""
|
|
108
|
+
self.console.print(f"[dim]{message}[/dim]")
|
|
109
|
+
self.console.print()
|
|
110
|
+
|
|
111
|
+
def show_rate_limit_message(
|
|
112
|
+
self,
|
|
113
|
+
limit_type: str = "Archive API",
|
|
114
|
+
remaining_capabilities: Optional[list] = None
|
|
115
|
+
):
|
|
116
|
+
"""
|
|
117
|
+
Show soft degradation message when rate limited
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
limit_type: What service is limited (e.g., "Archive API")
|
|
121
|
+
remaining_capabilities: List of what's still available
|
|
122
|
+
"""
|
|
123
|
+
self.console.print(
|
|
124
|
+
f"\n[yellow]I've reached the daily limit for {limit_type} queries.[/yellow]\n"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if remaining_capabilities:
|
|
128
|
+
self.console.print("[bold]However, I can still assist you with:[/bold]")
|
|
129
|
+
for capability in remaining_capabilities:
|
|
130
|
+
self.console.print(f" • {capability}")
|
|
131
|
+
self.console.print()
|
|
132
|
+
|
|
133
|
+
self.console.print(
|
|
134
|
+
"[dim]For unlimited access, consider upgrading to Pro.[/dim]\n"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def get_user_input(self, prompt: str = "You: ") -> str:
|
|
138
|
+
"""Get user input with custom prompt"""
|
|
139
|
+
try:
|
|
140
|
+
user_input = self.console.input(f"[bold]{prompt}[/bold]")
|
|
141
|
+
self.console.print()
|
|
142
|
+
return user_input.strip()
|
|
143
|
+
except (KeyboardInterrupt, EOFError):
|
|
144
|
+
self.console.print("\n[dim]Goodbye![/dim]")
|
|
145
|
+
sys.exit(0)
|
|
146
|
+
|
|
147
|
+
def clear_screen(self):
|
|
148
|
+
"""Clear terminal screen"""
|
|
149
|
+
self.console.clear()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# Utility functions for streaming from Groq API
|
|
153
|
+
|
|
154
|
+
async def groq_stream_to_generator(stream) -> AsyncGenerator[str, None]:
|
|
155
|
+
"""
|
|
156
|
+
Convert Groq streaming response to async generator
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
stream: Groq stream object from client.chat.completions.create(stream=True)
|
|
160
|
+
|
|
161
|
+
Yields:
|
|
162
|
+
Text chunks from the stream
|
|
163
|
+
"""
|
|
164
|
+
for chunk in stream:
|
|
165
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
166
|
+
yield chunk.choices[0].delta.content
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def simulate_streaming(text: str, chunk_size: int = 5) -> AsyncGenerator[str, None]:
|
|
170
|
+
"""
|
|
171
|
+
Simulate streaming for testing purposes
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
text: Full text to stream
|
|
175
|
+
chunk_size: Characters per chunk
|
|
176
|
+
|
|
177
|
+
Yields:
|
|
178
|
+
Text chunks
|
|
179
|
+
"""
|
|
180
|
+
for i in range(0, len(text), chunk_size):
|
|
181
|
+
chunk = text[i:i + chunk_size]
|
|
182
|
+
yield chunk
|
|
183
|
+
await asyncio.sleep(0.05) # Simulate network delay
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# Example usage
|
|
187
|
+
async def example_usage():
|
|
188
|
+
"""Example of how to use the streaming UI"""
|
|
189
|
+
|
|
190
|
+
ui = StreamingChatUI(
|
|
191
|
+
app_name="Nocturnal Archive",
|
|
192
|
+
working_dir="/home/researcher/project"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Show header on startup
|
|
196
|
+
ui.show_header()
|
|
197
|
+
|
|
198
|
+
# Simulate conversation
|
|
199
|
+
ui.show_user_message("hello")
|
|
200
|
+
|
|
201
|
+
# Simulate streaming response
|
|
202
|
+
response_text = (
|
|
203
|
+
"Good evening. I'm ready to assist with your analysis. "
|
|
204
|
+
"What would you like to work on today?"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
async def response_generator():
|
|
208
|
+
async for chunk in simulate_streaming(response_text):
|
|
209
|
+
yield chunk
|
|
210
|
+
|
|
211
|
+
await ui.stream_agent_response(response_generator())
|
|
212
|
+
|
|
213
|
+
# Get next user input
|
|
214
|
+
user_input = ui.get_user_input()
|
|
215
|
+
ui.show_user_message(user_input)
|
|
216
|
+
|
|
217
|
+
# Show action indicator
|
|
218
|
+
indicator = ui.show_action_indicator("reading file")
|
|
219
|
+
await asyncio.sleep(2) # Simulate work
|
|
220
|
+
indicator.stop()
|
|
221
|
+
|
|
222
|
+
# Stream another response with markdown
|
|
223
|
+
markdown_response = """
|
|
224
|
+
I can see you have several data files here:
|
|
225
|
+
|
|
226
|
+
• **gdp_data_2020_2024.csv** (245 KB)
|
|
227
|
+
• **unemployment_rates.xlsx** (89 KB)
|
|
228
|
+
|
|
229
|
+
Which dataset would you like me to analyze first?
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
async def md_generator():
|
|
233
|
+
async for chunk in simulate_streaming(markdown_response):
|
|
234
|
+
yield chunk
|
|
235
|
+
|
|
236
|
+
await ui.stream_agent_response(md_generator())
|
|
237
|
+
|
|
238
|
+
# Show rate limit message
|
|
239
|
+
ui.show_rate_limit_message(
|
|
240
|
+
limit_type="Archive API",
|
|
241
|
+
remaining_capabilities=[
|
|
242
|
+
"Local data analysis (unlimited)",
|
|
243
|
+
"Web searches (unlimited)",
|
|
244
|
+
"Financial data (5 queries remaining)",
|
|
245
|
+
"Conversation and file reading"
|
|
246
|
+
]
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
# Run example
|
|
252
|
+
asyncio.run(example_usage())
|
cite_agent/updater.py
CHANGED
|
@@ -12,18 +12,29 @@ from pathlib import Path
|
|
|
12
12
|
from typing import Optional, Dict, Any
|
|
13
13
|
|
|
14
14
|
try:
|
|
15
|
-
|
|
15
|
+
# Use modern importlib.metadata instead of deprecated pkg_resources
|
|
16
|
+
from importlib.metadata import version as get_version
|
|
17
|
+
pkg_resources = None # Not needed anymore
|
|
16
18
|
except ImportError:
|
|
17
|
-
|
|
19
|
+
# Fallback for Python < 3.8
|
|
20
|
+
try:
|
|
21
|
+
import warnings
|
|
22
|
+
with warnings.catch_warnings():
|
|
23
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
24
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
25
|
+
import pkg_resources
|
|
26
|
+
except ImportError:
|
|
27
|
+
pkg_resources = None
|
|
28
|
+
get_version = None
|
|
18
29
|
|
|
19
30
|
class NocturnalUpdater:
|
|
20
31
|
"""Handles automatic updates for Nocturnal Archive"""
|
|
21
32
|
|
|
22
33
|
def __init__(self):
|
|
23
34
|
self.current_version = self.get_current_version()
|
|
24
|
-
self.package_name = "nocturnal-archive"
|
|
35
|
+
self.package_name = "cite-agent" # Fixed: was "nocturnal-archive"
|
|
25
36
|
self.pypi_url = f"https://pypi.org/pypi/{self.package_name}/json"
|
|
26
|
-
self.kill_switch_url = "https://api.
|
|
37
|
+
self.kill_switch_url = "https://cite-agent-api-720dfadd602c.herokuapp.com/api/health"
|
|
27
38
|
|
|
28
39
|
def check_kill_switch(self) -> Dict[str, Any]:
|
|
29
40
|
"""Check if kill switch is activated"""
|
|
@@ -37,16 +48,23 @@ class NocturnalUpdater:
|
|
|
37
48
|
|
|
38
49
|
def get_current_version(self) -> str:
|
|
39
50
|
"""Get current installed version"""
|
|
51
|
+
# Try modern importlib.metadata first
|
|
52
|
+
try:
|
|
53
|
+
return get_version(self.package_name)
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
# Fallback to pkg_resources (deprecated)
|
|
40
58
|
if pkg_resources:
|
|
41
59
|
try:
|
|
42
60
|
return pkg_resources.get_distribution(self.package_name).version
|
|
43
|
-
except
|
|
61
|
+
except Exception:
|
|
44
62
|
pass
|
|
45
63
|
|
|
46
|
-
#
|
|
64
|
+
# Last resort: try to get version from installed package
|
|
47
65
|
try:
|
|
48
|
-
import
|
|
49
|
-
return getattr(
|
|
66
|
+
import cite_agent
|
|
67
|
+
return getattr(cite_agent, '__version__', '1.0.0')
|
|
50
68
|
except ImportError:
|
|
51
69
|
return "1.0.0"
|
|
52
70
|
|
|
@@ -103,32 +121,47 @@ class NocturnalUpdater:
|
|
|
103
121
|
except:
|
|
104
122
|
return False
|
|
105
123
|
|
|
106
|
-
def update_package(self, force: bool = False) -> bool:
|
|
124
|
+
def update_package(self, force: bool = False, silent: bool = False) -> bool:
|
|
107
125
|
"""Update the package to latest version"""
|
|
108
126
|
try:
|
|
109
|
-
|
|
127
|
+
if not silent:
|
|
128
|
+
print("🔄 Updating cite-agent...")
|
|
110
129
|
|
|
111
130
|
# Check if update is needed
|
|
112
131
|
if not force:
|
|
113
132
|
update_info = self.check_for_updates()
|
|
114
133
|
if not update_info or not update_info["available"]:
|
|
115
|
-
|
|
134
|
+
if not silent:
|
|
135
|
+
print("✅ No updates available")
|
|
116
136
|
return True
|
|
117
137
|
|
|
118
|
-
# Perform update
|
|
119
|
-
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", self.package_name]
|
|
138
|
+
# Perform update with user flag to avoid system package conflicts
|
|
139
|
+
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "--user", self.package_name]
|
|
120
140
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
121
141
|
|
|
122
142
|
if result.returncode == 0:
|
|
123
|
-
|
|
124
|
-
|
|
143
|
+
# Create flag file to notify next launch
|
|
144
|
+
try:
|
|
145
|
+
from pathlib import Path
|
|
146
|
+
update_flag = Path.home() / ".nocturnal_archive" / ".updated"
|
|
147
|
+
update_flag.parent.mkdir(exist_ok=True)
|
|
148
|
+
update_flag.write_text(self.get_current_version())
|
|
149
|
+
except:
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
if not silent:
|
|
153
|
+
new_version = self.get_current_version()
|
|
154
|
+
print(f"✅ Updated to version {new_version}")
|
|
155
|
+
print("🔄 Restart cite-agent to use the new version")
|
|
125
156
|
return True
|
|
126
157
|
else:
|
|
127
|
-
|
|
158
|
+
if not silent:
|
|
159
|
+
print(f"❌ Update failed: {result.stderr}")
|
|
128
160
|
return False
|
|
129
161
|
|
|
130
162
|
except Exception as e:
|
|
131
|
-
|
|
163
|
+
if not silent:
|
|
164
|
+
print(f"❌ Update error: {e}")
|
|
132
165
|
return False
|
|
133
166
|
|
|
134
167
|
def show_update_status(self):
|