sigma-terminal 3.3.2__py3-none-any.whl → 3.4.1__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.
- sigma/__init__.py +5 -5
- sigma/app.py +570 -171
- sigma/charts.py +33 -8
- sigma/cli.py +11 -11
- sigma/config.py +175 -34
- sigma/llm.py +160 -13
- sigma/setup.py +16 -16
- sigma/tools.py +868 -3
- sigma_terminal-3.4.1.dist-info/METADATA +272 -0
- {sigma_terminal-3.3.2.dist-info → sigma_terminal-3.4.1.dist-info}/RECORD +13 -13
- sigma_terminal-3.3.2.dist-info/METADATA +0 -444
- {sigma_terminal-3.3.2.dist-info → sigma_terminal-3.4.1.dist-info}/WHEEL +0 -0
- {sigma_terminal-3.3.2.dist-info → sigma_terminal-3.4.1.dist-info}/entry_points.txt +0 -0
- {sigma_terminal-3.3.2.dist-info → sigma_terminal-3.4.1.dist-info}/licenses/LICENSE +0 -0
sigma/app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Sigma v3.
|
|
1
|
+
"""Sigma v3.4.1 - Finance Research Agent."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import os
|
|
@@ -17,13 +17,13 @@ from textual.containers import Container, Horizontal, Vertical, ScrollableContai
|
|
|
17
17
|
from textual.widgets import Footer, Input, RichLog, Static
|
|
18
18
|
from textual.suggester import Suggester
|
|
19
19
|
|
|
20
|
-
from .config import LLMProvider, get_settings, save_api_key, AVAILABLE_MODELS
|
|
20
|
+
from .config import LLMProvider, get_settings, save_api_key, AVAILABLE_MODELS, SigmaError, ErrorCode
|
|
21
21
|
from .llm import get_llm
|
|
22
22
|
from .tools import TOOLS, execute_tool
|
|
23
23
|
from .backtest import run_backtest, get_available_strategies, BACKTEST_TOOL
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
__version__ = "3.
|
|
26
|
+
__version__ = "3.4.1"
|
|
27
27
|
SIGMA = "σ"
|
|
28
28
|
|
|
29
29
|
# Common stock tickers for recognition
|
|
@@ -38,52 +38,156 @@ COMMON_TICKERS = {
|
|
|
38
38
|
"SPY", "QQQ", "IWM", "DIA", "VTI", "VOO", "VXX", "ARKK", "XLF", "XLK", "XLE",
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
41
|
+
|
|
42
|
+
def format_return(value: float, include_sign: bool = True) -> str:
|
|
43
|
+
"""Format a return value with color coding. Green for positive, red for negative."""
|
|
44
|
+
if value > 0:
|
|
45
|
+
sign = "+" if include_sign else ""
|
|
46
|
+
return f"[#22c55e]{sign}{value:.2f}%[/#22c55e]"
|
|
47
|
+
elif value < 0:
|
|
48
|
+
return f"[#ef4444]{value:.2f}%[/#ef4444]"
|
|
49
|
+
else:
|
|
50
|
+
return f"[dim]{value:.2f}%[/dim]"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def format_price_change(price: float, change: float, change_pct: float) -> str:
|
|
54
|
+
"""Format price with change indicators."""
|
|
55
|
+
if change >= 0:
|
|
56
|
+
arrow = "^"
|
|
57
|
+
color = "#22c55e"
|
|
58
|
+
sign = "+"
|
|
59
|
+
else:
|
|
60
|
+
arrow = "v"
|
|
61
|
+
color = "#ef4444"
|
|
62
|
+
sign = ""
|
|
63
|
+
return f"[bold]${price:.2f}[/bold] [{color}]{arrow} {sign}{change:.2f} ({sign}{change_pct:.2f}%)[/{color}]"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def format_metric(label: str, value: str, good: Optional[bool] = None) -> str:
|
|
67
|
+
"""Format a metric with optional good/bad coloring."""
|
|
68
|
+
if good is True:
|
|
69
|
+
return f"[dim]{label}:[/dim] [#22c55e]{value}[/#22c55e]"
|
|
70
|
+
elif good is False:
|
|
71
|
+
return f"[dim]{label}:[/dim] [#ef4444]{value}[/#ef4444]"
|
|
72
|
+
else:
|
|
73
|
+
return f"[dim]{label}:[/dim] [bold]{value}[/bold]"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Sigma animation frames - smooth color breathing like Claude Code
|
|
77
|
+
SIGMA_FRAMES = [
|
|
78
|
+
"[bold #1e3a8a]σ[/bold #1e3a8a]",
|
|
79
|
+
"[bold #1e40af]σ[/bold #1e40af]",
|
|
80
|
+
"[bold #2563eb]σ[/bold #2563eb]",
|
|
81
|
+
"[bold #3b82f6]σ[/bold #3b82f6]",
|
|
82
|
+
"[bold #60a5fa]σ[/bold #60a5fa]",
|
|
83
|
+
"[bold #93c5fd]σ[/bold #93c5fd]",
|
|
84
|
+
"[bold #bfdbfe]σ[/bold #bfdbfe]",
|
|
45
85
|
"[bold white]σ[/bold white]",
|
|
86
|
+
"[bold #bfdbfe]σ[/bold #bfdbfe]",
|
|
87
|
+
"[bold #93c5fd]σ[/bold #93c5fd]",
|
|
46
88
|
"[bold #60a5fa]σ[/bold #60a5fa]",
|
|
89
|
+
"[bold #3b82f6]σ[/bold #3b82f6]",
|
|
90
|
+
"[bold #2563eb]σ[/bold #2563eb]",
|
|
91
|
+
"[bold #1e40af]σ[/bold #1e40af]",
|
|
47
92
|
]
|
|
48
93
|
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
"
|
|
94
|
+
# Sigma pulse animation for tool calls (faster pulse)
|
|
95
|
+
SIGMA_PULSE_FRAMES = [
|
|
96
|
+
"[bold #22c55e]σ[/bold #22c55e]",
|
|
97
|
+
"[bold #4ade80]σ[/bold #4ade80]",
|
|
98
|
+
"[bold #86efac]σ[/bold #86efac]",
|
|
99
|
+
"[bold #bbf7d0]σ[/bold #bbf7d0]",
|
|
100
|
+
"[bold #86efac]σ[/bold #86efac]",
|
|
101
|
+
"[bold #4ade80]σ[/bold #4ade80]",
|
|
102
|
+
"[bold #22c55e]σ[/bold #22c55e]",
|
|
103
|
+
"[bold #16a34a]σ[/bold #16a34a]",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
# Tool call spinner frames - classic ASCII spinner
|
|
107
|
+
TOOL_SPINNER_FRAMES = [
|
|
108
|
+
"|", "/", "-", "\\"
|
|
52
109
|
]
|
|
53
110
|
|
|
54
111
|
# Welcome banner - clean design
|
|
55
112
|
WELCOME_BANNER = """
|
|
56
|
-
[bold
|
|
57
|
-
[bold
|
|
58
|
-
[bold
|
|
59
|
-
[bold
|
|
60
|
-
[bold
|
|
61
|
-
[bold
|
|
62
|
-
|
|
63
|
-
[bold cyan]Finance Research Agent[/bold cyan] [dim]v3.
|
|
113
|
+
[bold #3b82f6]███████╗██╗ ██████╗ ███╗ ███╗ █████╗ [/bold #3b82f6]
|
|
114
|
+
[bold #60a5fa]██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗[/bold #60a5fa]
|
|
115
|
+
[bold #93c5fd]███████╗██║██║ ███╗██╔████╔██║███████║[/bold #93c5fd]
|
|
116
|
+
[bold #60a5fa]╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║[/bold #60a5fa]
|
|
117
|
+
[bold #3b82f6]███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║[/bold #3b82f6]
|
|
118
|
+
[bold #1d4ed8]╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold #1d4ed8]
|
|
119
|
+
|
|
120
|
+
[bold cyan]Finance Research Agent[/bold cyan] [dim]v3.4.1[/dim]
|
|
64
121
|
"""
|
|
65
122
|
|
|
66
|
-
SYSTEM_PROMPT = """You are Sigma,
|
|
123
|
+
SYSTEM_PROMPT = """You are Sigma, an elite AI-powered Finance Research Agent. You combine the analytical rigor of a quantitative analyst, the market intuition of a seasoned portfolio manager, and the communication clarity of a top financial advisor.
|
|
67
124
|
|
|
68
125
|
CORE CAPABILITIES:
|
|
69
|
-
- Real-time market data
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
126
|
+
- Real-time market data (quotes, prices, volume) via yfinance and Polygon.io
|
|
127
|
+
- Chart generation (candlestick, line, technical, comparison charts)
|
|
128
|
+
- Fundamental analysis (financials, ratios, earnings, valuations, balance sheets)
|
|
129
|
+
- Technical analysis (RSI, MACD, Bollinger Bands, SMA/EMA, support/resistance)
|
|
130
|
+
- Valuation analysis (P/E, P/B, PEG, EV/EBITDA with fair value assessment)
|
|
131
|
+
- Risk metrics (volatility, VaR, max drawdown, Sharpe, Sortino, Beta, Alpha)
|
|
132
|
+
- Earnings analysis (EPS, upcoming dates, quarterly history, surprises)
|
|
133
|
+
- Dividend analysis (yield, payout ratio, growth, ex-dividend dates)
|
|
134
|
+
- Options analysis (put/call ratio, implied volatility, sentiment)
|
|
135
|
+
- Peer comparison (industry peers on key metrics)
|
|
136
|
+
- Backtesting (SMA crossover, RSI, MACD, Bollinger, momentum, breakout)
|
|
137
|
+
- Market overview with sector performance and economic indicators
|
|
138
|
+
- Insider trading and institutional activity tracking
|
|
139
|
+
- Financial news search and SEC filings analysis
|
|
140
|
+
|
|
141
|
+
CHART GENERATION:
|
|
142
|
+
- Use generate_stock_chart for single stock charts (candlestick, line, technical)
|
|
143
|
+
- Use generate_comparison_chart for comparing multiple stocks visually
|
|
144
|
+
- Charts are saved as PNG files - always mention the file path
|
|
145
|
+
|
|
146
|
+
RESPONSE PHILOSOPHY:
|
|
147
|
+
1. BE PROACTIVE: Anticipate follow-up questions and address them
|
|
148
|
+
2. BE THOROUGH: When analyzing, cover fundamentals + technicals + sentiment
|
|
149
|
+
3. BE ACTIONABLE: Always end with clear recommendations or next steps
|
|
150
|
+
4. BE DATA-DRIVEN: Cite specific numbers, dates, and percentages
|
|
151
|
+
5. BE HONEST: Acknowledge uncertainty and risks
|
|
152
|
+
|
|
153
|
+
RESPONSE FORMAT:
|
|
154
|
+
- Start with a 1-2 sentence executive summary (the key takeaway)
|
|
155
|
+
- Use structured sections with clear headers (## format)
|
|
156
|
+
- Present comparative data in markdown tables
|
|
157
|
+
- Highlight key metrics: **bold** for critical numbers
|
|
158
|
+
- For returns: indicate positive (+) or negative (-) clearly
|
|
159
|
+
- Use bullet points for easy scanning
|
|
160
|
+
- End with "**Bottom Line:**" or "**Recommendation:**"
|
|
161
|
+
|
|
162
|
+
RATING SYSTEM (when asked for recommendations):
|
|
163
|
+
- STRONG BUY [A+]: Exceptional opportunity, high conviction
|
|
164
|
+
- BUY [A]: Favorable outlook, solid fundamentals
|
|
165
|
+
- HOLD [B]: Fair value, wait for better entry
|
|
166
|
+
- SELL [C]: Concerns outweigh positives
|
|
167
|
+
- STRONG SELL [D]: Significant risks, avoid
|
|
168
|
+
|
|
169
|
+
DATA GATHERING RULES:
|
|
170
|
+
- ALWAYS use tools to fetch current data before answering stock questions
|
|
171
|
+
- Use multiple tools for comprehensive analysis (quote + technicals + fundamentals)
|
|
172
|
+
- Cross-reference data points for accuracy
|
|
173
|
+
- If a tool fails, try alternative approaches or acknowledge the limitation
|
|
174
|
+
|
|
175
|
+
PROACTIVE INSIGHTS:
|
|
176
|
+
When analyzing any stock, proactively mention:
|
|
177
|
+
- Recent earnings surprises or upcoming earnings dates
|
|
178
|
+
- Major analyst rating changes
|
|
179
|
+
- Unusual volume or price movements
|
|
180
|
+
- Relevant sector trends
|
|
181
|
+
- Key support/resistance levels
|
|
182
|
+
- Comparison to peers when relevant
|
|
183
|
+
|
|
184
|
+
FORBIDDEN:
|
|
185
|
+
- Never fabricate data or prices
|
|
186
|
+
- Never provide data without fetching it first
|
|
187
|
+
- Never give buy/sell advice without disclosure of risks
|
|
188
|
+
- Never claim certainty about future performance
|
|
189
|
+
|
|
190
|
+
Remember: Users trust you for professional-grade financial research. Exceed their expectations with every response."""
|
|
87
191
|
|
|
88
192
|
# Enhanced autocomplete suggestions with more variety
|
|
89
193
|
SUGGESTIONS = [
|
|
@@ -128,11 +232,27 @@ SUGGESTIONS = [
|
|
|
128
232
|
"insider trading for AAPL",
|
|
129
233
|
"institutional holders of NVDA",
|
|
130
234
|
"analyst recommendations for TSLA",
|
|
235
|
+
# Chart generation
|
|
236
|
+
"plot AAPL stock chart",
|
|
237
|
+
"chart NVDA candlestick",
|
|
238
|
+
"show me a chart of TSLA",
|
|
239
|
+
"compare AAPL MSFT GOOGL chart",
|
|
240
|
+
"technical chart for SPY",
|
|
241
|
+
"generate chart for AMZN",
|
|
242
|
+
# Advanced analysis
|
|
243
|
+
"valuation of AAPL",
|
|
244
|
+
"risk metrics for NVDA",
|
|
245
|
+
"is TSLA overvalued",
|
|
246
|
+
"dividend analysis of JNJ",
|
|
247
|
+
"options summary for SPY",
|
|
248
|
+
"peer comparison for NVDA",
|
|
249
|
+
"earnings analysis of AAPL",
|
|
131
250
|
# Natural language queries
|
|
132
251
|
"what should I know about AAPL",
|
|
133
|
-
"is NVDA overvalued",
|
|
134
252
|
"best tech stocks right now",
|
|
135
253
|
"should I buy TSLA",
|
|
254
|
+
"how risky is NVDA",
|
|
255
|
+
"what is the P/E of MSFT",
|
|
136
256
|
# Commands
|
|
137
257
|
"/help",
|
|
138
258
|
"/clear",
|
|
@@ -142,6 +262,29 @@ SUGGESTIONS = [
|
|
|
142
262
|
"/backtest",
|
|
143
263
|
]
|
|
144
264
|
|
|
265
|
+
# Extended action verbs for smart completion
|
|
266
|
+
ACTION_VERBS = [
|
|
267
|
+
"analyze", "compare", "show", "get", "what is", "tell me about",
|
|
268
|
+
"technical analysis", "fundamentals", "price", "quote", "chart",
|
|
269
|
+
"backtest", "insider trading", "institutional", "analyst", "earnings",
|
|
270
|
+
"financials", "valuation", "risk", "dividend", "options", "peers",
|
|
271
|
+
"news", "sector", "market", "portfolio"
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
# Ticker categories for smart suggestions
|
|
275
|
+
TICKER_CATEGORIES = {
|
|
276
|
+
"tech": ["AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "AMD", "INTC", "CRM", "ADBE"],
|
|
277
|
+
"finance": ["JPM", "BAC", "GS", "MS", "V", "MA", "BRK.B", "C", "WFC", "AXP"],
|
|
278
|
+
"healthcare": ["JNJ", "UNH", "PFE", "MRK", "ABBV", "LLY", "TMO", "ABT", "BMY", "GILD"],
|
|
279
|
+
"consumer": ["AMZN", "TSLA", "HD", "NKE", "MCD", "SBUX", "COST", "WMT", "TGT", "LOW"],
|
|
280
|
+
"energy": ["XOM", "CVX", "COP", "SLB", "EOG", "MPC", "PSX", "VLO", "OXY", "HAL"],
|
|
281
|
+
"etf": ["SPY", "QQQ", "IWM", "DIA", "VTI", "VOO", "VXX", "ARKK", "XLF", "XLK"],
|
|
282
|
+
"crypto": ["COIN", "MSTR", "RIOT", "MARA", "HUT"],
|
|
283
|
+
"ev": ["TSLA", "RIVN", "LCID", "NIO", "XPEV", "LI"],
|
|
284
|
+
"ai": ["NVDA", "AMD", "MSFT", "GOOGL", "META", "PLTR", "AI", "PATH", "SNOW"],
|
|
285
|
+
"semiconductor": ["NVDA", "AMD", "INTC", "AVGO", "QCOM", "TXN", "MU", "AMAT", "LRCX", "ASML"],
|
|
286
|
+
}
|
|
287
|
+
|
|
145
288
|
|
|
146
289
|
def extract_tickers(text: str) -> List[str]:
|
|
147
290
|
"""Extract stock tickers from text."""
|
|
@@ -167,34 +310,174 @@ def extract_tickers(text: str) -> List[str]:
|
|
|
167
310
|
|
|
168
311
|
|
|
169
312
|
class SigmaSuggester(Suggester):
|
|
170
|
-
"""
|
|
313
|
+
"""Smart autocomplete with fuzzy matching and context awareness."""
|
|
171
314
|
|
|
172
315
|
def __init__(self):
|
|
173
|
-
super().__init__(use_cache=
|
|
316
|
+
super().__init__(use_cache=False, case_sensitive=False)
|
|
317
|
+
self._build_suggestion_index()
|
|
318
|
+
|
|
319
|
+
def _build_suggestion_index(self):
|
|
320
|
+
"""Build an index of all possible suggestions for fast lookup."""
|
|
321
|
+
self.all_suggestions = []
|
|
322
|
+
|
|
323
|
+
# Add static suggestions
|
|
324
|
+
self.all_suggestions.extend(SUGGESTIONS)
|
|
325
|
+
|
|
326
|
+
# Generate dynamic suggestions for all tickers
|
|
327
|
+
for ticker in COMMON_TICKERS:
|
|
328
|
+
self.all_suggestions.extend([
|
|
329
|
+
f"analyze {ticker}",
|
|
330
|
+
f"technical analysis of {ticker}",
|
|
331
|
+
f"fundamentals of {ticker}",
|
|
332
|
+
f"price of {ticker}",
|
|
333
|
+
f"quote {ticker}",
|
|
334
|
+
f"insider trading {ticker}",
|
|
335
|
+
f"earnings {ticker}",
|
|
336
|
+
f"news {ticker}",
|
|
337
|
+
])
|
|
338
|
+
|
|
339
|
+
# Add category-based suggestions
|
|
340
|
+
for category, tickers in TICKER_CATEGORIES.items():
|
|
341
|
+
self.all_suggestions.append(f"best {category} stocks")
|
|
342
|
+
self.all_suggestions.append(f"{category} sector performance")
|
|
343
|
+
if len(tickers) >= 3:
|
|
344
|
+
self.all_suggestions.append(f"compare {' '.join(tickers[:3])}")
|
|
345
|
+
|
|
346
|
+
def _fuzzy_match(self, query: str, target: str) -> float:
|
|
347
|
+
"""Calculate fuzzy match score (0-1). Higher is better."""
|
|
348
|
+
query = query.lower()
|
|
349
|
+
target = target.lower()
|
|
350
|
+
|
|
351
|
+
# Exact prefix match is best
|
|
352
|
+
if target.startswith(query):
|
|
353
|
+
return 1.0 + len(query) / len(target)
|
|
354
|
+
|
|
355
|
+
# Check if all query chars appear in order
|
|
356
|
+
q_idx = 0
|
|
357
|
+
matches = 0
|
|
358
|
+
consecutive = 0
|
|
359
|
+
max_consecutive = 0
|
|
360
|
+
last_match = -1
|
|
361
|
+
|
|
362
|
+
for t_idx, char in enumerate(target):
|
|
363
|
+
if q_idx < len(query) and char == query[q_idx]:
|
|
364
|
+
matches += 1
|
|
365
|
+
if last_match == t_idx - 1:
|
|
366
|
+
consecutive += 1
|
|
367
|
+
max_consecutive = max(max_consecutive, consecutive)
|
|
368
|
+
else:
|
|
369
|
+
consecutive = 1
|
|
370
|
+
last_match = t_idx
|
|
371
|
+
q_idx += 1
|
|
372
|
+
|
|
373
|
+
if q_idx < len(query):
|
|
374
|
+
# Not all chars matched
|
|
375
|
+
# Try substring match
|
|
376
|
+
if query in target:
|
|
377
|
+
return 0.5
|
|
378
|
+
return 0
|
|
379
|
+
|
|
380
|
+
# Score based on match quality
|
|
381
|
+
score = (matches / len(query)) * 0.5 + (max_consecutive / len(query)) * 0.3
|
|
382
|
+
# Bonus for shorter targets (more relevant)
|
|
383
|
+
score += (1 - len(target) / 100) * 0.2
|
|
384
|
+
|
|
385
|
+
return score
|
|
386
|
+
|
|
387
|
+
def _get_context_suggestions(self, value: str) -> List[str]:
|
|
388
|
+
"""Get context-aware suggestions based on partial input."""
|
|
389
|
+
suggestions = []
|
|
390
|
+
value_lower = value.lower().strip()
|
|
391
|
+
|
|
392
|
+
# Command shortcuts
|
|
393
|
+
if value.startswith("/"):
|
|
394
|
+
cmd = value_lower[1:]
|
|
395
|
+
commands = ["/help", "/clear", "/keys", "/models", "/status", "/backtest",
|
|
396
|
+
"/provider", "/model", "/setkey", "/tickers"]
|
|
397
|
+
for c in commands:
|
|
398
|
+
if c[1:].startswith(cmd):
|
|
399
|
+
suggestions.append(c)
|
|
400
|
+
return suggestions
|
|
401
|
+
|
|
402
|
+
# Detect if user is typing a ticker
|
|
403
|
+
words = value.split()
|
|
404
|
+
last_word = words[-1] if words else ""
|
|
405
|
+
|
|
406
|
+
if last_word.startswith("$") or (last_word.isupper() and len(last_word) >= 1):
|
|
407
|
+
ticker_prefix = last_word.lstrip("$").upper()
|
|
408
|
+
matching_tickers = [t for t in COMMON_TICKERS if t.startswith(ticker_prefix)]
|
|
409
|
+
|
|
410
|
+
# If we have an action verb, complete with ticker
|
|
411
|
+
action_words = ["analyze", "compare", "technical", "price", "quote",
|
|
412
|
+
"fundamentals", "insider", "earnings", "news"]
|
|
413
|
+
prefix = " ".join(words[:-1]).lower() if len(words) > 1 else ""
|
|
414
|
+
|
|
415
|
+
for ticker in matching_tickers[:5]:
|
|
416
|
+
if prefix:
|
|
417
|
+
suggestions.append(f"{prefix} {ticker}")
|
|
418
|
+
else:
|
|
419
|
+
suggestions.append(f"analyze {ticker}")
|
|
420
|
+
return suggestions
|
|
421
|
+
|
|
422
|
+
# Natural language patterns
|
|
423
|
+
patterns = [
|
|
424
|
+
("ana", "analyze"),
|
|
425
|
+
("tech", "technical analysis of"),
|
|
426
|
+
("fun", "fundamentals of"),
|
|
427
|
+
("comp", "compare"),
|
|
428
|
+
("back", "backtest"),
|
|
429
|
+
("mark", "market overview"),
|
|
430
|
+
("sect", "sector performance"),
|
|
431
|
+
("pri", "price of"),
|
|
432
|
+
("quo", "quote"),
|
|
433
|
+
("ins", "insider trading"),
|
|
434
|
+
("ear", "earnings"),
|
|
435
|
+
("wha", "what should I know about"),
|
|
436
|
+
("sho", "should I buy"),
|
|
437
|
+
("is ", "is NVDA overvalued"),
|
|
438
|
+
("how", "how is"),
|
|
439
|
+
("bes", "best tech stocks"),
|
|
440
|
+
]
|
|
441
|
+
|
|
442
|
+
for prefix, expansion in patterns:
|
|
443
|
+
if value_lower.startswith(prefix):
|
|
444
|
+
if expansion.endswith("of") or expansion.endswith("about"):
|
|
445
|
+
# Add popular tickers
|
|
446
|
+
for ticker in ["AAPL", "NVDA", "MSFT", "TSLA", "GOOGL"]:
|
|
447
|
+
suggestions.append(f"{expansion} {ticker}")
|
|
448
|
+
else:
|
|
449
|
+
suggestions.append(expansion)
|
|
450
|
+
|
|
451
|
+
return suggestions
|
|
174
452
|
|
|
175
453
|
async def get_suggestion(self, value: str) -> Optional[str]:
|
|
176
|
-
"""Get autocomplete suggestion."""
|
|
177
|
-
if not value or len(value) <
|
|
454
|
+
"""Get the best autocomplete suggestion."""
|
|
455
|
+
if not value or len(value) < 1:
|
|
178
456
|
return None
|
|
179
457
|
|
|
180
|
-
value_lower = value.lower()
|
|
458
|
+
value_lower = value.lower().strip()
|
|
459
|
+
|
|
460
|
+
# Get context-aware suggestions first
|
|
461
|
+
context_suggestions = self._get_context_suggestions(value)
|
|
462
|
+
if context_suggestions:
|
|
463
|
+
return context_suggestions[0]
|
|
181
464
|
|
|
182
|
-
#
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
465
|
+
# Fuzzy match against all suggestions
|
|
466
|
+
scored = []
|
|
467
|
+
for suggestion in self.all_suggestions:
|
|
468
|
+
score = self._fuzzy_match(value_lower, suggestion)
|
|
469
|
+
if score > 0:
|
|
470
|
+
scored.append((score, suggestion))
|
|
188
471
|
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
if suggestion.lower().startswith(value_lower):
|
|
192
|
-
return suggestion
|
|
472
|
+
# Sort by score and return best
|
|
473
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
193
474
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
475
|
+
if scored:
|
|
476
|
+
return scored[0][1]
|
|
477
|
+
|
|
478
|
+
# Last resort: if looks like a ticker, suggest analyze
|
|
479
|
+
if value.isupper() and len(value) <= 5 and value.isalpha():
|
|
480
|
+
return f"analyze {value}"
|
|
198
481
|
|
|
199
482
|
return None
|
|
200
483
|
|
|
@@ -230,8 +513,8 @@ Screen {
|
|
|
230
513
|
|
|
231
514
|
#status-bar {
|
|
232
515
|
height: 3;
|
|
233
|
-
background: #
|
|
234
|
-
border-top: solid #
|
|
516
|
+
background: #0f1419;
|
|
517
|
+
border-top: solid #1e293b;
|
|
235
518
|
padding: 0 2;
|
|
236
519
|
dock: bottom;
|
|
237
520
|
}
|
|
@@ -256,9 +539,9 @@ Screen {
|
|
|
256
539
|
#tool-calls-display {
|
|
257
540
|
width: 100%;
|
|
258
541
|
height: auto;
|
|
259
|
-
max-height:
|
|
260
|
-
background: #
|
|
261
|
-
border:
|
|
542
|
+
max-height: 8;
|
|
543
|
+
background: #0f1419;
|
|
544
|
+
border: round #1e293b;
|
|
262
545
|
margin: 0 2;
|
|
263
546
|
padding: 0 1;
|
|
264
547
|
display: none;
|
|
@@ -271,7 +554,7 @@ Screen {
|
|
|
271
554
|
#input-area {
|
|
272
555
|
height: 5;
|
|
273
556
|
padding: 1 2;
|
|
274
|
-
background: #
|
|
557
|
+
background: #0f1419;
|
|
275
558
|
}
|
|
276
559
|
|
|
277
560
|
#input-row {
|
|
@@ -283,22 +566,24 @@ Screen {
|
|
|
283
566
|
width: 4;
|
|
284
567
|
height: 3;
|
|
285
568
|
content-align: center middle;
|
|
569
|
+
background: transparent;
|
|
286
570
|
}
|
|
287
571
|
|
|
288
572
|
#prompt-input {
|
|
289
573
|
width: 1fr;
|
|
290
|
-
background: #
|
|
291
|
-
border:
|
|
292
|
-
color: #
|
|
574
|
+
background: #1e293b;
|
|
575
|
+
border: tall #3b82f6;
|
|
576
|
+
color: #f8fafc;
|
|
293
577
|
padding: 0 1;
|
|
294
578
|
}
|
|
295
579
|
|
|
296
580
|
#prompt-input:focus {
|
|
297
|
-
border:
|
|
581
|
+
border: tall #60a5fa;
|
|
582
|
+
background: #1e3a5f;
|
|
298
583
|
}
|
|
299
584
|
|
|
300
585
|
#prompt-input.-autocomplete {
|
|
301
|
-
border:
|
|
586
|
+
border: tall #22c55e;
|
|
302
587
|
}
|
|
303
588
|
|
|
304
589
|
#ticker-highlight {
|
|
@@ -306,6 +591,7 @@ Screen {
|
|
|
306
591
|
height: 1;
|
|
307
592
|
padding: 0 1;
|
|
308
593
|
background: transparent;
|
|
594
|
+
color: #22d3ee;
|
|
309
595
|
}
|
|
310
596
|
|
|
311
597
|
Footer {
|
|
@@ -345,7 +631,7 @@ Footer > .footer--description {
|
|
|
345
631
|
|
|
346
632
|
|
|
347
633
|
class ToolCallDisplay(Static):
|
|
348
|
-
"""Animated display for tool calls."""
|
|
634
|
+
"""Animated display for tool calls - professional tool execution view."""
|
|
349
635
|
|
|
350
636
|
def __init__(self, *args, **kwargs):
|
|
351
637
|
super().__init__(*args, **kwargs)
|
|
@@ -355,11 +641,13 @@ class ToolCallDisplay(Static):
|
|
|
355
641
|
|
|
356
642
|
def add_tool_call(self, name: str, status: str = "running"):
|
|
357
643
|
"""Add a tool call to the display."""
|
|
358
|
-
|
|
644
|
+
# Format tool name nicely
|
|
645
|
+
display_name = name.replace("_", " ").title()
|
|
646
|
+
self.tool_calls.append({"name": name, "display": display_name, "status": status, "frame": 0})
|
|
359
647
|
self.add_class("visible")
|
|
360
|
-
self.
|
|
648
|
+
self._update_display()
|
|
361
649
|
if not self.timer:
|
|
362
|
-
self.timer = self.set_interval(0.
|
|
650
|
+
self.timer = self.set_interval(0.06, self._animate) # Faster animation
|
|
363
651
|
|
|
364
652
|
def complete_tool_call(self, name: str):
|
|
365
653
|
"""Mark a tool call as complete."""
|
|
@@ -367,7 +655,7 @@ class ToolCallDisplay(Static):
|
|
|
367
655
|
if tc["name"] == name and tc["status"] == "running":
|
|
368
656
|
tc["status"] = "complete"
|
|
369
657
|
break
|
|
370
|
-
self.
|
|
658
|
+
self._update_display()
|
|
371
659
|
|
|
372
660
|
def clear(self):
|
|
373
661
|
"""Clear all tool calls."""
|
|
@@ -380,52 +668,64 @@ class ToolCallDisplay(Static):
|
|
|
380
668
|
|
|
381
669
|
def _animate(self):
|
|
382
670
|
"""Animate the spinner."""
|
|
383
|
-
self.frame = (self.frame + 1) % len(
|
|
671
|
+
self.frame = (self.frame + 1) % len(TOOL_SPINNER_FRAMES)
|
|
384
672
|
for tc in self.tool_calls:
|
|
385
673
|
if tc["status"] == "running":
|
|
386
674
|
tc["frame"] = self.frame
|
|
387
|
-
self.
|
|
675
|
+
self._update_display()
|
|
388
676
|
|
|
389
|
-
def
|
|
390
|
-
"""
|
|
677
|
+
def _update_display(self):
|
|
678
|
+
"""Update the tool calls display content."""
|
|
391
679
|
if not self.tool_calls:
|
|
680
|
+
self.update("")
|
|
392
681
|
return
|
|
393
682
|
|
|
394
683
|
lines = []
|
|
395
684
|
for tc in self.tool_calls:
|
|
396
685
|
if tc["status"] == "running":
|
|
397
|
-
spinner =
|
|
398
|
-
lines.append(f" [
|
|
686
|
+
spinner = TOOL_SPINNER_FRAMES[tc["frame"] % len(TOOL_SPINNER_FRAMES)]
|
|
687
|
+
lines.append(f" [bold #60a5fa]{spinner}[/bold #60a5fa] [bold white]{tc['display']}[/bold white] [dim italic]running...[/dim italic]")
|
|
399
688
|
else:
|
|
400
|
-
lines.append(f" [
|
|
689
|
+
lines.append(f" [bold #22c55e][OK][/bold #22c55e] [bold white]{tc['display']}[/bold white] [#22c55e]done[/#22c55e]")
|
|
401
690
|
|
|
402
691
|
self.update(Text.from_markup("\n".join(lines)))
|
|
403
692
|
|
|
404
693
|
|
|
405
694
|
class SigmaIndicator(Static):
|
|
406
|
-
"""
|
|
695
|
+
"""Animated sigma indicator - typewriter style when active."""
|
|
407
696
|
|
|
408
697
|
def __init__(self, *args, **kwargs):
|
|
409
698
|
super().__init__(*args, **kwargs)
|
|
410
699
|
self.active = False
|
|
700
|
+
self.mode = "idle" # idle, thinking, tool
|
|
411
701
|
self.frame = 0
|
|
412
702
|
self.timer = None
|
|
413
703
|
|
|
414
704
|
def on_mount(self):
|
|
415
705
|
self.update(Text.from_markup(f"[bold blue]{SIGMA}[/bold blue]"))
|
|
416
706
|
|
|
417
|
-
def set_active(self, active: bool):
|
|
707
|
+
def set_active(self, active: bool, mode: str = "thinking"):
|
|
418
708
|
self.active = active
|
|
709
|
+
self.mode = mode if active else "idle"
|
|
419
710
|
if active and not self.timer:
|
|
420
|
-
self.
|
|
711
|
+
self.frame = 0
|
|
712
|
+
# Fast smooth animation - 0.05s for thinking, 0.04s for tool calls
|
|
713
|
+
interval = 0.05 if mode == "thinking" else 0.04
|
|
714
|
+
self.timer = self.set_interval(interval, self._animate)
|
|
421
715
|
elif not active and self.timer:
|
|
422
716
|
self.timer.stop()
|
|
423
717
|
self.timer = None
|
|
424
718
|
self.update(Text.from_markup(f"[bold blue]{SIGMA}[/bold blue]"))
|
|
425
719
|
|
|
426
|
-
def
|
|
427
|
-
|
|
428
|
-
|
|
720
|
+
def _animate(self):
|
|
721
|
+
if self.mode == "thinking":
|
|
722
|
+
# Typewriter effect: s -> si -> sig -> sigm -> sigma -> sigm -> ...
|
|
723
|
+
self.frame = (self.frame + 1) % len(SIGMA_FRAMES)
|
|
724
|
+
self.update(Text.from_markup(SIGMA_FRAMES[self.frame]))
|
|
725
|
+
else:
|
|
726
|
+
# Pulse effect for tool calls
|
|
727
|
+
self.frame = (self.frame + 1) % len(SIGMA_PULSE_FRAMES)
|
|
728
|
+
self.update(Text.from_markup(SIGMA_PULSE_FRAMES[self.frame]))
|
|
429
729
|
|
|
430
730
|
|
|
431
731
|
class TickerHighlight(Static):
|
|
@@ -538,20 +838,28 @@ class SigmaApp(App):
|
|
|
538
838
|
chat.write_welcome()
|
|
539
839
|
|
|
540
840
|
provider = getattr(self.settings.default_provider, 'value', str(self.settings.default_provider))
|
|
541
|
-
chat.write_system(f"{SIGMA} Provider: {provider} | Model: {self.settings.default_model}")
|
|
542
|
-
chat.write_system(f"{SIGMA} Type /help for commands •
|
|
841
|
+
chat.write_system(f"{SIGMA} Provider: [bold]{provider}[/bold] | Model: [bold]{self.settings.default_model}[/bold]")
|
|
842
|
+
chat.write_system(f"{SIGMA} Type [cyan]/help[/cyan] for commands • [cyan]/keys[/cyan] to set up API keys")
|
|
543
843
|
chat.write_system("")
|
|
544
844
|
|
|
545
845
|
self._init_llm()
|
|
546
846
|
self.query_one("#prompt-input", Input).focus()
|
|
547
847
|
|
|
548
848
|
def _init_llm(self):
|
|
849
|
+
"""Initialize the LLM client with proper error handling."""
|
|
549
850
|
try:
|
|
550
851
|
self.llm = get_llm(self.settings.default_provider, self.settings.default_model)
|
|
852
|
+
except SigmaError as e:
|
|
853
|
+
chat = self.query_one("#chat-log", ChatLog)
|
|
854
|
+
chat.write_error(f"[E{e.code}] {e.message}")
|
|
855
|
+
if e.details.get("hint"):
|
|
856
|
+
chat.write_system(f"[dim]Hint: {e.details['hint']}[/dim]")
|
|
857
|
+
self.llm = None
|
|
551
858
|
except Exception as e:
|
|
552
859
|
chat = self.query_one("#chat-log", ChatLog)
|
|
553
|
-
chat.write_error(f"
|
|
554
|
-
chat.write_system("Use /keys to configure API keys")
|
|
860
|
+
chat.write_error(f"[E{ErrorCode.PROVIDER_ERROR}] Failed to initialize: {str(e)[:100]}")
|
|
861
|
+
chat.write_system("[dim]Use /keys to configure API keys[/dim]")
|
|
862
|
+
self.llm = None
|
|
555
863
|
|
|
556
864
|
@on(Input.Changed)
|
|
557
865
|
def on_input_change(self, event: Input.Changed):
|
|
@@ -613,59 +921,58 @@ class SigmaApp(App):
|
|
|
613
921
|
def _show_comprehensive_help(self, chat: ChatLog):
|
|
614
922
|
"""Show comprehensive help with examples."""
|
|
615
923
|
help_text = f"""
|
|
616
|
-
[bold
|
|
617
|
-
[bold] {SIGMA}
|
|
618
|
-
[bold
|
|
619
|
-
|
|
620
|
-
[bold
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
[cyan]/
|
|
630
|
-
[cyan]/
|
|
631
|
-
[cyan]/
|
|
632
|
-
[cyan]/
|
|
633
|
-
[cyan]/
|
|
634
|
-
[cyan]/
|
|
635
|
-
[cyan]/
|
|
636
|
-
[cyan]/
|
|
637
|
-
[cyan]/
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
[bold
|
|
653
|
-
[bold]
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
[bold]
|
|
657
|
-
[bold]Ctrl+
|
|
658
|
-
[bold]
|
|
659
|
-
|
|
660
|
-
[bold
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
• Detected tickers shown next to input
|
|
924
|
+
[bold white on #1e3a8a] [/bold white on #1e3a8a]
|
|
925
|
+
[bold white on #1e3a8a] {SIGMA} S I G M A H E L P C E N T E R [/bold white on #1e3a8a]
|
|
926
|
+
[bold white on #1e3a8a] [/bold white on #1e3a8a]
|
|
927
|
+
|
|
928
|
+
[bold #3b82f6]GETTING STARTED[/bold #3b82f6]
|
|
929
|
+
Type naturally - Sigma understands finance queries:
|
|
930
|
+
[dim]>>[/dim] analyze AAPL [dim]Full company analysis[/dim]
|
|
931
|
+
[dim]>>[/dim] compare NVDA AMD INTC [dim]Side-by-side comparison[/dim]
|
|
932
|
+
[dim]>>[/dim] technical analysis SPY [dim]RSI, MACD, Bollinger Bands[/dim]
|
|
933
|
+
[dim]>>[/dim] plot TSLA chart [dim]Generate price chart[/dim]
|
|
934
|
+
[dim]>>[/dim] backtest SMA on AAPL [dim]Strategy simulation[/dim]
|
|
935
|
+
|
|
936
|
+
[bold #3b82f6]COMMANDS[/bold #3b82f6]
|
|
937
|
+
[cyan]/help[/cyan] Full help documentation
|
|
938
|
+
[cyan]/clear[/cyan] Clear conversation history
|
|
939
|
+
[cyan]/keys[/cyan] API key configuration
|
|
940
|
+
[cyan]/models[/cyan] Available AI models
|
|
941
|
+
[cyan]/status[/cyan] Current settings
|
|
942
|
+
[cyan]/backtest[/cyan] Backtesting strategies
|
|
943
|
+
[cyan]/provider[/cyan] [dim]<name>[/dim] Switch provider (google, openai, anthropic)
|
|
944
|
+
[cyan]/model[/cyan] [dim]<name>[/dim] Switch model
|
|
945
|
+
[cyan]/setkey[/cyan] [dim]<p> <key>[/dim] Set API key
|
|
946
|
+
[cyan]/tickers[/cyan] Popular ticker list
|
|
947
|
+
|
|
948
|
+
[bold #3b82f6]ANALYSIS CAPABILITIES[/bold #3b82f6]
|
|
949
|
+
[bold]Fundamental[/bold] financials, earnings, valuation, balance sheet
|
|
950
|
+
[bold]Technical[/bold] RSI, MACD, SMA/EMA, Bollinger, support/resistance
|
|
951
|
+
[bold]Sentiment[/bold] analyst ratings, insider trades, institutional holdings
|
|
952
|
+
[bold]Market[/bold] sector performance, economic indicators, market news
|
|
953
|
+
[bold]Charts[/bold] candlestick, line, technical, comparison charts
|
|
954
|
+
|
|
955
|
+
[bold #3b82f6]BACKTEST STRATEGIES[/bold #3b82f6]
|
|
956
|
+
[bold]sma_crossover[/bold] SMA 20/50 crossover signals
|
|
957
|
+
[bold]rsi[/bold] RSI mean reversion (30/70)
|
|
958
|
+
[bold]macd[/bold] MACD momentum signals
|
|
959
|
+
[bold]bollinger[/bold] Bollinger Bands bounce
|
|
960
|
+
[bold]momentum[/bold] Dual momentum strategy
|
|
961
|
+
[bold]breakout[/bold] Price breakout signals
|
|
962
|
+
|
|
963
|
+
[bold #3b82f6]KEYBOARD[/bold #3b82f6]
|
|
964
|
+
[bold]Tab[/bold] Smart autocomplete
|
|
965
|
+
[bold]Ctrl+L[/bold] Clear chat
|
|
966
|
+
[bold]Ctrl+M[/bold] Models menu
|
|
967
|
+
[bold]Ctrl+H[/bold] Quick help
|
|
968
|
+
[bold]Ctrl+P[/bold] Command palette
|
|
969
|
+
|
|
970
|
+
[dim]Returns: [/dim][#22c55e]+green = gain[/#22c55e][dim], [/dim][#ef4444]-red = loss[/#ef4444][dim] | Tickers auto-detected from input[/dim]
|
|
664
971
|
"""
|
|
665
972
|
chat.write(Panel(
|
|
666
973
|
Text.from_markup(help_text),
|
|
667
974
|
title=f"[bold cyan]{SIGMA} Help[/bold cyan]",
|
|
668
|
-
border_style="
|
|
975
|
+
border_style="#3b82f6",
|
|
669
976
|
padding=(0, 1),
|
|
670
977
|
))
|
|
671
978
|
|
|
@@ -687,37 +994,88 @@ class SigmaApp(App):
|
|
|
687
994
|
))
|
|
688
995
|
|
|
689
996
|
def _show_keys(self, chat: ChatLog):
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
997
|
+
"""Show comprehensive API key management interface."""
|
|
998
|
+
keys_help = f"""
|
|
999
|
+
[bold cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold cyan]
|
|
1000
|
+
[bold] {SIGMA} API KEY MANAGER [/bold]
|
|
1001
|
+
[bold cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold cyan]
|
|
1002
|
+
|
|
1003
|
+
[bold yellow]QUICK SETUP[/bold yellow]
|
|
1004
|
+
Set a key: [cyan]/setkey <provider> <your-api-key>[/cyan]
|
|
1005
|
+
|
|
1006
|
+
[bold yellow]LLM PROVIDERS[/bold yellow]
|
|
1007
|
+
[bold]google[/bold] → https://aistudio.google.com/apikey
|
|
1008
|
+
[bold]openai[/bold] → https://platform.openai.com/api-keys
|
|
1009
|
+
[bold]anthropic[/bold] → https://console.anthropic.com/settings/keys
|
|
1010
|
+
[bold]groq[/bold] → https://console.groq.com/keys [dim](free!)[/dim]
|
|
1011
|
+
[bold]xai[/bold] → https://console.x.ai
|
|
1012
|
+
|
|
1013
|
+
[bold yellow]DATA PROVIDERS[/bold yellow] [dim](optional - enhances data quality)[/dim]
|
|
1014
|
+
[bold]polygon[/bold] → https://polygon.io/dashboard/api-keys
|
|
1015
|
+
|
|
1016
|
+
[bold yellow]EXAMPLES[/bold yellow]
|
|
1017
|
+
/setkey google AIzaSyB...
|
|
1018
|
+
/setkey openai sk-proj-...
|
|
1019
|
+
/setkey polygon abc123...
|
|
1020
|
+
/provider groq [dim]← switch to groq[/dim]
|
|
693
1021
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1022
|
+
[bold yellow]TIPS[/bold yellow]
|
|
1023
|
+
• [green]Groq[/green] is free and fast - great for starting out
|
|
1024
|
+
• [green]Ollama[/green] runs locally - no key needed (/provider ollama)
|
|
1025
|
+
• Keys are stored in [dim]~/.sigma/config.env[/dim]
|
|
1026
|
+
"""
|
|
1027
|
+
chat.write(Panel(
|
|
1028
|
+
Text.from_markup(keys_help),
|
|
1029
|
+
title=f"[bold cyan]{SIGMA} API Keys[/bold cyan]",
|
|
1030
|
+
border_style="cyan",
|
|
1031
|
+
padding=(0, 1),
|
|
1032
|
+
))
|
|
697
1033
|
self._show_status(chat)
|
|
698
1034
|
|
|
699
1035
|
def _show_status(self, chat: ChatLog):
|
|
700
|
-
|
|
701
|
-
table
|
|
702
|
-
table.add_column("")
|
|
1036
|
+
"""Show current configuration status."""
|
|
1037
|
+
table = Table(show_header=True, box=None, padding=(0, 2), title=f"{SIGMA} Current Status")
|
|
1038
|
+
table.add_column("Setting", style="bold")
|
|
1039
|
+
table.add_column("Value")
|
|
1040
|
+
table.add_column("Status")
|
|
703
1041
|
|
|
704
1042
|
provider = getattr(self.settings.default_provider, 'value', str(self.settings.default_provider))
|
|
705
|
-
table.add_row("Provider", provider)
|
|
706
|
-
table.add_row("Model", self.settings.default_model)
|
|
707
|
-
table.add_row("", "")
|
|
708
|
-
|
|
709
|
-
keys
|
|
710
|
-
|
|
711
|
-
("
|
|
712
|
-
("
|
|
713
|
-
("
|
|
714
|
-
("
|
|
1043
|
+
table.add_row("Provider", provider, "[green][*][/green] Active")
|
|
1044
|
+
table.add_row("Model", self.settings.default_model, "")
|
|
1045
|
+
table.add_row("", "", "")
|
|
1046
|
+
|
|
1047
|
+
# LLM Keys - show FULL keys (no masking)
|
|
1048
|
+
llm_keys = [
|
|
1049
|
+
("Google", self.settings.google_api_key, "google"),
|
|
1050
|
+
("OpenAI", self.settings.openai_api_key, "openai"),
|
|
1051
|
+
("Anthropic", self.settings.anthropic_api_key, "anthropic"),
|
|
1052
|
+
("Groq", self.settings.groq_api_key, "groq"),
|
|
1053
|
+
("xAI", self.settings.xai_api_key, "xai"),
|
|
715
1054
|
]
|
|
716
|
-
for name, key in
|
|
717
|
-
|
|
718
|
-
|
|
1055
|
+
for name, key, prov in llm_keys:
|
|
1056
|
+
if key:
|
|
1057
|
+
# Show full key - no masking
|
|
1058
|
+
display_key = key
|
|
1059
|
+
status = "[green]OK[/green]"
|
|
1060
|
+
else:
|
|
1061
|
+
display_key = "[dim]not set[/dim]"
|
|
1062
|
+
status = "[dim]--[/dim]"
|
|
1063
|
+
|
|
1064
|
+
# Highlight active provider
|
|
1065
|
+
if prov == provider:
|
|
1066
|
+
name = f"[bold cyan]{name}[/bold cyan]"
|
|
1067
|
+
table.add_row(f" {name}", Text.from_markup(display_key), Text.from_markup(status))
|
|
1068
|
+
|
|
1069
|
+
table.add_row("", "", "")
|
|
719
1070
|
|
|
720
|
-
|
|
1071
|
+
# Data Keys - show FULL keys
|
|
1072
|
+
polygon_key = getattr(self.settings, 'polygon_api_key', None)
|
|
1073
|
+
if polygon_key:
|
|
1074
|
+
table.add_row(" Polygon", polygon_key, Text.from_markup("[green]OK[/green]"))
|
|
1075
|
+
else:
|
|
1076
|
+
table.add_row(" Polygon", Text.from_markup("[dim]not set[/dim]"), Text.from_markup("[dim]optional[/dim]"))
|
|
1077
|
+
|
|
1078
|
+
chat.write(Panel(table, border_style="dim"))
|
|
721
1079
|
|
|
722
1080
|
def _show_models(self, chat: ChatLog):
|
|
723
1081
|
table = Table(title=f"{SIGMA} Models", show_header=True, border_style="dim")
|
|
@@ -756,18 +1114,45 @@ Example: /setkey google AIzaSy...
|
|
|
756
1114
|
chat.write_system(f"Model: {model}")
|
|
757
1115
|
|
|
758
1116
|
def _set_key(self, provider: str, key: str, chat: ChatLog):
|
|
1117
|
+
"""Save an API key for a provider."""
|
|
1118
|
+
# Normalize provider name
|
|
1119
|
+
provider = provider.lower().strip()
|
|
1120
|
+
valid_providers = ["google", "openai", "anthropic", "groq", "xai", "polygon", "alphavantage", "exa"]
|
|
1121
|
+
|
|
1122
|
+
if provider not in valid_providers:
|
|
1123
|
+
chat.write_error(f"[E{ErrorCode.INVALID_INPUT}] Unknown provider: {provider}")
|
|
1124
|
+
chat.write_system(f"Valid providers: {', '.join(valid_providers)}")
|
|
1125
|
+
return
|
|
1126
|
+
|
|
1127
|
+
# Basic key validation
|
|
1128
|
+
key = key.strip()
|
|
1129
|
+
if len(key) < 10:
|
|
1130
|
+
chat.write_error(f"[E{ErrorCode.INVALID_INPUT}] API key seems too short")
|
|
1131
|
+
return
|
|
1132
|
+
|
|
759
1133
|
try:
|
|
760
|
-
save_api_key(
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
self.
|
|
1134
|
+
success = save_api_key(provider, key)
|
|
1135
|
+
if success:
|
|
1136
|
+
# Reload settings
|
|
1137
|
+
self.settings = get_settings()
|
|
1138
|
+
|
|
1139
|
+
# Show success with FULL key (no masking)
|
|
1140
|
+
chat.write_system(f"[green]OK[/green] {SIGMA} Key saved for [bold]{provider}[/bold]: {key}")
|
|
1141
|
+
|
|
1142
|
+
# Auto-switch to this provider if it's an LLM provider and we don't have an LLM
|
|
1143
|
+
llm_providers = ["google", "openai", "anthropic", "groq", "xai"]
|
|
1144
|
+
if provider in llm_providers:
|
|
1145
|
+
if not self.llm or provider == getattr(self.settings.default_provider, 'value', ''):
|
|
1146
|
+
self._switch_provider(provider, chat)
|
|
1147
|
+
else:
|
|
1148
|
+
chat.write_error(f"[E{ErrorCode.UNKNOWN_ERROR}] Failed to save key")
|
|
764
1149
|
except Exception as e:
|
|
765
|
-
chat.write_error(str(e))
|
|
1150
|
+
chat.write_error(f"[E{ErrorCode.UNKNOWN_ERROR}] {str(e)}")
|
|
766
1151
|
|
|
767
1152
|
@work(exclusive=True)
|
|
768
1153
|
async def _process_query(self, query: str, chat: ChatLog):
|
|
769
1154
|
if not self.llm:
|
|
770
|
-
chat.write_error("No LLM. Use /keys to
|
|
1155
|
+
chat.write_error(f"[E{ErrorCode.API_KEY_MISSING}] No LLM configured. Use /keys to set up.")
|
|
771
1156
|
return
|
|
772
1157
|
|
|
773
1158
|
self.is_processing = True
|
|
@@ -777,7 +1162,7 @@ Example: /setkey google AIzaSy...
|
|
|
777
1162
|
|
|
778
1163
|
# Clear ticker highlight and start sigma animation
|
|
779
1164
|
ticker_highlight.update("")
|
|
780
|
-
indicator.set_active(True)
|
|
1165
|
+
indicator.set_active(True, mode="thinking")
|
|
781
1166
|
|
|
782
1167
|
try:
|
|
783
1168
|
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
@@ -808,10 +1193,24 @@ Example: /setkey google AIzaSy...
|
|
|
808
1193
|
if len(self.conversation) > 20:
|
|
809
1194
|
self.conversation = self.conversation[-20:]
|
|
810
1195
|
else:
|
|
811
|
-
chat.write_error("No response")
|
|
1196
|
+
chat.write_error(f"[E{ErrorCode.RESPONSE_INVALID}] No response received")
|
|
1197
|
+
|
|
1198
|
+
except SigmaError as e:
|
|
1199
|
+
tool_display.clear()
|
|
1200
|
+
# Format SigmaError nicely
|
|
1201
|
+
chat.write_error(f"[E{e.code}] {e.message}")
|
|
1202
|
+
if e.details.get("hint"):
|
|
1203
|
+
chat.write_system(f"[dim]Hint: {e.details['hint']}[/dim]")
|
|
812
1204
|
except Exception as e:
|
|
813
1205
|
tool_display.clear()
|
|
814
|
-
|
|
1206
|
+
# Try to parse common API errors
|
|
1207
|
+
error_str = str(e)
|
|
1208
|
+
if "401" in error_str or "invalid" in error_str.lower() and "key" in error_str.lower():
|
|
1209
|
+
chat.write_error(f"[E{ErrorCode.API_KEY_INVALID}] API key is invalid. Use /keys to update.")
|
|
1210
|
+
elif "429" in error_str or "rate" in error_str.lower():
|
|
1211
|
+
chat.write_error(f"[E{ErrorCode.API_KEY_RATE_LIMITED}] Rate limit hit. Wait a moment and try again.")
|
|
1212
|
+
else:
|
|
1213
|
+
chat.write_error(f"[E{ErrorCode.PROVIDER_ERROR}] {error_str[:200]}")
|
|
815
1214
|
finally:
|
|
816
1215
|
indicator.set_active(False)
|
|
817
1216
|
self.is_processing = False
|