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.

Files changed (48) 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 +397 -64
  5. cite_agent/cli_conversational.py +294 -0
  6. cite_agent/cli_workflow.py +276 -0
  7. cite_agent/enhanced_ai_agent.py +3222 -117
  8. cite_agent/session_manager.py +215 -0
  9. cite_agent/setup_config.py +5 -21
  10. cite_agent/streaming_ui.py +252 -0
  11. cite_agent/updater.py +50 -17
  12. cite_agent/workflow.py +427 -0
  13. cite_agent/workflow_integration.py +275 -0
  14. cite_agent-1.2.3.dist-info/METADATA +442 -0
  15. cite_agent-1.2.3.dist-info/RECORD +54 -0
  16. {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/top_level.txt +1 -0
  17. src/__init__.py +1 -0
  18. src/services/__init__.py +132 -0
  19. src/services/auth_service/__init__.py +3 -0
  20. src/services/auth_service/auth_manager.py +33 -0
  21. src/services/graph/__init__.py +1 -0
  22. src/services/graph/knowledge_graph.py +194 -0
  23. src/services/llm_service/__init__.py +5 -0
  24. src/services/llm_service/llm_manager.py +495 -0
  25. src/services/paper_service/__init__.py +5 -0
  26. src/services/paper_service/openalex.py +231 -0
  27. src/services/performance_service/__init__.py +1 -0
  28. src/services/performance_service/rust_performance.py +395 -0
  29. src/services/research_service/__init__.py +23 -0
  30. src/services/research_service/chatbot.py +2056 -0
  31. src/services/research_service/citation_manager.py +436 -0
  32. src/services/research_service/context_manager.py +1441 -0
  33. src/services/research_service/conversation_manager.py +597 -0
  34. src/services/research_service/critical_paper_detector.py +577 -0
  35. src/services/research_service/enhanced_research.py +121 -0
  36. src/services/research_service/enhanced_synthesizer.py +375 -0
  37. src/services/research_service/query_generator.py +777 -0
  38. src/services/research_service/synthesizer.py +1273 -0
  39. src/services/search_service/__init__.py +5 -0
  40. src/services/search_service/indexer.py +186 -0
  41. src/services/search_service/search_engine.py +342 -0
  42. src/services/simple_enhanced_main.py +287 -0
  43. cite_agent/__distribution__.py +0 -7
  44. cite_agent-1.0.4.dist-info/METADATA +0 -234
  45. cite_agent-1.0.4.dist-info/RECORD +0 -23
  46. {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/WHEEL +0 -0
  47. {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/entry_points.txt +0 -0
  48. {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -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())
@@ -0,0 +1,276 @@
1
+ """
2
+ Enhanced CLI with workflow integration features
3
+ Reduces context switching for scholars
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Dict, List, Any, Optional
11
+ try:
12
+ import structlog
13
+ except ImportError:
14
+ import logging
15
+ structlog = logging
16
+
17
+ from .enhanced_ai_agent import EnhancedNocturnalAgent, ChatRequest
18
+ from .workflow_integration import WorkflowIntegration
19
+
20
+ logger = structlog.getLogger(__name__)
21
+
22
+ class WorkflowCLI:
23
+ """Enhanced CLI with workflow integration"""
24
+
25
+ def __init__(self):
26
+ self.agent = None
27
+ self.workflow = WorkflowIntegration()
28
+ self.session_id = f"workflow_{os.getpid()}"
29
+
30
+ async def initialize(self):
31
+ """Initialize the agent and workflow"""
32
+ self.agent = EnhancedNocturnalAgent()
33
+ await self.agent.initialize()
34
+
35
+ async def close(self):
36
+ """Clean up resources"""
37
+ if self.agent:
38
+ await self.agent.close()
39
+
40
+ async def search_and_save(self, query: str, user_id: str = "default") -> Dict[str, Any]:
41
+ """Search papers and save to library"""
42
+ try:
43
+ # Search for papers
44
+ request = ChatRequest(
45
+ question=f"Find academic papers about: {query}",
46
+ user_id=user_id,
47
+ conversation_id=self.session_id
48
+ )
49
+
50
+ response = await self.agent.process_request(request)
51
+
52
+ # Extract papers from response
53
+ papers = self._extract_papers_from_response(response)
54
+
55
+ # Save papers to library
56
+ saved_papers = []
57
+ for paper in papers:
58
+ paper_id = self.workflow.save_paper_to_library(paper, user_id)
59
+ saved_papers.append({
60
+ "id": paper_id,
61
+ "title": paper.get("title", "Unknown Title")
62
+ })
63
+
64
+ # Save session
65
+ session_id = self.workflow.save_session_history(
66
+ user_id,
67
+ query,
68
+ {
69
+ "response": response.response,
70
+ "papers": papers,
71
+ "tools_used": response.tools_used
72
+ }
73
+ )
74
+
75
+ return {
76
+ "success": True,
77
+ "papers_found": len(papers),
78
+ "papers_saved": len(saved_papers),
79
+ "session_id": session_id,
80
+ "saved_papers": saved_papers
81
+ }
82
+
83
+ except Exception as e:
84
+ logger.error("Error in search_and_save", error=str(e))
85
+ return {"success": False, "error": str(e)}
86
+
87
+ def _extract_papers_from_response(self, response) -> List[Dict[str, Any]]:
88
+ """Extract paper data from agent response"""
89
+ papers = []
90
+
91
+ # Try to extract from execution_results
92
+ if hasattr(response, 'execution_results') and response.execution_results:
93
+ for result in response.execution_results.values():
94
+ if isinstance(result, dict) and 'papers' in result:
95
+ papers.extend(result['papers'])
96
+
97
+ # Try to extract from api_results
98
+ if hasattr(response, 'api_results') and response.api_results:
99
+ for result in response.api_results.values():
100
+ if isinstance(result, dict) and 'papers' in result:
101
+ papers.extend(result['papers'])
102
+
103
+ return papers
104
+
105
+ async def export_library(self, user_id: str, format: str = "bibtex") -> str:
106
+ """Export user's library in specified format"""
107
+ try:
108
+ library = self.workflow.get_user_library(user_id)
109
+ papers = [paper_data.get('paper', {}) for paper_data in library]
110
+
111
+ if format == "bibtex":
112
+ filename = f"library_{user_id}.bib"
113
+ file_path = self.workflow.export_to_bibtex(papers, filename)
114
+ elif format == "markdown":
115
+ filename = f"library_{user_id}.md"
116
+ file_path = self.workflow.export_to_markdown(papers, filename)
117
+ else:
118
+ raise ValueError(f"Unsupported format: {format}")
119
+
120
+ return file_path
121
+
122
+ except Exception as e:
123
+ logger.error("Error exporting library", error=str(e))
124
+ raise
125
+
126
+ def search_library(self, user_id: str, query: str) -> List[Dict[str, Any]]:
127
+ """Search user's saved library"""
128
+ return self.workflow.search_library(user_id, query)
129
+
130
+ def get_library_stats(self, user_id: str) -> Dict[str, Any]:
131
+ """Get library statistics"""
132
+ library = self.workflow.get_user_library(user_id)
133
+ sessions = self.workflow.get_session_history(user_id, 100)
134
+
135
+ # Calculate stats
136
+ total_papers = len(library)
137
+
138
+ # Papers by year
139
+ papers_by_year = {}
140
+ for paper_data in library:
141
+ paper = paper_data.get('paper', {})
142
+ year = paper.get('year', 'Unknown')
143
+ papers_by_year[year] = papers_by_year.get(year, 0) + 1
144
+
145
+ # Most used tools
146
+ tools_used = {}
147
+ for session in sessions:
148
+ for tool in session.get('tools_used', []):
149
+ tools_used[tool] = tools_used.get(tool, 0) + 1
150
+
151
+ return {
152
+ "total_papers": total_papers,
153
+ "total_sessions": len(sessions),
154
+ "papers_by_year": papers_by_year,
155
+ "most_used_tools": dict(sorted(tools_used.items(), key=lambda x: x[1], reverse=True)[:5])
156
+ }
157
+
158
+ async def get_citation_suggestions(self, paper: Dict[str, Any]) -> List[str]:
159
+ """Get citation suggestions for a paper"""
160
+ return self.workflow.generate_citation_suggestions(paper)
161
+
162
+ def get_session_history(self, user_id: str, limit: int = 10) -> List[Dict[str, Any]]:
163
+ """Get recent session history"""
164
+ return self.workflow.get_session_history(user_id, limit)
165
+
166
+ async def interactive_workflow(self, user_id: str = "default"):
167
+ """Interactive workflow mode"""
168
+ print("🔬 Cite-Agent Workflow Mode")
169
+ print("=" * 50)
170
+ print("Commands:")
171
+ print(" search <query> - Search and save papers")
172
+ print(" library - Show saved papers")
173
+ print(" export <format> - Export library (bibtex/markdown)")
174
+ print(" stats - Show library statistics")
175
+ print(" history - Show recent searches")
176
+ print(" suggest <title> - Get citation suggestions")
177
+ print(" quit - Exit workflow mode")
178
+ print()
179
+
180
+ while True:
181
+ try:
182
+ command = input("workflow> ").strip()
183
+
184
+ if command.lower() in ['quit', 'exit', 'q']:
185
+ break
186
+ elif command.startswith('search '):
187
+ query = command[7:].strip()
188
+ if query:
189
+ result = await self.search_and_save(query, user_id)
190
+ if result['success']:
191
+ print(f"✅ Found {result['papers_found']} papers, saved {result['papers_saved']}")
192
+ else:
193
+ print(f"❌ Error: {result.get('error', 'Unknown error')}")
194
+ else:
195
+ print("❌ Please provide a search query")
196
+
197
+ elif command == 'library':
198
+ library = self.workflow.get_user_library(user_id)
199
+ if library:
200
+ print(f"📚 Library ({len(library)} papers):")
201
+ for i, paper_data in enumerate(library[:10], 1):
202
+ paper = paper_data.get('paper', {})
203
+ title = paper.get('title', 'Unknown Title')[:60]
204
+ year = paper.get('year', 'Unknown')
205
+ print(f" {i}. {title}... ({year})")
206
+ if len(library) > 10:
207
+ print(f" ... and {len(library) - 10} more")
208
+ else:
209
+ print("📚 Library is empty")
210
+
211
+ elif command.startswith('export '):
212
+ format = command[7:].strip().lower()
213
+ if format in ['bibtex', 'markdown']:
214
+ try:
215
+ file_path = await self.export_library(user_id, format)
216
+ print(f"✅ Exported to: {file_path}")
217
+ except Exception as e:
218
+ print(f"❌ Export error: {e}")
219
+ else:
220
+ print("❌ Supported formats: bibtex, markdown")
221
+
222
+ elif command == 'stats':
223
+ stats = self.get_library_stats(user_id)
224
+ print("📊 Library Statistics:")
225
+ print(f" Total papers: {stats['total_papers']}")
226
+ print(f" Total sessions: {stats['total_sessions']}")
227
+ print(" Papers by year:")
228
+ for year, count in sorted(stats['papers_by_year'].items()):
229
+ print(f" {year}: {count}")
230
+ print(" Most used tools:")
231
+ for tool, count in stats['most_used_tools'].items():
232
+ print(f" {tool}: {count}")
233
+
234
+ elif command == 'history':
235
+ history = self.get_session_history(user_id, 5)
236
+ if history:
237
+ print("🕒 Recent Searches:")
238
+ for i, session in enumerate(history, 1):
239
+ query = session.get('query', 'Unknown')[:50]
240
+ timestamp = session.get('timestamp', 'Unknown')[:19]
241
+ papers = session.get('papers_found', 0)
242
+ print(f" {i}. {query}... ({papers} papers) - {timestamp}")
243
+ else:
244
+ print("🕒 No search history")
245
+
246
+ elif command.startswith('suggest '):
247
+ title = command[8:].strip()
248
+ if title:
249
+ # Find paper in library
250
+ library = self.workflow.get_user_library(user_id)
251
+ paper = None
252
+ for paper_data in library:
253
+ if title.lower() in paper_data.get('paper', {}).get('title', '').lower():
254
+ paper = paper_data.get('paper', {})
255
+ break
256
+
257
+ if paper:
258
+ suggestions = await self.get_citation_suggestions(paper)
259
+ print(f"💡 Suggestions for '{paper.get('title', 'Unknown')}':")
260
+ for suggestion in suggestions:
261
+ print(f" • {suggestion}")
262
+ else:
263
+ print(f"❌ Paper not found in library: {title}")
264
+ else:
265
+ print("❌ Please provide a paper title")
266
+
267
+ else:
268
+ print("❌ Unknown command. Type 'quit' to exit.")
269
+
270
+ except KeyboardInterrupt:
271
+ print("\n👋 Goodbye!")
272
+ break
273
+ except Exception as e:
274
+ print(f"❌ Error: {e}")
275
+
276
+ print("👋 Workflow mode ended")