cite-agent 1.3.5__py3-none-any.whl → 1.3.7__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 (37) hide show
  1. cite_agent/__version__.py +1 -1
  2. cite_agent/cli.py +22 -2
  3. cite_agent/enhanced_ai_agent.py +407 -82
  4. cite_agent/project_detector.py +148 -0
  5. {cite_agent-1.3.5.dist-info → cite_agent-1.3.7.dist-info}/METADATA +1 -1
  6. cite_agent-1.3.7.dist-info/RECORD +31 -0
  7. {cite_agent-1.3.5.dist-info → cite_agent-1.3.7.dist-info}/top_level.txt +0 -1
  8. cite_agent-1.3.5.dist-info/RECORD +0 -56
  9. src/__init__.py +0 -1
  10. src/services/__init__.py +0 -132
  11. src/services/auth_service/__init__.py +0 -3
  12. src/services/auth_service/auth_manager.py +0 -33
  13. src/services/graph/__init__.py +0 -1
  14. src/services/graph/knowledge_graph.py +0 -194
  15. src/services/llm_service/__init__.py +0 -5
  16. src/services/llm_service/llm_manager.py +0 -495
  17. src/services/paper_service/__init__.py +0 -5
  18. src/services/paper_service/openalex.py +0 -231
  19. src/services/performance_service/__init__.py +0 -1
  20. src/services/performance_service/rust_performance.py +0 -395
  21. src/services/research_service/__init__.py +0 -23
  22. src/services/research_service/chatbot.py +0 -2056
  23. src/services/research_service/citation_manager.py +0 -436
  24. src/services/research_service/context_manager.py +0 -1441
  25. src/services/research_service/conversation_manager.py +0 -597
  26. src/services/research_service/critical_paper_detector.py +0 -577
  27. src/services/research_service/enhanced_research.py +0 -121
  28. src/services/research_service/enhanced_synthesizer.py +0 -375
  29. src/services/research_service/query_generator.py +0 -777
  30. src/services/research_service/synthesizer.py +0 -1273
  31. src/services/search_service/__init__.py +0 -5
  32. src/services/search_service/indexer.py +0 -186
  33. src/services/search_service/search_engine.py +0 -342
  34. src/services/simple_enhanced_main.py +0 -287
  35. {cite_agent-1.3.5.dist-info → cite_agent-1.3.7.dist-info}/WHEEL +0 -0
  36. {cite_agent-1.3.5.dist-info → cite_agent-1.3.7.dist-info}/entry_points.txt +0 -0
  37. {cite_agent-1.3.5.dist-info → cite_agent-1.3.7.dist-info}/licenses/LICENSE +0 -0
@@ -89,6 +89,15 @@ class EnhancedNocturnalAgent:
89
89
  from .workflow import WorkflowManager
90
90
  self.workflow = WorkflowManager()
91
91
  self.last_paper_result = None # Track last paper mentioned for "save that"
92
+
93
+ # File context tracking (for pronoun resolution and multi-turn)
94
+ self.file_context = {
95
+ 'last_file': None, # Last file mentioned/read
96
+ 'last_directory': None, # Last directory mentioned/navigated
97
+ 'recent_files': [], # Last 5 files (for "those files")
98
+ 'recent_dirs': [], # Last 5 directories
99
+ 'current_cwd': None, # Track shell's current directory
100
+ }
92
101
  try:
93
102
  self.per_user_token_limit = int(os.getenv("GROQ_PER_USER_TOKENS", 50000))
94
103
  except (TypeError, ValueError):
@@ -1698,25 +1707,20 @@ class EnhancedNocturnalAgent:
1698
1707
 
1699
1708
  elif response.status == 503:
1700
1709
  # 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
1710
+ # Auto-retry silently with exponential backoff
1704
1711
 
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... ")
1712
+ print("\n💭 Thinking... (backend is busy, retrying automatically)")
1713
+
1714
+ import asyncio
1715
+ retry_delays = [5, 15, 30] # Exponential backoff
1716
+
1717
+ for retry_num, delay in enumerate(retry_delays):
1718
+ await asyncio.sleep(delay)
1715
1719
 
1716
1720
  # Retry the request
1717
1721
  async with self.session.post(url, json=payload, headers=headers, timeout=60) as retry_response:
1718
1722
  if retry_response.status == 200:
1719
- # Success after retry!
1723
+ # Success!
1720
1724
  data = await retry_response.json()
1721
1725
  response_text = data.get('response', '')
1722
1726
  tokens = data.get('tokens_used', 0)
@@ -1739,17 +1743,19 @@ class EnhancedNocturnalAgent:
1739
1743
  return ChatResponse(
1740
1744
  response=response_text,
1741
1745
  tokens_used=tokens,
1742
- model_used=data.get('model'),
1743
- sources=data.get('sources', [])
1746
+ tools_used=all_tools,
1747
+ model=data.get('model', 'llama-3.3-70b'),
1748
+ timestamp=data.get('timestamp', datetime.now(timezone.utc).isoformat()),
1749
+ api_results=api_results
1744
1750
  )
1745
1751
  elif retry_response.status != 503:
1746
1752
  # Different error, stop retrying
1747
1753
  break
1748
1754
 
1749
- # All retries failed
1755
+ # All retries exhausted
1750
1756
  return ChatResponse(
1751
- response="❌ Service still unavailable after retries. Please try again in a few minutes.",
1752
- error_message="Service temporarily unavailable after retries"
1757
+ response="❌ Service unavailable. Please try again in a few minutes.",
1758
+ error_message="Service unavailable after retries"
1753
1759
  )
1754
1760
 
1755
1761
  elif response.status == 200:
@@ -1953,14 +1959,17 @@ class EnhancedNocturnalAgent:
1953
1959
  url = f"{self.finsight_base_url}/{endpoint}"
1954
1960
  # Start fresh with headers - don't use _default_headers which might be wrong
1955
1961
  headers = {}
1956
-
1962
+
1957
1963
  # Always use demo key for FinSight (SEC data is public)
1958
1964
  headers["X-API-Key"] = "demo-key-123"
1959
-
1965
+
1966
+ # Mark request as agent-mediated for product separation
1967
+ headers["X-Request-Source"] = "agent"
1968
+
1960
1969
  # Also add JWT if we have it
1961
1970
  if self.auth_token:
1962
1971
  headers["Authorization"] = f"Bearer {self.auth_token}"
1963
-
1972
+
1964
1973
  debug_mode = os.getenv("NOCTURNAL_DEBUG", "").lower() == "1"
1965
1974
  if debug_mode:
1966
1975
  print(f"🔍 FinSight headers: {list(headers.keys())}, X-API-Key={headers.get('X-API-Key')}")
@@ -2186,36 +2195,80 @@ class EnhancedNocturnalAgent:
2186
2195
  except Exception as e:
2187
2196
  return f"ERROR: {e}"
2188
2197
 
2189
- def _is_safe_shell_command(self, cmd: str) -> bool:
2198
+ def _classify_command_safety(self, cmd: str) -> str:
2190
2199
  """
2191
- Minimal safety check - only block truly catastrophic commands.
2192
- Philosophy: This is the user's machine. They can do anything in terminal anyway.
2193
- We only block commands that could cause immediate, irreversible system damage.
2200
+ Classify command by safety level for smart execution.
2201
+ Returns: 'SAFE', 'WRITE', 'DANGEROUS', or 'BLOCKED'
2194
2202
  """
2195
2203
  cmd = cmd.strip()
2196
2204
  if not cmd:
2197
- return False
2198
-
2199
- # Block ONLY truly catastrophic commands
2205
+ return 'BLOCKED'
2206
+
2207
+ cmd_lower = cmd.lower()
2208
+ cmd_parts = cmd.split()
2209
+ cmd_base = cmd_parts[0] if cmd_parts else ''
2210
+ cmd_with_sub = ' '.join(cmd_parts[:2]) if len(cmd_parts) >= 2 else ''
2211
+
2212
+ # BLOCKED: Catastrophic commands
2200
2213
  nuclear_patterns = [
2201
- 'rm -rf /', # Wipe root filesystem
2202
- 'rm -rf ~/*', # Wipe home directory
2203
- 'dd if=/dev/zero of=/dev/sda', # Wipe disk
2204
- 'dd if=/dev/zero of=/dev/hda',
2205
- 'mkfs', # Format filesystem
2206
- 'fdisk', # Partition disk
2214
+ 'rm -rf /',
2215
+ 'rm -rf ~',
2216
+ 'rm -rf /*',
2217
+ 'dd if=/dev/zero',
2218
+ 'mkfs',
2219
+ 'fdisk',
2207
2220
  ':(){ :|:& };:', # Fork bomb
2208
- 'chmod -R 777 /', # Make everything executable
2221
+ 'chmod -r 777 /',
2222
+ '> /dev/sda',
2209
2223
  ]
2210
-
2211
- cmd_lower = cmd.lower()
2212
2224
  for pattern in nuclear_patterns:
2213
- if pattern.lower() in cmd_lower:
2214
- return False
2215
-
2216
- # Allow everything else - pip, npm, git, pipes, redirection, etc.
2217
- # User asked for it, user gets it. Just like Cursor.
2218
- return True
2225
+ if pattern in cmd_lower:
2226
+ return 'BLOCKED'
2227
+
2228
+ # SAFE: Read-only commands
2229
+ safe_commands = {
2230
+ 'pwd', 'ls', 'cd', 'cat', 'head', 'tail', 'grep', 'find', 'which', 'type',
2231
+ 'wc', 'diff', 'echo', 'ps', 'top', 'df', 'du', 'file', 'stat', 'tree',
2232
+ 'whoami', 'hostname', 'date', 'cal', 'uptime', 'printenv', 'env',
2233
+ }
2234
+ safe_git = {'git status', 'git log', 'git diff', 'git branch', 'git show', 'git remote'}
2235
+
2236
+ if cmd_base in safe_commands or cmd_with_sub in safe_git:
2237
+ return 'SAFE'
2238
+
2239
+ # WRITE: File creation/modification (allowed but tracked)
2240
+ write_commands = {'mkdir', 'touch', 'cp', 'mv', 'tee'}
2241
+ if cmd_base in write_commands:
2242
+ return 'WRITE'
2243
+
2244
+ # WRITE: Redirection operations (echo > file, cat > file)
2245
+ if '>' in cmd or '>>' in cmd:
2246
+ # Allow redirection to regular files, block to devices
2247
+ if '/dev/' not in cmd_lower:
2248
+ return 'WRITE'
2249
+ else:
2250
+ return 'BLOCKED'
2251
+
2252
+ # DANGEROUS: Deletion and permission changes
2253
+ dangerous_commands = {'rm', 'rmdir', 'chmod', 'chown', 'chgrp'}
2254
+ if cmd_base in dangerous_commands:
2255
+ return 'DANGEROUS'
2256
+
2257
+ # WRITE: Git write operations
2258
+ write_git = {'git add', 'git commit', 'git push', 'git pull', 'git checkout', 'git merge'}
2259
+ if cmd_with_sub in write_git:
2260
+ return 'WRITE'
2261
+
2262
+ # Default: Treat unknown commands as requiring user awareness
2263
+ return 'WRITE'
2264
+
2265
+ def _is_safe_shell_command(self, cmd: str) -> bool:
2266
+ """
2267
+ Compatibility wrapper for old safety check.
2268
+ Now uses tiered classification system.
2269
+ """
2270
+ classification = self._classify_command_safety(cmd)
2271
+ return classification in ['SAFE', 'WRITE'] # Allow SAFE and WRITE, block DANGEROUS and BLOCKED
2219
2272
 
2220
2273
  def _check_token_budget(self, estimated_tokens: int) -> bool:
2221
2274
  """Check if we have enough token budget"""
@@ -2453,12 +2506,42 @@ class EnhancedNocturnalAgent:
2453
2506
  async def _analyze_request_type(self, question: str) -> Dict[str, Any]:
2454
2507
  """Analyze what type of request this is and what APIs to use"""
2455
2508
 
2456
- # Financial indicators
2509
+ # Financial indicators - COMPREHENSIVE list to ensure FinSight is used
2457
2510
  financial_keywords = [
2458
- 'financial', 'revenue', 'profit', 'earnings', 'stock', 'market',
2459
- 'ticker', 'company', 'balance sheet', 'income statement', 'cash flow',
2460
- 'valuation', 'pe ratio', 'debt', 'equity', 'dividend', 'growth',
2461
- 'ceo', 'earnings call', 'quarterly', 'annual report'
2511
+ # Core metrics
2512
+ 'financial', 'revenue', 'sales', 'income', 'profit', 'earnings', 'loss',
2513
+ 'net income', 'operating income', 'gross profit', 'ebitda', 'ebit',
2514
+
2515
+ # Margins & Ratios
2516
+ 'margin', 'gross margin', 'profit margin', 'operating margin', 'net margin', 'ebitda margin',
2517
+ 'ratio', 'current ratio', 'quick ratio', 'debt ratio', 'pe ratio', 'p/e',
2518
+ 'roe', 'roa', 'roic', 'roce', 'eps',
2519
+
2520
+ # Balance Sheet
2521
+ 'assets', 'liabilities', 'equity', 'debt', 'cash', 'capital',
2522
+ 'balance sheet', 'total assets', 'current assets', 'fixed assets',
2523
+ 'shareholders equity', 'stockholders equity', 'retained earnings',
2524
+
2525
+ # Cash Flow
2526
+ 'cash flow', 'fcf', 'free cash flow', 'operating cash flow',
2527
+ 'cfo', 'cfi', 'cff', 'capex', 'capital expenditure',
2528
+
2529
+ # Market Metrics
2530
+ 'stock', 'market cap', 'market capitalization', 'enterprise value',
2531
+ 'valuation', 'price', 'share price', 'stock price', 'quote',
2532
+ 'volume', 'trading volume', 'shares outstanding',
2533
+
2534
+ # Financial Statements
2535
+ 'income statement', '10-k', '10-q', '8-k', 'filing', 'sec filing',
2536
+ 'quarterly', 'annual report', 'earnings report', 'financial statement',
2537
+
2538
+ # Company Info
2539
+ 'ticker', 'company', 'corporation', 'ceo', 'earnings call',
2540
+ 'dividend', 'dividend yield', 'payout ratio',
2541
+
2542
+ # Growth & Performance
2543
+ 'growth', 'yoy', 'year over year', 'qoq', 'quarter over quarter',
2544
+ 'cagr', 'trend', 'performance', 'returns'
2462
2545
  ]
2463
2546
 
2464
2547
  # Research indicators (quantitative)
@@ -2667,30 +2750,68 @@ class EnhancedNocturnalAgent:
2667
2750
  # Quick check if query might need shell
2668
2751
  question_lower = request.question.lower()
2669
2752
  might_need_shell = any(word in question_lower for word in [
2670
- 'directory', 'folder', 'where', 'find', 'list', 'files', 'look', 'search', 'check', 'into'
2753
+ 'directory', 'folder', 'where', 'find', 'list', 'files', 'file', 'look', 'search', 'check', 'into',
2754
+ 'show', 'open', 'read', 'display', 'cat', 'view', 'contents', '.r', '.py', '.csv', '.ipynb',
2755
+ 'create', 'make', 'mkdir', 'touch', 'new', 'write', 'copy', 'move', 'delete', 'remove',
2756
+ 'git', 'grep', 'navigate', 'go to', 'change to'
2671
2757
  ])
2672
2758
 
2673
2759
  if might_need_shell and self.shell_session:
2760
+ # Get current directory and context for intelligent planning
2761
+ try:
2762
+ current_dir = self.execute_command("pwd").strip()
2763
+ self.file_context['current_cwd'] = current_dir
2764
+ except:
2765
+ current_dir = "~"
2766
+
2767
+ last_file = self.file_context.get('last_file') or 'None'
2768
+ last_dir = self.file_context.get('last_directory') or 'None'
2769
+
2674
2770
  # Ask LLM planner: What shell command should we run?
2675
- planner_prompt = f"""You are a shell command planner. Determine what shell command to run.
2771
+ planner_prompt = f"""You are a shell command planner. Determine what shell command to run, if any.
2676
2772
 
2677
2773
  User query: "{request.question}"
2678
2774
  Previous conversation: {json.dumps(self.conversation_history[-2:]) if self.conversation_history else "None"}
2775
+ Current directory: {current_dir}
2776
+ Last file mentioned: {last_file}
2777
+ Last directory mentioned: {last_dir}
2679
2778
 
2680
2779
  Respond ONLY with JSON:
2681
2780
  {{
2682
- "action": "pwd|ls|find|none",
2683
- "search_target": "cm522" (if find),
2684
- "search_path": "~/Downloads" (if find),
2685
- "target_path": "/full/path" (if ls on previous result)
2781
+ "action": "execute|none",
2782
+ "command": "pwd" (the actual shell command to run, if action=execute),
2783
+ "reason": "Show current directory" (why this command is needed),
2784
+ "updates_context": true (set to true if command changes files/directories)
2686
2785
  }}
2687
2786
 
2787
+ IMPORTANT RULES:
2788
+ 1. Return "none" for conversational queries ("hello", "test", "thanks", "how are you")
2789
+ 2. Return "none" when query is ambiguous without more context
2790
+ 3. Return "none" for questions about data that don't need shell (e.g., "Tesla revenue", "Apple stock price")
2791
+ 4. Use ACTUAL shell commands (pwd, ls, cd, mkdir, cat, grep, find, touch, etc.)
2792
+ 5. Resolve pronouns using context: "it"={last_file}, "there"/{last_dir}
2793
+ 6. For reading files, prefer: head -100 filename (shows first 100 lines)
2794
+ 7. For finding things, use: find ~ -maxdepth 4 -name '*pattern*' 2>/dev/null
2795
+ 8. For creating files: touch filename OR echo "content" > filename
2796
+ 9. For creating directories: mkdir dirname
2797
+ 10. ALWAYS include 2>/dev/null to suppress errors from find
2798
+
2688
2799
  Examples:
2689
- "where am i?" → {{"action": "pwd"}}
2690
- "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"}}
2693
- "Tesla revenue" → {{"action": "none"}}
2800
+ "where am i?" → {{"action": "execute", "command": "pwd", "reason": "Show current directory", "updates_context": false}}
2801
+ "list files" → {{"action": "execute", "command": "ls -lah", "reason": "List all files with details", "updates_context": false}}
2802
+ "find cm522" → {{"action": "execute", "command": "find ~ -maxdepth 4 -name '*cm522*' -type d 2>/dev/null | head -20", "reason": "Search for cm522 directory", "updates_context": false}}
2803
+ "go to Downloads" {{"action": "execute", "command": "cd ~/Downloads && pwd", "reason": "Navigate to Downloads directory", "updates_context": true}}
2804
+ "show me calc.R" → {{"action": "execute", "command": "head -100 calc.R", "reason": "Display file contents", "updates_context": true}}
2805
+ "create test directory" → {{"action": "execute", "command": "mkdir test && echo 'Created test/'", "reason": "Create new directory", "updates_context": true}}
2806
+ "create empty config.json" → {{"action": "execute", "command": "touch config.json && echo 'Created config.json'", "reason": "Create empty file", "updates_context": true}}
2807
+ "search for TODO in py files" → {{"action": "execute", "command": "grep -n 'TODO' *.py 2>/dev/null", "reason": "Find TODO comments", "updates_context": false}}
2808
+ "git status" → {{"action": "execute", "command": "git status", "reason": "Check repository status", "updates_context": false}}
2809
+ "what's in that file?" + last_file=data.csv → {{"action": "execute", "command": "head -100 data.csv", "reason": "Show file contents", "updates_context": false}}
2810
+ "hello" → {{"action": "none", "reason": "Conversational greeting, no command needed"}}
2811
+ "test" → {{"action": "none", "reason": "Ambiguous query, needs clarification"}}
2812
+ "thanks" → {{"action": "none", "reason": "Conversational acknowledgment"}}
2813
+ "Tesla revenue" → {{"action": "none", "reason": "Finance query, will use FinSight API not shell"}}
2814
+ "what does the error mean?" → {{"action": "none", "reason": "Explanation request, no command needed"}}
2694
2815
 
2695
2816
  JSON:"""
2696
2817
 
@@ -2708,17 +2829,82 @@ JSON:"""
2708
2829
 
2709
2830
  plan = json.loads(plan_text)
2710
2831
  shell_action = plan.get("action", "none")
2832
+ command = plan.get("command", "")
2833
+ reason = plan.get("reason", "")
2834
+ updates_context = plan.get("updates_context", False)
2711
2835
 
2712
2836
  if debug_mode:
2713
2837
  print(f"🔍 SHELL PLAN: {plan}")
2714
2838
 
2715
- # Execute shell command based on plan
2716
- if shell_action == "pwd":
2717
- pwd_output = self.execute_command("pwd")
2718
- api_results["shell_info"] = {"current_directory": pwd_output.strip()}
2719
- tools_used.append("shell_execution")
2839
+ # GENERIC COMMAND EXECUTION - No more hardcoded actions!
2840
+ if shell_action == "execute" and command:
2841
+ # Check command safety
2842
+ safety_level = self._classify_command_safety(command)
2843
+
2844
+ if debug_mode:
2845
+ print(f"🔍 Command: {command}")
2846
+ print(f"🔍 Safety: {safety_level}")
2847
+
2848
+ if safety_level == 'BLOCKED':
2849
+ api_results["shell_info"] = {
2850
+ "error": f"Command blocked for safety: {command}",
2851
+ "reason": "This command could cause system damage"
2852
+ }
2853
+ else:
2854
+ # Execute the command
2855
+ output = self.execute_command(command)
2856
+
2857
+ if not output.startswith("ERROR"):
2858
+ # Success - store results
2859
+ api_results["shell_info"] = {
2860
+ "command": command,
2861
+ "output": output,
2862
+ "reason": reason,
2863
+ "safety_level": safety_level
2864
+ }
2865
+ tools_used.append("shell_execution")
2866
+
2867
+ # Update file context if needed
2868
+ if updates_context:
2869
+ import re
2870
+ # Extract file paths from command
2871
+ file_patterns = r'([a-zA-Z0-9_\-./]+\.(py|r|csv|txt|json|md|ipynb|rmd))'
2872
+ files_mentioned = re.findall(file_patterns, command, re.IGNORECASE)
2873
+ if files_mentioned:
2874
+ file_path = files_mentioned[0][0]
2875
+ self.file_context['last_file'] = file_path
2876
+ if file_path not in self.file_context['recent_files']:
2877
+ self.file_context['recent_files'].append(file_path)
2878
+ self.file_context['recent_files'] = self.file_context['recent_files'][-5:] # Keep last 5
2879
+
2880
+ # Extract directory paths
2881
+ dir_patterns = r'cd\s+([^\s&|;]+)|mkdir\s+([^\s&|;]+)'
2882
+ dirs_mentioned = re.findall(dir_patterns, command)
2883
+ if dirs_mentioned:
2884
+ for dir_tuple in dirs_mentioned:
2885
+ dir_path = dir_tuple[0] or dir_tuple[1]
2886
+ if dir_path:
2887
+ self.file_context['last_directory'] = dir_path
2888
+ if dir_path not in self.file_context['recent_dirs']:
2889
+ self.file_context['recent_dirs'].append(dir_path)
2890
+ self.file_context['recent_dirs'] = self.file_context['recent_dirs'][-5:] # Keep last 5
2891
+
2892
+ # If cd command, update current_cwd
2893
+ if command.startswith('cd '):
2894
+ try:
2895
+ new_cwd = self.execute_command("pwd").strip()
2896
+ self.file_context['current_cwd'] = new_cwd
2897
+ except:
2898
+ pass
2899
+ else:
2900
+ # Command failed
2901
+ api_results["shell_info"] = {
2902
+ "error": output,
2903
+ "command": command
2904
+ }
2720
2905
 
2721
- elif shell_action == "ls":
2906
+ # Backwards compatibility: support old hardcoded actions if LLM still returns them
2907
+ elif shell_action == "pwd":
2722
2908
  target = plan.get("target_path")
2723
2909
  if target:
2724
2910
  ls_output = self.execute_command(f"ls -lah {target}")
@@ -2749,6 +2935,91 @@ JSON:"""
2749
2935
  "search_results": f"No directories matching '{search_target}' found in {search_path}"
2750
2936
  }
2751
2937
  tools_used.append("shell_execution")
2938
+
2939
+ elif shell_action == "cd":
2940
+ # NEW: Change directory
2941
+ target = plan.get("target_path")
2942
+ if target:
2943
+ # Expand ~ to home directory
2944
+ if target.startswith("~"):
2945
+ home = os.path.expanduser("~")
2946
+ target = target.replace("~", home, 1)
2947
+
2948
+ # Execute cd command
2949
+ cd_cmd = f"cd {target} && pwd"
2950
+ cd_output = self.execute_command(cd_cmd)
2951
+
2952
+ if not cd_output.startswith("ERROR"):
2953
+ api_results["shell_info"] = {
2954
+ "directory_changed": True,
2955
+ "new_directory": cd_output.strip(),
2956
+ "target_path": target
2957
+ }
2958
+ tools_used.append("shell_execution")
2959
+ else:
2960
+ api_results["shell_info"] = {
2961
+ "directory_changed": False,
2962
+ "error": f"Failed to change to {target}: {cd_output}"
2963
+ }
2964
+
2965
+ elif shell_action == "read_file":
2966
+ # NEW: Read and inspect file (R, Python, CSV, etc.)
2967
+ import re # Import at function level
2968
+
2969
+ file_path = plan.get("file_path", "")
2970
+ if not file_path and might_need_shell:
2971
+ # Try to infer from query (e.g., "show me calculate_betas.R")
2972
+ filenames = re.findall(r'([a-zA-Z0-9_-]+\.[a-zA-Z]{1,4})', request.question)
2973
+ if filenames:
2974
+ # Check if file exists in current directory
2975
+ pwd = self.execute_command("pwd").strip()
2976
+ file_path = f"{pwd}/{filenames[0]}"
2977
+
2978
+ if file_path:
2979
+ if debug_mode:
2980
+ print(f"🔍 READING FILE: {file_path}")
2981
+
2982
+ # Read file content (first 100 lines to detect structure)
2983
+ cat_output = self.execute_command(f"head -100 {file_path}")
2984
+
2985
+ if not cat_output.startswith("ERROR"):
2986
+ # Detect file type and extract structure
2987
+ file_ext = file_path.split('.')[-1].lower()
2988
+
2989
+ # Extract column/variable info based on file type
2990
+ columns_info = ""
2991
+ if file_ext in ['csv', 'tsv']:
2992
+ # CSV: first line is usually headers
2993
+ first_line = cat_output.split('\n')[0] if cat_output else ""
2994
+ columns_info = f"CSV columns: {first_line}"
2995
+ elif file_ext in ['r', 'rmd']:
2996
+ # R script: look for dataframe column references (df$columnname)
2997
+ column_refs = re.findall(r'\$(\w+)', cat_output)
2998
+ unique_cols = list(dict.fromkeys(column_refs))[:10]
2999
+ if unique_cols:
3000
+ columns_info = f"Detected columns/variables: {', '.join(unique_cols)}"
3001
+ elif file_ext == 'py':
3002
+ # Python: look for DataFrame['column'] or df.column
3003
+ column_refs = re.findall(r'\[[\'""](\w+)[\'"]\]|\.(\w+)', cat_output)
3004
+ unique_cols = list(dict.fromkeys([c[0] or c[1] for c in column_refs if c[0] or c[1]]))[:10]
3005
+ if unique_cols:
3006
+ columns_info = f"Detected columns/attributes: {', '.join(unique_cols)}"
3007
+
3008
+ api_results["file_context"] = {
3009
+ "file_path": file_path,
3010
+ "file_type": file_ext,
3011
+ "content_preview": cat_output[:2000], # First 2000 chars
3012
+ "structure": columns_info,
3013
+ "full_content": cat_output # Full content for analysis
3014
+ }
3015
+ tools_used.append("file_read")
3016
+
3017
+ if debug_mode:
3018
+ print(f"🔍 FILE STRUCTURE: {columns_info}")
3019
+ else:
3020
+ api_results["file_context"] = {
3021
+ "error": f"Could not read file: {file_path}"
3022
+ }
2752
3023
 
2753
3024
  except Exception as e:
2754
3025
  if debug_mode:
@@ -2770,6 +3041,14 @@ JSON:"""
2770
3041
  if debug_mode and is_vague:
2771
3042
  print(f"🔍 Query is VAGUE - skipping expensive APIs")
2772
3043
 
3044
+ # If query is vague, hint to backend LLM to ask clarifying questions
3045
+ if is_vague:
3046
+ api_results["query_analysis"] = {
3047
+ "is_vague": True,
3048
+ "suggestion": "Ask clarifying questions instead of guessing",
3049
+ "reason": "Query needs more specificity to provide accurate answer"
3050
+ }
3051
+
2773
3052
  # Skip Archive/FinSight if query is too vague, but still allow web search later
2774
3053
  if not is_vague:
2775
3054
  # Archive API for research
@@ -2848,32 +3127,78 @@ JSON:"""
2848
3127
  # - Shell said "none" (not a directory/file operation)
2849
3128
  # - We don't have enough data from Archive/FinSight
2850
3129
 
2851
- if self.web_search and shell_action == "none":
3130
+ # First check: Is this a conversational query that doesn't need web search?
3131
+ def is_conversational_query(query: str) -> bool:
3132
+ """Detect if query is conversational (greeting, thanks, testing, etc.)"""
3133
+ query_lower = query.lower().strip()
3134
+
3135
+ # Single word queries that are conversational
3136
+ conversational_words = {
3137
+ 'hello', 'hi', 'hey', 'thanks', 'thank', 'ok', 'okay', 'yes', 'no',
3138
+ 'test', 'testing', 'cool', 'nice', 'great', 'awesome', 'perfect',
3139
+ 'bye', 'goodbye', 'quit', 'exit', 'help'
3140
+ }
3141
+
3142
+ # Short conversational phrases
3143
+ conversational_phrases = [
3144
+ 'how are you', 'thank you', 'thanks!', 'ok', 'got it', 'i see',
3145
+ 'makes sense', 'sounds good', 'that works', 'no problem'
3146
+ ]
3147
+
3148
+ words = query_lower.split()
3149
+
3150
+ # Single word check
3151
+ if len(words) == 1 and words[0] in conversational_words:
3152
+ return True
3153
+
3154
+ # Short phrase check
3155
+ if len(words) <= 3 and any(phrase in query_lower for phrase in conversational_phrases):
3156
+ return True
3157
+
3158
+ # Question marks with no content words (just pronouns)
3159
+ if '?' in query_lower and len(words) <= 2:
3160
+ return True
3161
+
3162
+ return False
3163
+
3164
+ skip_web_search = is_conversational_query(request.question)
3165
+
3166
+ if self.web_search and shell_action == "none" and not skip_web_search:
2852
3167
  # Ask LLM: Should we web search for this?
2853
- web_decision_prompt = f"""Should we use web search for this query?
3168
+ web_decision_prompt = f"""You are a tool selection expert. Decide if web search is needed.
2854
3169
 
2855
3170
  User query: "{request.question}"
2856
3171
  Data already available: {list(api_results.keys())}
2857
- Shell action: {shell_action}
3172
+ Tools already used: {tools_used}
3173
+
3174
+ AVAILABLE TOOLS YOU SHOULD KNOW:
3175
+ 1. FinSight API: Company financial data (revenue, income, margins, ratios, cash flow, balance sheet, SEC filings)
3176
+ - Covers: All US public companies (~8,000)
3177
+ - Data: SEC EDGAR + Yahoo Finance
3178
+ - Metrics: 50+ financial KPIs
3179
+
3180
+ 2. Archive API: Academic research papers
3181
+ - Covers: Semantic Scholar, OpenAlex, PubMed
3182
+ - Data: Papers, citations, abstracts
3183
+
3184
+ 3. Web Search: General information, current events
3185
+ - Covers: Anything on the internet
3186
+ - Use for: Market share, industry news, non-financial company info
3187
+
3188
+ DECISION RULES:
3189
+ - If query is about company financials (revenue, profit, margins, etc.) → Check if FinSight already provided data
3190
+ - If FinSight has data in api_results → Web search is NOT needed
3191
+ - If FinSight was called but no data → Web search as fallback is OK
3192
+ - If query is about market share, industry size, trends → Web search (FinSight doesn't have this)
3193
+ - If query is about research papers → Archive handles it, not web
3194
+ - If query is conversational → Already filtered, you won't see these
2858
3195
 
2859
3196
  Respond with JSON:
2860
3197
  {{
2861
3198
  "use_web_search": true/false,
2862
- "reason": "why or why not"
3199
+ "reason": "explain why based on tools available and data already fetched"
2863
3200
  }}
2864
3201
 
2865
- Use web search for:
2866
- - Market share/size (not in SEC filings)
2867
- - Current prices (Bitcoin, commodities, real-time data)
2868
- - Industry data, statistics
2869
- - Recent events, news
2870
- - Questions not answered by existing data
2871
-
2872
- Don't use if:
2873
- - Shell already handled it (pwd/ls/find)
2874
- - Question answered by research/financial APIs
2875
- - Pure opinion question
2876
-
2877
3202
  JSON:"""
2878
3203
 
2879
3204
  try: