cite-agent 1.3.5__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.5"
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,25 +1698,20 @@ class EnhancedNocturnalAgent:
1698
1698
 
1699
1699
  elif response.status == 503:
1700
1700
  # Backend AI service temporarily unavailable (Cerebras/Groq rate limited)
1701
- # Auto-retry with exponential backoff
1702
- max_retries = 3
1703
- retry_delays = [5, 15, 30] # seconds
1701
+ # Auto-retry silently with exponential backoff
1704
1702
 
1705
- for retry_num in range(max_retries):
1706
- delay = retry_delays[retry_num]
1707
- print(f"\n⏳ Hit rate limit. Waiting {delay} seconds before retry {retry_num + 1}/{max_retries}...")
1708
-
1709
- # Wait with countdown
1710
- import asyncio
1711
- for remaining in range(delay, 0, -1):
1712
- print(f"\r⏱️ Retrying in {remaining}s...", end='', flush=True)
1713
- await asyncio.sleep(1)
1714
- print("\r🔄 Retrying now... ")
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)
1715
1710
 
1716
1711
  # Retry the request
1717
1712
  async with self.session.post(url, json=payload, headers=headers, timeout=60) as retry_response:
1718
1713
  if retry_response.status == 200:
1719
- # Success after retry!
1714
+ # Success!
1720
1715
  data = await retry_response.json()
1721
1716
  response_text = data.get('response', '')
1722
1717
  tokens = data.get('tokens_used', 0)
@@ -1739,17 +1734,19 @@ class EnhancedNocturnalAgent:
1739
1734
  return ChatResponse(
1740
1735
  response=response_text,
1741
1736
  tokens_used=tokens,
1742
- model_used=data.get('model'),
1743
- sources=data.get('sources', [])
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
1744
1741
  )
1745
1742
  elif retry_response.status != 503:
1746
1743
  # Different error, stop retrying
1747
1744
  break
1748
1745
 
1749
- # All retries failed
1746
+ # All retries exhausted
1750
1747
  return ChatResponse(
1751
- response="❌ Service still unavailable after retries. Please try again in a few minutes.",
1752
- error_message="Service temporarily unavailable after retries"
1748
+ response="❌ Service unavailable. Please try again in a few minutes.",
1749
+ error_message="Service unavailable after retries"
1753
1750
  )
1754
1751
 
1755
1752
  elif response.status == 200:
@@ -2667,7 +2664,8 @@ class EnhancedNocturnalAgent:
2667
2664
  # Quick check if query might need shell
2668
2665
  question_lower = request.question.lower()
2669
2666
  might_need_shell = any(word in question_lower for word in [
2670
- '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'
2671
2669
  ])
2672
2670
 
2673
2671
  if might_need_shell and self.shell_session:
@@ -2679,19 +2677,28 @@ Previous conversation: {json.dumps(self.conversation_history[-2:]) if self.conve
2679
2677
 
2680
2678
  Respond ONLY with JSON:
2681
2679
  {{
2682
- "action": "pwd|ls|find|none",
2680
+ "action": "pwd|ls|find|read_file|none",
2683
2681
  "search_target": "cm522" (if find),
2684
2682
  "search_path": "~/Downloads" (if find),
2685
- "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)
2686
2685
  }}
2687
2686
 
2688
2687
  Examples:
2689
2688
  "where am i?" → {{"action": "pwd"}}
2690
2689
  "what files here?" → {{"action": "ls"}}
2691
- "find cm522 in downloads" → {{"action": "find", "search_target": "cm522", "search_path": "~/Downloads"}}
2692
- "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)
2693
2698
  "Tesla revenue" → {{"action": "none"}}
2694
2699
 
2700
+ KEY: If query mentions a specific FILENAME (*.R, *.py, *.csv), use read_file, NOT find!
2701
+
2695
2702
  JSON:"""
2696
2703
 
2697
2704
  try:
@@ -2749,6 +2756,65 @@ JSON:"""
2749
2756
  "search_results": f"No directories matching '{search_target}' found in {search_path}"
2750
2757
  }
2751
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
+ }
2752
2818
 
2753
2819
  except Exception as e:
2754
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.5
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=tdqvkGH0OryRjjXzO3HS5DyYol-VTO9fC8m43nB2PgI,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=ha7cps216cNV59yLnQe2-K8Af9wXmSL7TokT3TX-JuY,167857
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.5.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.5.dist-info/METADATA,sha256=qCT90yGGiD7au0geLi4DZYVpOH2i7bWWNJb1tLZHryw,12231
53
- cite_agent-1.3.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
54
- cite_agent-1.3.5.dist-info/entry_points.txt,sha256=bJ0u28nFIxQKH1PWQ2ak4PV-FAjhoxTC7YADEdDenFw,83
55
- cite_agent-1.3.5.dist-info/top_level.txt,sha256=TgOFqJTIy8vDZuOoYA2QgagkqZtfhM5Acvt_IsWzAKo,15
56
- cite_agent-1.3.5.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,,