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.
- cite_agent/__version__.py +1 -1
- cite_agent/cli.py +22 -2
- cite_agent/enhanced_ai_agent.py +407 -82
- cite_agent/project_detector.py +148 -0
- {cite_agent-1.3.5.dist-info → cite_agent-1.3.7.dist-info}/METADATA +1 -1
- cite_agent-1.3.7.dist-info/RECORD +31 -0
- {cite_agent-1.3.5.dist-info → cite_agent-1.3.7.dist-info}/top_level.txt +0 -1
- cite_agent-1.3.5.dist-info/RECORD +0 -56
- src/__init__.py +0 -1
- src/services/__init__.py +0 -132
- src/services/auth_service/__init__.py +0 -3
- src/services/auth_service/auth_manager.py +0 -33
- src/services/graph/__init__.py +0 -1
- src/services/graph/knowledge_graph.py +0 -194
- src/services/llm_service/__init__.py +0 -5
- src/services/llm_service/llm_manager.py +0 -495
- src/services/paper_service/__init__.py +0 -5
- src/services/paper_service/openalex.py +0 -231
- src/services/performance_service/__init__.py +0 -1
- src/services/performance_service/rust_performance.py +0 -395
- src/services/research_service/__init__.py +0 -23
- src/services/research_service/chatbot.py +0 -2056
- src/services/research_service/citation_manager.py +0 -436
- src/services/research_service/context_manager.py +0 -1441
- src/services/research_service/conversation_manager.py +0 -597
- src/services/research_service/critical_paper_detector.py +0 -577
- src/services/research_service/enhanced_research.py +0 -121
- src/services/research_service/enhanced_synthesizer.py +0 -375
- src/services/research_service/query_generator.py +0 -777
- src/services/research_service/synthesizer.py +0 -1273
- src/services/search_service/__init__.py +0 -5
- src/services/search_service/indexer.py +0 -186
- src/services/search_service/search_engine.py +0 -342
- src/services/simple_enhanced_main.py +0 -287
- {cite_agent-1.3.5.dist-info → cite_agent-1.3.7.dist-info}/WHEEL +0 -0
- {cite_agent-1.3.5.dist-info → cite_agent-1.3.7.dist-info}/entry_points.txt +0 -0
- {cite_agent-1.3.5.dist-info → cite_agent-1.3.7.dist-info}/licenses/LICENSE +0 -0
cite_agent/enhanced_ai_agent.py
CHANGED
|
@@ -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
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
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
|
|
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
|
-
|
|
1743
|
-
|
|
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
|
|
1755
|
+
# All retries exhausted
|
|
1750
1756
|
return ChatResponse(
|
|
1751
|
-
response="❌ Service
|
|
1752
|
-
error_message="Service
|
|
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
|
|
2198
|
+
def _classify_command_safety(self, cmd: str) -> str:
|
|
2190
2199
|
"""
|
|
2191
|
-
|
|
2192
|
-
|
|
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
|
|
2198
|
-
|
|
2199
|
-
|
|
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 /',
|
|
2202
|
-
'rm -rf
|
|
2203
|
-
'
|
|
2204
|
-
'dd if=/dev/zero
|
|
2205
|
-
'mkfs',
|
|
2206
|
-
'fdisk',
|
|
2214
|
+
'rm -rf /',
|
|
2215
|
+
'rm -rf ~',
|
|
2216
|
+
'rm -rf /*',
|
|
2217
|
+
'dd if=/dev/zero',
|
|
2218
|
+
'mkfs',
|
|
2219
|
+
'fdisk',
|
|
2207
2220
|
':(){ :|:& };:', # Fork bomb
|
|
2208
|
-
'chmod -
|
|
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
|
|
2214
|
-
return
|
|
2215
|
-
|
|
2216
|
-
#
|
|
2217
|
-
|
|
2218
|
-
|
|
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
|
-
|
|
2459
|
-
'
|
|
2460
|
-
'
|
|
2461
|
-
|
|
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": "
|
|
2683
|
-
"
|
|
2684
|
-
"
|
|
2685
|
-
"
|
|
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
|
-
"
|
|
2691
|
-
"find cm522
|
|
2692
|
-
"
|
|
2693
|
-
"
|
|
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
|
-
#
|
|
2716
|
-
if shell_action == "
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"""
|
|
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
|
-
|
|
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
|
|
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:
|