cite-agent 1.3.4__py3-none-any.whl → 1.3.6__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/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.3.4"
1
+ __version__ = "1.3.6"
cite_agent/cli.py CHANGED
@@ -256,6 +256,19 @@ class NocturnalCLI:
256
256
  if not await self.initialize():
257
257
  return
258
258
 
259
+ # Detect if user is in a project directory (R, Python, Node, Jupyter, etc.)
260
+ try:
261
+ from .project_detector import ProjectDetector
262
+ detector = ProjectDetector()
263
+ project_info = detector.detect_project()
264
+
265
+ if project_info:
266
+ # Show project banner
267
+ banner = detector.format_project_banner(project_info)
268
+ self.console.print(banner, style="dim")
269
+ except:
270
+ pass # Silently skip if detection fails
271
+
259
272
  self.console.print("\n[bold]🤖 Interactive Mode[/] — Type your questions or 'quit' to exit")
260
273
  self.console.rule(style="magenta")
261
274
 
@@ -1698,10 +1698,55 @@ class EnhancedNocturnalAgent:
1698
1698
 
1699
1699
  elif response.status == 503:
1700
1700
  # Backend AI service temporarily unavailable (Cerebras/Groq rate limited)
1701
- # Graceful degradation: return helpful message instead of error
1701
+ # Auto-retry silently with exponential backoff
1702
+
1703
+ print("\n💭 Thinking... (backend is busy, retrying automatically)")
1704
+
1705
+ import asyncio
1706
+ retry_delays = [5, 15, 30] # Exponential backoff
1707
+
1708
+ for retry_num, delay in enumerate(retry_delays):
1709
+ await asyncio.sleep(delay)
1710
+
1711
+ # Retry the request
1712
+ async with self.session.post(url, json=payload, headers=headers, timeout=60) as retry_response:
1713
+ if retry_response.status == 200:
1714
+ # Success!
1715
+ data = await retry_response.json()
1716
+ response_text = data.get('response', '')
1717
+ tokens = data.get('tokens_used', 0)
1718
+
1719
+ all_tools = tools_used or []
1720
+ all_tools.append("backend_llm")
1721
+
1722
+ self.workflow.save_query_result(
1723
+ query=query,
1724
+ response=response_text,
1725
+ metadata={
1726
+ "tools_used": all_tools,
1727
+ "tokens_used": tokens,
1728
+ "model": data.get('model'),
1729
+ "provider": data.get('provider'),
1730
+ "retries": retry_num + 1
1731
+ }
1732
+ )
1733
+
1734
+ return ChatResponse(
1735
+ response=response_text,
1736
+ tokens_used=tokens,
1737
+ tools_used=all_tools,
1738
+ model=data.get('model', 'llama-3.3-70b'),
1739
+ timestamp=data.get('timestamp', datetime.now(timezone.utc).isoformat()),
1740
+ api_results=api_results
1741
+ )
1742
+ elif retry_response.status != 503:
1743
+ # Different error, stop retrying
1744
+ break
1745
+
1746
+ # All retries exhausted
1702
1747
  return ChatResponse(
1703
- response="⚠️ AI service temporarily busy. Try again in a moment, or rephrase your question.",
1704
- error_message="Service temporarily unavailable"
1748
+ response=" Service unavailable. Please try again in a few minutes.",
1749
+ error_message="Service unavailable after retries"
1705
1750
  )
1706
1751
 
1707
1752
  elif response.status == 200:
@@ -2619,7 +2664,8 @@ class EnhancedNocturnalAgent:
2619
2664
  # Quick check if query might need shell
2620
2665
  question_lower = request.question.lower()
2621
2666
  might_need_shell = any(word in question_lower for word in [
2622
- 'directory', 'folder', 'where', 'find', 'list', 'files', 'look', 'search', 'check', 'into'
2667
+ 'directory', 'folder', 'where', 'find', 'list', 'files', 'look', 'search', 'check', 'into',
2668
+ 'show', 'open', 'read', 'display', 'cat', 'view', 'contents', '.r', '.py', '.csv', '.ipynb'
2623
2669
  ])
2624
2670
 
2625
2671
  if might_need_shell and self.shell_session:
@@ -2631,19 +2677,28 @@ Previous conversation: {json.dumps(self.conversation_history[-2:]) if self.conve
2631
2677
 
2632
2678
  Respond ONLY with JSON:
2633
2679
  {{
2634
- "action": "pwd|ls|find|none",
2680
+ "action": "pwd|ls|find|read_file|none",
2635
2681
  "search_target": "cm522" (if find),
2636
2682
  "search_path": "~/Downloads" (if find),
2637
- "target_path": "/full/path" (if ls on previous result)
2683
+ "target_path": "/full/path" (if ls on previous result),
2684
+ "file_path": "/full/path/to/file.R" (if read_file)
2638
2685
  }}
2639
2686
 
2640
2687
  Examples:
2641
2688
  "where am i?" → {{"action": "pwd"}}
2642
2689
  "what files here?" → {{"action": "ls"}}
2643
- "find cm522 in downloads" → {{"action": "find", "search_target": "cm522", "search_path": "~/Downloads"}}
2644
- "look into it" + Previous: "Found /path/to/dir" → {{"action": "ls", "target_path": "/path/to/dir"}}
2690
+ "find cm522" → {{"action": "find", "search_target": "cm522"}}
2691
+ "look into it" + Previous: "Found /path" → {{"action": "ls", "target_path": "/path"}}
2692
+ "show me calculate_betas.R" → {{"action": "read_file", "file_path": "calculate_betas.R"}}
2693
+ "open regression.R" → {{"action": "read_file", "file_path": "regression.R"}}
2694
+ "read that file" + Previous: "regression.R" → {{"action": "read_file", "file_path": "regression.R"}}
2695
+ "display analysis.py" → {{"action": "read_file", "file_path": "analysis.py"}}
2696
+ "cat data.csv" → {{"action": "read_file", "file_path": "data.csv"}}
2697
+ "what columns does it have?" + Previous: file was shown → {{"action": "none"}} (LLM will parse from conversation)
2645
2698
  "Tesla revenue" → {{"action": "none"}}
2646
2699
 
2700
+ KEY: If query mentions a specific FILENAME (*.R, *.py, *.csv), use read_file, NOT find!
2701
+
2647
2702
  JSON:"""
2648
2703
 
2649
2704
  try:
@@ -2701,6 +2756,65 @@ JSON:"""
2701
2756
  "search_results": f"No directories matching '{search_target}' found in {search_path}"
2702
2757
  }
2703
2758
  tools_used.append("shell_execution")
2759
+
2760
+ elif shell_action == "read_file":
2761
+ # NEW: Read and inspect file (R, Python, CSV, etc.)
2762
+ import re # Import at function level
2763
+
2764
+ file_path = plan.get("file_path", "")
2765
+ if not file_path and might_need_shell:
2766
+ # Try to infer from query (e.g., "show me calculate_betas.R")
2767
+ filenames = re.findall(r'([a-zA-Z0-9_-]+\.[a-zA-Z]{1,4})', request.question)
2768
+ if filenames:
2769
+ # Check if file exists in current directory
2770
+ pwd = self.execute_command("pwd").strip()
2771
+ file_path = f"{pwd}/{filenames[0]}"
2772
+
2773
+ if file_path:
2774
+ if debug_mode:
2775
+ print(f"🔍 READING FILE: {file_path}")
2776
+
2777
+ # Read file content (first 100 lines to detect structure)
2778
+ cat_output = self.execute_command(f"head -100 {file_path}")
2779
+
2780
+ if not cat_output.startswith("ERROR"):
2781
+ # Detect file type and extract structure
2782
+ file_ext = file_path.split('.')[-1].lower()
2783
+
2784
+ # Extract column/variable info based on file type
2785
+ columns_info = ""
2786
+ if file_ext in ['csv', 'tsv']:
2787
+ # CSV: first line is usually headers
2788
+ first_line = cat_output.split('\n')[0] if cat_output else ""
2789
+ columns_info = f"CSV columns: {first_line}"
2790
+ elif file_ext in ['r', 'rmd']:
2791
+ # R script: look for dataframe column references (df$columnname)
2792
+ column_refs = re.findall(r'\$(\w+)', cat_output)
2793
+ unique_cols = list(dict.fromkeys(column_refs))[:10]
2794
+ if unique_cols:
2795
+ columns_info = f"Detected columns/variables: {', '.join(unique_cols)}"
2796
+ elif file_ext == 'py':
2797
+ # Python: look for DataFrame['column'] or df.column
2798
+ column_refs = re.findall(r'\[[\'""](\w+)[\'"]\]|\.(\w+)', cat_output)
2799
+ unique_cols = list(dict.fromkeys([c[0] or c[1] for c in column_refs if c[0] or c[1]]))[:10]
2800
+ if unique_cols:
2801
+ columns_info = f"Detected columns/attributes: {', '.join(unique_cols)}"
2802
+
2803
+ api_results["file_context"] = {
2804
+ "file_path": file_path,
2805
+ "file_type": file_ext,
2806
+ "content_preview": cat_output[:2000], # First 2000 chars
2807
+ "structure": columns_info,
2808
+ "full_content": cat_output # Full content for analysis
2809
+ }
2810
+ tools_used.append("file_read")
2811
+
2812
+ if debug_mode:
2813
+ print(f"🔍 FILE STRUCTURE: {columns_info}")
2814
+ else:
2815
+ api_results["file_context"] = {
2816
+ "error": f"Could not read file: {file_path}"
2817
+ }
2704
2818
 
2705
2819
  except Exception as e:
2706
2820
  if debug_mode:
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generic project detection - works with ANY IDE/project type
4
+ Not RStudio-specific - detects R, Python, Node, Jupyter, etc.
5
+ """
6
+
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional, Any
10
+ import glob
11
+
12
+
13
+ class ProjectDetector:
14
+ """Detects project type and provides context"""
15
+
16
+ def __init__(self, working_dir: Optional[str] = None):
17
+ self.working_dir = Path(working_dir or os.getcwd())
18
+
19
+ def detect_project(self) -> Optional[Dict[str, Any]]:
20
+ """
21
+ Detect what kind of project user is working in
22
+ Returns project info or None if not in a project
23
+ """
24
+ project_info = {
25
+ "type": None,
26
+ "name": None,
27
+ "recent_files": [],
28
+ "description": None
29
+ }
30
+
31
+ # Check for R project (.Rproj file OR 2+ .R files)
32
+ rproj_files = list(self.working_dir.glob("*.Rproj"))
33
+ r_files = list(self.working_dir.glob("*.R")) + list(self.working_dir.glob("*.Rmd"))
34
+
35
+ if rproj_files:
36
+ project_info["type"] = "R"
37
+ project_info["name"] = rproj_files[0].stem
38
+ project_info["recent_files"] = self._get_recent_files([".R", ".Rmd", ".qmd"])
39
+ project_info["description"] = f"R/RStudio project: {project_info['name']}"
40
+ return project_info
41
+ elif len(r_files) >= 2:
42
+ # R project without .Rproj file
43
+ project_info["type"] = "R"
44
+ project_info["name"] = self.working_dir.name
45
+ project_info["recent_files"] = self._get_recent_files([".R", ".Rmd", ".qmd"])
46
+ project_info["description"] = f"R project: {project_info['name']}"
47
+ return project_info
48
+
49
+ # Check for Python project
50
+ if (self.working_dir / "pyproject.toml").exists() or \
51
+ (self.working_dir / "setup.py").exists() or \
52
+ (self.working_dir / "requirements.txt").exists():
53
+ project_info["type"] = "Python"
54
+ project_info["name"] = self.working_dir.name
55
+ project_info["recent_files"] = self._get_recent_files([".py", ".ipynb"])
56
+ project_info["description"] = f"Python project: {project_info['name']}"
57
+ return project_info
58
+
59
+ # Check for Node.js project
60
+ if (self.working_dir / "package.json").exists():
61
+ project_info["type"] = "Node"
62
+ project_info["name"] = self.working_dir.name
63
+ project_info["recent_files"] = self._get_recent_files([".js", ".ts", ".jsx", ".tsx"])
64
+ project_info["description"] = f"Node.js project: {project_info['name']}"
65
+ return project_info
66
+
67
+ # Check for Jupyter/Data Science directory
68
+ ipynb_files = list(self.working_dir.glob("*.ipynb"))
69
+ if len(ipynb_files) >= 2: # 2+ notebooks = likely data science project
70
+ project_info["type"] = "Jupyter"
71
+ project_info["name"] = self.working_dir.name
72
+ project_info["recent_files"] = self._get_recent_files([".ipynb", ".py", ".csv"])
73
+ project_info["description"] = f"Jupyter/Data Science project: {project_info['name']}"
74
+ return project_info
75
+
76
+ # Check for Git repository
77
+ if (self.working_dir / ".git").exists():
78
+ project_info["type"] = "Git"
79
+ project_info["name"] = self.working_dir.name
80
+ # Get recent files of any code type
81
+ project_info["recent_files"] = self._get_recent_files([".py", ".js", ".R", ".java", ".cpp", ".rs"])
82
+ project_info["description"] = f"Git repository: {project_info['name']}"
83
+ return project_info
84
+
85
+ # Not in a recognized project
86
+ return None
87
+
88
+ def _get_recent_files(self, extensions: List[str], limit: int = 5) -> List[Dict[str, Any]]:
89
+ """Get recently modified files with given extensions"""
90
+ files = []
91
+
92
+ for ext in extensions:
93
+ for filepath in self.working_dir.glob(f"**/*{ext}"):
94
+ if filepath.is_file() and not any(part.startswith('.') for part in filepath.parts):
95
+ try:
96
+ mtime = filepath.stat().st_mtime
97
+ files.append({
98
+ "name": filepath.name,
99
+ "path": str(filepath),
100
+ "relative_path": str(filepath.relative_to(self.working_dir)),
101
+ "modified": mtime,
102
+ "extension": ext
103
+ })
104
+ except:
105
+ pass
106
+
107
+ # Sort by modification time, newest first
108
+ files.sort(key=lambda f: f["modified"], reverse=True)
109
+ return files[:limit]
110
+
111
+ def format_project_banner(self, project_info: Dict[str, Any]) -> str:
112
+ """Format project info as a nice banner"""
113
+ if not project_info or not project_info["type"]:
114
+ return ""
115
+
116
+ icon_map = {
117
+ "R": "📊",
118
+ "Python": "🐍",
119
+ "Node": "📦",
120
+ "Jupyter": "📓",
121
+ "Git": "📂"
122
+ }
123
+
124
+ icon = icon_map.get(project_info["type"], "📁")
125
+
126
+ banner = f"\n{icon} {project_info['description']}\n"
127
+
128
+ if project_info["recent_files"]:
129
+ banner += "📄 Recent files:\n"
130
+ for f in project_info["recent_files"][:3]:
131
+ banner += f" • {f['relative_path']}\n"
132
+
133
+ return banner
134
+
135
+ def get_project_context_for_llm(self, project_info: Dict[str, Any]) -> str:
136
+ """Get project context to add to LLM prompts"""
137
+ if not project_info or not project_info["type"]:
138
+ return ""
139
+
140
+ context = f"User is working in a {project_info['type']} project: {project_info['name']}\n"
141
+
142
+ if project_info["recent_files"]:
143
+ context += "Recent files:\n"
144
+ for f in project_info["recent_files"][:5]:
145
+ context += f"- {f['relative_path']}\n"
146
+
147
+ return context
148
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cite-agent
3
- Version: 1.3.4
3
+ Version: 1.3.6
4
4
  Summary: Terminal AI assistant for academic research with citation verification
5
5
  Home-page: https://github.com/Spectating101/cite-agent
6
6
  Author: Cite-Agent Team
@@ -1,17 +1,18 @@
1
1
  cite_agent/__init__.py,sha256=wAXV2v8nNOmIAd0rh8196ItBl9hHWBVOBl5Re4VB77I,1645
2
2
  cite_agent/__main__.py,sha256=6x3lltwG-iZHeQbN12rwvdkPDfd2Rmdk71tOOaC89Mw,179
3
- cite_agent/__version__.py,sha256=U6E-HRsrit7kgSGgIeTI2eMUeyUCny5DH8LDV4I1o0g,22
3
+ cite_agent/__version__.py,sha256=5ZbAQtod5QalTI1C2N07edlxplzG_Q2XvGOSyOok4uA,22
4
4
  cite_agent/account_client.py,sha256=yLuzhIJoIZuXHXGbaVMzDxRATQwcy-wiaLnUrDuwUhI,5725
5
5
  cite_agent/agent_backend_only.py,sha256=H4DH4hmKhT0T3rQLAb2xnnJVjxl3pOZaljL9r6JndFY,6314
6
6
  cite_agent/ascii_plotting.py,sha256=lk8BaECs6fmjtp4iH12G09-frlRehAN7HLhHt2crers,8570
7
7
  cite_agent/auth.py,sha256=YtoGXKwcLkZQbop37iYYL9BzRWBRPlt_D9p71VGViS4,9833
8
8
  cite_agent/backend_only_client.py,sha256=WqLF8x7aXTro2Q3ehqKMsdCg53s6fNk9Hy86bGxqmmw,2561
9
- cite_agent/cli.py,sha256=f0x19N4MB85XgfNtOPEtovLQO_qzTHsli5IpG8wra30,35029
9
+ cite_agent/cli.py,sha256=Qq0Gt7sNVR4R2ue10KFZElOPYrujBG9xTOqy2qetxL4,35562
10
10
  cite_agent/cli_conversational.py,sha256=RAmgRNRyB8gQ8QLvWU-Tt23j2lmA34rQNT5F3_7SOq0,11141
11
11
  cite_agent/cli_enhanced.py,sha256=EAaSw9qtiYRWUXF6_05T19GCXlz9cCSz6n41ASnXIPc,7407
12
12
  cite_agent/cli_workflow.py,sha256=4oS_jW9D8ylovXbEFdsyLQONt4o0xxR4Xatfcc4tnBs,11641
13
13
  cite_agent/dashboard.py,sha256=VGV5XQU1PnqvTsxfKMcue3j2ri_nvm9Be6O5aVays_w,10502
14
- cite_agent/enhanced_ai_agent.py,sha256=HfCotlyrtBRXx6ENa8pRnG_x0JZ5izoIE87VwgLWAhU,165207
14
+ cite_agent/enhanced_ai_agent.py,sha256=hOL17pDKQdD1MJZRXkjEBlqyNmTdA_pcxIkQEojysFM,172282
15
+ cite_agent/project_detector.py,sha256=fPl5cLTy_oyufqrQ7RJ5IRVdofZoPqDRaQXW6tRtBJc,6086
15
16
  cite_agent/rate_limiter.py,sha256=-0fXx8Tl4zVB4O28n9ojU2weRo-FBF1cJo9Z5jC2LxQ,10908
16
17
  cite_agent/session_manager.py,sha256=B0MXSOsXdhO3DlvTG7S8x6pmGlYEDvIZ-o8TZM23niQ,9444
17
18
  cite_agent/setup_config.py,sha256=3m2e3gw0srEWA0OygdRo64r-8HK5ohyXfct0c__CF3s,16817
@@ -22,7 +23,7 @@ cite_agent/updater.py,sha256=udoAAN4gBKAvKDV7JTh2FJO_jIhNk9bby4x6n188MEY,8458
22
23
  cite_agent/web_search.py,sha256=FZCuNO7MAITiOIbpPbJyt2bzbXPzQla-9amJpnMpW_4,6520
23
24
  cite_agent/workflow.py,sha256=a0YC0Mzz4or1C5t2gZcuJBQ0uMOZrooaI8eLu2kkI0k,15086
24
25
  cite_agent/workflow_integration.py,sha256=A9ua0DN5pRtuU0cAwrUTGvqt2SXKhEHQbrHx16EGnDM,10910
25
- cite_agent-1.3.4.dist-info/licenses/LICENSE,sha256=XJkyO4IymhSUniN1ENY6lLrL2729gn_rbRlFK6_Hi9M,1074
26
+ cite_agent-1.3.6.dist-info/licenses/LICENSE,sha256=XJkyO4IymhSUniN1ENY6lLrL2729gn_rbRlFK6_Hi9M,1074
26
27
  src/__init__.py,sha256=0eEpjRfjRjOTilP66y-AbGNslBsVYr_clE-bZUzsX7s,40
27
28
  src/services/__init__.py,sha256=pTGLCH_84mz4nGtYMwQES5w-LzoSulUtx_uuNM6r-LA,4257
28
29
  src/services/simple_enhanced_main.py,sha256=IJoOplCqcVUg3GvN_BRyAhpGrLm_WEPy2jmHcNCY6R0,9257
@@ -49,8 +50,8 @@ src/services/research_service/synthesizer.py,sha256=lCcu37PWhWVNphHKaJJDIC-JQ5OI
49
50
  src/services/search_service/__init__.py,sha256=UZFXdd7r6wietQ2kESXEyGffdfBbpghquecQde7auF4,137
50
51
  src/services/search_service/indexer.py,sha256=u3-uwdAfmahWWsdebDF9i8XIyp7YtUMIHzlmBLBnPPM,7252
51
52
  src/services/search_service/search_engine.py,sha256=S9HqQ_mk-8W4d4MUOgBbEGQGV29-eSuceSFvVb4Xk-k,12500
52
- cite_agent-1.3.4.dist-info/METADATA,sha256=AFuXC6Gvnkb1wUAuau1XbuD-xNVlJdmjoKBHyaG-0OM,12231
53
- cite_agent-1.3.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
54
- cite_agent-1.3.4.dist-info/entry_points.txt,sha256=bJ0u28nFIxQKH1PWQ2ak4PV-FAjhoxTC7YADEdDenFw,83
55
- cite_agent-1.3.4.dist-info/top_level.txt,sha256=TgOFqJTIy8vDZuOoYA2QgagkqZtfhM5Acvt_IsWzAKo,15
56
- cite_agent-1.3.4.dist-info/RECORD,,
53
+ cite_agent-1.3.6.dist-info/METADATA,sha256=Nw2biNkpNAmCACEe_2dJ5dGy-KvL7DtxfFuNhgNWIN4,12231
54
+ cite_agent-1.3.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
55
+ cite_agent-1.3.6.dist-info/entry_points.txt,sha256=bJ0u28nFIxQKH1PWQ2ak4PV-FAjhoxTC7YADEdDenFw,83
56
+ cite_agent-1.3.6.dist-info/top_level.txt,sha256=TgOFqJTIy8vDZuOoYA2QgagkqZtfhM5Acvt_IsWzAKo,15
57
+ cite_agent-1.3.6.dist-info/RECORD,,