sigma-terminal 3.3.2__py3-none-any.whl → 3.4.0__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 +442 -112
- sigma/charts.py +33 -8
- sigma/cli.py +11 -11
- sigma/config.py +172 -34
- sigma/llm.py +153 -6
- sigma/setup.py +14 -14
- sigma/tools.py +277 -3
- sigma_terminal-3.4.0.dist-info/METADATA +264 -0
- {sigma_terminal-3.3.2.dist-info → sigma_terminal-3.4.0.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.0.dist-info}/WHEEL +0 -0
- {sigma_terminal-3.3.2.dist-info → sigma_terminal-3.4.0.dist-info}/entry_points.txt +0 -0
- {sigma_terminal-3.3.2.dist-info → sigma_terminal-3.4.0.dist-info}/licenses/LICENSE +0 -0
sigma/app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Sigma v3.
|
|
1
|
+
"""Sigma v3.4.0 - 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.0"
|
|
27
27
|
SIGMA = "σ"
|
|
28
28
|
|
|
29
29
|
# Common stock tickers for recognition
|
|
@@ -38,52 +38,107 @@ COMMON_TICKERS = {
|
|
|
38
38
|
"SPY", "QQQ", "IWM", "DIA", "VTI", "VOO", "VXX", "ARKK", "XLF", "XLK", "XLE",
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
"[bold
|
|
44
|
-
"[bold
|
|
45
|
-
"[bold
|
|
46
|
-
"[bold #
|
|
41
|
+
# Sigma animation frames - color cycling for thinking/processing state
|
|
42
|
+
SIGMA_FRAMES = [
|
|
43
|
+
"[bold #3b82f6]s[/bold #3b82f6]",
|
|
44
|
+
"[bold #60a5fa]si[/bold #60a5fa]",
|
|
45
|
+
"[bold #93c5fd]sig[/bold #93c5fd]",
|
|
46
|
+
"[bold #bfdbfe]sigm[/bold #bfdbfe]",
|
|
47
|
+
"[bold white]sigma[/bold white]",
|
|
48
|
+
"[bold #bfdbfe]sigm[/bold #bfdbfe]",
|
|
49
|
+
"[bold #93c5fd]sig[/bold #93c5fd]",
|
|
50
|
+
"[bold #60a5fa]si[/bold #60a5fa]",
|
|
51
|
+
"[bold #3b82f6]s[/bold #3b82f6]",
|
|
52
|
+
"[bold cyan].[/bold cyan]",
|
|
47
53
|
]
|
|
48
54
|
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
"
|
|
55
|
+
# Sigma pulse animation (color breathing)
|
|
56
|
+
SIGMA_PULSE_FRAMES = [
|
|
57
|
+
"[bold #1e40af]o[/bold #1e40af]",
|
|
58
|
+
"[bold #2563eb]o[/bold #2563eb]",
|
|
59
|
+
"[bold #3b82f6]o[/bold #3b82f6]",
|
|
60
|
+
"[bold #60a5fa]o[/bold #60a5fa]",
|
|
61
|
+
"[bold #93c5fd]o[/bold #93c5fd]",
|
|
62
|
+
"[bold #bfdbfe]o[/bold #bfdbfe]",
|
|
63
|
+
"[bold #93c5fd]o[/bold #93c5fd]",
|
|
64
|
+
"[bold #60a5fa]o[/bold #60a5fa]",
|
|
65
|
+
"[bold #3b82f6]o[/bold #3b82f6]",
|
|
66
|
+
"[bold #2563eb]o[/bold #2563eb]",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# Tool call spinner frames - ASCII based
|
|
70
|
+
TOOL_SPINNER_FRAMES = [
|
|
71
|
+
"|", "/", "-", "\\"
|
|
52
72
|
]
|
|
53
73
|
|
|
54
74
|
# Welcome banner - clean design
|
|
55
75
|
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.
|
|
76
|
+
[bold #3b82f6]███████╗██╗ ██████╗ ███╗ ███╗ █████╗ [/bold #3b82f6]
|
|
77
|
+
[bold #60a5fa]██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗[/bold #60a5fa]
|
|
78
|
+
[bold #93c5fd]███████╗██║██║ ███╗██╔████╔██║███████║[/bold #93c5fd]
|
|
79
|
+
[bold #60a5fa]╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║[/bold #60a5fa]
|
|
80
|
+
[bold #3b82f6]███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║[/bold #3b82f6]
|
|
81
|
+
[bold #1d4ed8]╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold #1d4ed8]
|
|
82
|
+
|
|
83
|
+
[bold cyan]Finance Research Agent[/bold cyan] [dim]v3.4.0[/dim]
|
|
64
84
|
"""
|
|
65
85
|
|
|
66
|
-
SYSTEM_PROMPT = """You are Sigma,
|
|
86
|
+
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
87
|
|
|
68
88
|
CORE CAPABILITIES:
|
|
69
|
-
- Real-time market data analysis (quotes, charts, technicals)
|
|
70
|
-
- Fundamental analysis (financials, ratios, earnings)
|
|
71
|
-
- Technical analysis (RSI, MACD, Bollinger Bands, moving averages)
|
|
89
|
+
- Real-time market data analysis (quotes, charts, technicals) via yfinance and Polygon.io
|
|
90
|
+
- Fundamental analysis (financials, ratios, earnings, valuations)
|
|
91
|
+
- Technical analysis (RSI, MACD, Bollinger Bands, moving averages, support/resistance)
|
|
72
92
|
- Backtesting strategies (SMA crossover, RSI, MACD, Bollinger, momentum, breakout)
|
|
73
93
|
- Portfolio analysis and optimization
|
|
74
|
-
- Sector and market overview
|
|
75
|
-
- Insider and
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
- Sector and market overview with economic indicators
|
|
95
|
+
- Insider trading and institutional activity tracking
|
|
96
|
+
- Financial news search and SEC filings analysis
|
|
97
|
+
|
|
98
|
+
RESPONSE PHILOSOPHY:
|
|
99
|
+
1. BE PROACTIVE: Anticipate follow-up questions and address them
|
|
100
|
+
2. BE THOROUGH: When analyzing, cover fundamentals + technicals + sentiment
|
|
101
|
+
3. BE ACTIONABLE: Always end with clear recommendations or next steps
|
|
102
|
+
4. BE DATA-DRIVEN: Cite specific numbers, dates, and percentages
|
|
103
|
+
5. BE HONEST: Acknowledge uncertainty and risks
|
|
104
|
+
|
|
105
|
+
RESPONSE FORMAT:
|
|
106
|
+
- Start with a 1-2 sentence executive summary (the key takeaway)
|
|
107
|
+
- Use structured sections with clear headers (## format)
|
|
108
|
+
- Present comparative data in markdown tables
|
|
109
|
+
- Highlight key metrics: **bold** for critical numbers
|
|
110
|
+
- Use bullet points for easy scanning
|
|
111
|
+
- End with "**Bottom Line:**" or "**Recommendation:**"
|
|
112
|
+
|
|
113
|
+
RATING SYSTEM (when asked for recommendations):
|
|
114
|
+
- STRONG BUY [A+]: Exceptional opportunity, high conviction
|
|
115
|
+
- BUY [A]: Favorable outlook, solid fundamentals
|
|
116
|
+
- HOLD [B]: Fair value, wait for better entry
|
|
117
|
+
- SELL [C]: Concerns outweigh positives
|
|
118
|
+
- STRONG SELL [D]: Significant risks, avoid
|
|
119
|
+
|
|
120
|
+
DATA GATHERING RULES:
|
|
121
|
+
- ALWAYS use tools to fetch current data before answering stock questions
|
|
122
|
+
- Use multiple tools for comprehensive analysis (quote + technicals + fundamentals)
|
|
123
|
+
- Cross-reference data points for accuracy
|
|
124
|
+
- If a tool fails, try alternative approaches or acknowledge the limitation
|
|
125
|
+
|
|
126
|
+
PROACTIVE INSIGHTS:
|
|
127
|
+
When analyzing any stock, proactively mention:
|
|
128
|
+
- Recent earnings surprises or upcoming earnings dates
|
|
129
|
+
- Major analyst rating changes
|
|
130
|
+
- Unusual volume or price movements
|
|
131
|
+
- Relevant sector trends
|
|
132
|
+
- Key support/resistance levels
|
|
133
|
+
- Comparison to peers when relevant
|
|
134
|
+
|
|
135
|
+
FORBIDDEN:
|
|
136
|
+
- Never fabricate data or prices
|
|
137
|
+
- Never provide data without fetching it first
|
|
138
|
+
- Never give buy/sell advice without disclosure of risks
|
|
139
|
+
- Never claim certainty about future performance
|
|
140
|
+
|
|
141
|
+
Remember: Users trust you for professional-grade financial research. Exceed their expectations with every response."""
|
|
87
142
|
|
|
88
143
|
# Enhanced autocomplete suggestions with more variety
|
|
89
144
|
SUGGESTIONS = [
|
|
@@ -142,6 +197,28 @@ SUGGESTIONS = [
|
|
|
142
197
|
"/backtest",
|
|
143
198
|
]
|
|
144
199
|
|
|
200
|
+
# Extended action verbs for smart completion
|
|
201
|
+
ACTION_VERBS = [
|
|
202
|
+
"analyze", "compare", "show", "get", "what is", "tell me about",
|
|
203
|
+
"technical analysis", "fundamentals", "price", "quote", "chart",
|
|
204
|
+
"backtest", "insider trading", "institutional", "analyst", "earnings",
|
|
205
|
+
"financials", "valuation", "news", "sector", "market", "portfolio"
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
# Ticker categories for smart suggestions
|
|
209
|
+
TICKER_CATEGORIES = {
|
|
210
|
+
"tech": ["AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "AMD", "INTC", "CRM", "ADBE"],
|
|
211
|
+
"finance": ["JPM", "BAC", "GS", "MS", "V", "MA", "BRK.B", "C", "WFC", "AXP"],
|
|
212
|
+
"healthcare": ["JNJ", "UNH", "PFE", "MRK", "ABBV", "LLY", "TMO", "ABT", "BMY", "GILD"],
|
|
213
|
+
"consumer": ["AMZN", "TSLA", "HD", "NKE", "MCD", "SBUX", "COST", "WMT", "TGT", "LOW"],
|
|
214
|
+
"energy": ["XOM", "CVX", "COP", "SLB", "EOG", "MPC", "PSX", "VLO", "OXY", "HAL"],
|
|
215
|
+
"etf": ["SPY", "QQQ", "IWM", "DIA", "VTI", "VOO", "VXX", "ARKK", "XLF", "XLK"],
|
|
216
|
+
"crypto": ["COIN", "MSTR", "RIOT", "MARA", "HUT"],
|
|
217
|
+
"ev": ["TSLA", "RIVN", "LCID", "NIO", "XPEV", "LI"],
|
|
218
|
+
"ai": ["NVDA", "AMD", "MSFT", "GOOGL", "META", "PLTR", "AI", "PATH", "SNOW"],
|
|
219
|
+
"semiconductor": ["NVDA", "AMD", "INTC", "AVGO", "QCOM", "TXN", "MU", "AMAT", "LRCX", "ASML"],
|
|
220
|
+
}
|
|
221
|
+
|
|
145
222
|
|
|
146
223
|
def extract_tickers(text: str) -> List[str]:
|
|
147
224
|
"""Extract stock tickers from text."""
|
|
@@ -167,34 +244,174 @@ def extract_tickers(text: str) -> List[str]:
|
|
|
167
244
|
|
|
168
245
|
|
|
169
246
|
class SigmaSuggester(Suggester):
|
|
170
|
-
"""
|
|
247
|
+
"""Smart autocomplete with fuzzy matching and context awareness."""
|
|
171
248
|
|
|
172
249
|
def __init__(self):
|
|
173
|
-
super().__init__(use_cache=
|
|
250
|
+
super().__init__(use_cache=False, case_sensitive=False)
|
|
251
|
+
self._build_suggestion_index()
|
|
252
|
+
|
|
253
|
+
def _build_suggestion_index(self):
|
|
254
|
+
"""Build an index of all possible suggestions for fast lookup."""
|
|
255
|
+
self.all_suggestions = []
|
|
256
|
+
|
|
257
|
+
# Add static suggestions
|
|
258
|
+
self.all_suggestions.extend(SUGGESTIONS)
|
|
259
|
+
|
|
260
|
+
# Generate dynamic suggestions for all tickers
|
|
261
|
+
for ticker in COMMON_TICKERS:
|
|
262
|
+
self.all_suggestions.extend([
|
|
263
|
+
f"analyze {ticker}",
|
|
264
|
+
f"technical analysis of {ticker}",
|
|
265
|
+
f"fundamentals of {ticker}",
|
|
266
|
+
f"price of {ticker}",
|
|
267
|
+
f"quote {ticker}",
|
|
268
|
+
f"insider trading {ticker}",
|
|
269
|
+
f"earnings {ticker}",
|
|
270
|
+
f"news {ticker}",
|
|
271
|
+
])
|
|
272
|
+
|
|
273
|
+
# Add category-based suggestions
|
|
274
|
+
for category, tickers in TICKER_CATEGORIES.items():
|
|
275
|
+
self.all_suggestions.append(f"best {category} stocks")
|
|
276
|
+
self.all_suggestions.append(f"{category} sector performance")
|
|
277
|
+
if len(tickers) >= 3:
|
|
278
|
+
self.all_suggestions.append(f"compare {' '.join(tickers[:3])}")
|
|
279
|
+
|
|
280
|
+
def _fuzzy_match(self, query: str, target: str) -> float:
|
|
281
|
+
"""Calculate fuzzy match score (0-1). Higher is better."""
|
|
282
|
+
query = query.lower()
|
|
283
|
+
target = target.lower()
|
|
284
|
+
|
|
285
|
+
# Exact prefix match is best
|
|
286
|
+
if target.startswith(query):
|
|
287
|
+
return 1.0 + len(query) / len(target)
|
|
288
|
+
|
|
289
|
+
# Check if all query chars appear in order
|
|
290
|
+
q_idx = 0
|
|
291
|
+
matches = 0
|
|
292
|
+
consecutive = 0
|
|
293
|
+
max_consecutive = 0
|
|
294
|
+
last_match = -1
|
|
295
|
+
|
|
296
|
+
for t_idx, char in enumerate(target):
|
|
297
|
+
if q_idx < len(query) and char == query[q_idx]:
|
|
298
|
+
matches += 1
|
|
299
|
+
if last_match == t_idx - 1:
|
|
300
|
+
consecutive += 1
|
|
301
|
+
max_consecutive = max(max_consecutive, consecutive)
|
|
302
|
+
else:
|
|
303
|
+
consecutive = 1
|
|
304
|
+
last_match = t_idx
|
|
305
|
+
q_idx += 1
|
|
306
|
+
|
|
307
|
+
if q_idx < len(query):
|
|
308
|
+
# Not all chars matched
|
|
309
|
+
# Try substring match
|
|
310
|
+
if query in target:
|
|
311
|
+
return 0.5
|
|
312
|
+
return 0
|
|
313
|
+
|
|
314
|
+
# Score based on match quality
|
|
315
|
+
score = (matches / len(query)) * 0.5 + (max_consecutive / len(query)) * 0.3
|
|
316
|
+
# Bonus for shorter targets (more relevant)
|
|
317
|
+
score += (1 - len(target) / 100) * 0.2
|
|
318
|
+
|
|
319
|
+
return score
|
|
320
|
+
|
|
321
|
+
def _get_context_suggestions(self, value: str) -> List[str]:
|
|
322
|
+
"""Get context-aware suggestions based on partial input."""
|
|
323
|
+
suggestions = []
|
|
324
|
+
value_lower = value.lower().strip()
|
|
325
|
+
|
|
326
|
+
# Command shortcuts
|
|
327
|
+
if value.startswith("/"):
|
|
328
|
+
cmd = value_lower[1:]
|
|
329
|
+
commands = ["/help", "/clear", "/keys", "/models", "/status", "/backtest",
|
|
330
|
+
"/provider", "/model", "/setkey", "/tickers"]
|
|
331
|
+
for c in commands:
|
|
332
|
+
if c[1:].startswith(cmd):
|
|
333
|
+
suggestions.append(c)
|
|
334
|
+
return suggestions
|
|
335
|
+
|
|
336
|
+
# Detect if user is typing a ticker
|
|
337
|
+
words = value.split()
|
|
338
|
+
last_word = words[-1] if words else ""
|
|
339
|
+
|
|
340
|
+
if last_word.startswith("$") or (last_word.isupper() and len(last_word) >= 1):
|
|
341
|
+
ticker_prefix = last_word.lstrip("$").upper()
|
|
342
|
+
matching_tickers = [t for t in COMMON_TICKERS if t.startswith(ticker_prefix)]
|
|
343
|
+
|
|
344
|
+
# If we have an action verb, complete with ticker
|
|
345
|
+
action_words = ["analyze", "compare", "technical", "price", "quote",
|
|
346
|
+
"fundamentals", "insider", "earnings", "news"]
|
|
347
|
+
prefix = " ".join(words[:-1]).lower() if len(words) > 1 else ""
|
|
348
|
+
|
|
349
|
+
for ticker in matching_tickers[:5]:
|
|
350
|
+
if prefix:
|
|
351
|
+
suggestions.append(f"{prefix} {ticker}")
|
|
352
|
+
else:
|
|
353
|
+
suggestions.append(f"analyze {ticker}")
|
|
354
|
+
return suggestions
|
|
355
|
+
|
|
356
|
+
# Natural language patterns
|
|
357
|
+
patterns = [
|
|
358
|
+
("ana", "analyze"),
|
|
359
|
+
("tech", "technical analysis of"),
|
|
360
|
+
("fun", "fundamentals of"),
|
|
361
|
+
("comp", "compare"),
|
|
362
|
+
("back", "backtest"),
|
|
363
|
+
("mark", "market overview"),
|
|
364
|
+
("sect", "sector performance"),
|
|
365
|
+
("pri", "price of"),
|
|
366
|
+
("quo", "quote"),
|
|
367
|
+
("ins", "insider trading"),
|
|
368
|
+
("ear", "earnings"),
|
|
369
|
+
("wha", "what should I know about"),
|
|
370
|
+
("sho", "should I buy"),
|
|
371
|
+
("is ", "is NVDA overvalued"),
|
|
372
|
+
("how", "how is"),
|
|
373
|
+
("bes", "best tech stocks"),
|
|
374
|
+
]
|
|
375
|
+
|
|
376
|
+
for prefix, expansion in patterns:
|
|
377
|
+
if value_lower.startswith(prefix):
|
|
378
|
+
if expansion.endswith("of") or expansion.endswith("about"):
|
|
379
|
+
# Add popular tickers
|
|
380
|
+
for ticker in ["AAPL", "NVDA", "MSFT", "TSLA", "GOOGL"]:
|
|
381
|
+
suggestions.append(f"{expansion} {ticker}")
|
|
382
|
+
else:
|
|
383
|
+
suggestions.append(expansion)
|
|
384
|
+
|
|
385
|
+
return suggestions
|
|
174
386
|
|
|
175
387
|
async def get_suggestion(self, value: str) -> Optional[str]:
|
|
176
|
-
"""Get autocomplete suggestion."""
|
|
177
|
-
if not value or len(value) <
|
|
388
|
+
"""Get the best autocomplete suggestion."""
|
|
389
|
+
if not value or len(value) < 1:
|
|
178
390
|
return None
|
|
179
391
|
|
|
180
|
-
value_lower = value.lower()
|
|
392
|
+
value_lower = value.lower().strip()
|
|
393
|
+
|
|
394
|
+
# Get context-aware suggestions first
|
|
395
|
+
context_suggestions = self._get_context_suggestions(value)
|
|
396
|
+
if context_suggestions:
|
|
397
|
+
return context_suggestions[0]
|
|
181
398
|
|
|
182
|
-
#
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
399
|
+
# Fuzzy match against all suggestions
|
|
400
|
+
scored = []
|
|
401
|
+
for suggestion in self.all_suggestions:
|
|
402
|
+
score = self._fuzzy_match(value_lower, suggestion)
|
|
403
|
+
if score > 0:
|
|
404
|
+
scored.append((score, suggestion))
|
|
188
405
|
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
if suggestion.lower().startswith(value_lower):
|
|
192
|
-
return suggestion
|
|
406
|
+
# Sort by score and return best
|
|
407
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
193
408
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
409
|
+
if scored:
|
|
410
|
+
return scored[0][1]
|
|
411
|
+
|
|
412
|
+
# Last resort: if looks like a ticker, suggest analyze
|
|
413
|
+
if value.isupper() and len(value) <= 5 and value.isalpha():
|
|
414
|
+
return f"analyze {value}"
|
|
198
415
|
|
|
199
416
|
return None
|
|
200
417
|
|
|
@@ -230,8 +447,8 @@ Screen {
|
|
|
230
447
|
|
|
231
448
|
#status-bar {
|
|
232
449
|
height: 3;
|
|
233
|
-
background: #
|
|
234
|
-
border-top: solid #
|
|
450
|
+
background: #0f1419;
|
|
451
|
+
border-top: solid #1e293b;
|
|
235
452
|
padding: 0 2;
|
|
236
453
|
dock: bottom;
|
|
237
454
|
}
|
|
@@ -256,9 +473,9 @@ Screen {
|
|
|
256
473
|
#tool-calls-display {
|
|
257
474
|
width: 100%;
|
|
258
475
|
height: auto;
|
|
259
|
-
max-height:
|
|
260
|
-
background: #
|
|
261
|
-
border:
|
|
476
|
+
max-height: 8;
|
|
477
|
+
background: #0f1419;
|
|
478
|
+
border: round #1e293b;
|
|
262
479
|
margin: 0 2;
|
|
263
480
|
padding: 0 1;
|
|
264
481
|
display: none;
|
|
@@ -271,7 +488,7 @@ Screen {
|
|
|
271
488
|
#input-area {
|
|
272
489
|
height: 5;
|
|
273
490
|
padding: 1 2;
|
|
274
|
-
background: #
|
|
491
|
+
background: #0f1419;
|
|
275
492
|
}
|
|
276
493
|
|
|
277
494
|
#input-row {
|
|
@@ -283,22 +500,24 @@ Screen {
|
|
|
283
500
|
width: 4;
|
|
284
501
|
height: 3;
|
|
285
502
|
content-align: center middle;
|
|
503
|
+
background: transparent;
|
|
286
504
|
}
|
|
287
505
|
|
|
288
506
|
#prompt-input {
|
|
289
507
|
width: 1fr;
|
|
290
|
-
background: #
|
|
291
|
-
border:
|
|
292
|
-
color: #
|
|
508
|
+
background: #1e293b;
|
|
509
|
+
border: tall #3b82f6;
|
|
510
|
+
color: #f8fafc;
|
|
293
511
|
padding: 0 1;
|
|
294
512
|
}
|
|
295
513
|
|
|
296
514
|
#prompt-input:focus {
|
|
297
|
-
border:
|
|
515
|
+
border: tall #60a5fa;
|
|
516
|
+
background: #1e3a5f;
|
|
298
517
|
}
|
|
299
518
|
|
|
300
519
|
#prompt-input.-autocomplete {
|
|
301
|
-
border:
|
|
520
|
+
border: tall #22c55e;
|
|
302
521
|
}
|
|
303
522
|
|
|
304
523
|
#ticker-highlight {
|
|
@@ -306,6 +525,7 @@ Screen {
|
|
|
306
525
|
height: 1;
|
|
307
526
|
padding: 0 1;
|
|
308
527
|
background: transparent;
|
|
528
|
+
color: #22d3ee;
|
|
309
529
|
}
|
|
310
530
|
|
|
311
531
|
Footer {
|
|
@@ -380,7 +600,7 @@ class ToolCallDisplay(Static):
|
|
|
380
600
|
|
|
381
601
|
def _animate(self):
|
|
382
602
|
"""Animate the spinner."""
|
|
383
|
-
self.frame = (self.frame + 1) % len(
|
|
603
|
+
self.frame = (self.frame + 1) % len(TOOL_SPINNER_FRAMES)
|
|
384
604
|
for tc in self.tool_calls:
|
|
385
605
|
if tc["status"] == "running":
|
|
386
606
|
tc["frame"] = self.frame
|
|
@@ -394,38 +614,48 @@ class ToolCallDisplay(Static):
|
|
|
394
614
|
lines = []
|
|
395
615
|
for tc in self.tool_calls:
|
|
396
616
|
if tc["status"] == "running":
|
|
397
|
-
spinner =
|
|
617
|
+
spinner = TOOL_SPINNER_FRAMES[tc["frame"] % len(TOOL_SPINNER_FRAMES)]
|
|
398
618
|
lines.append(f" [cyan]{spinner}[/cyan] [bold]{tc['name']}[/bold] [dim]executing...[/dim]")
|
|
399
619
|
else:
|
|
400
|
-
lines.append(f" [green]
|
|
620
|
+
lines.append(f" [green][ok][/green] [bold]{tc['name']}[/bold] [green]done[/green]")
|
|
401
621
|
|
|
402
622
|
self.update(Text.from_markup("\n".join(lines)))
|
|
403
623
|
|
|
404
624
|
|
|
405
625
|
class SigmaIndicator(Static):
|
|
406
|
-
"""
|
|
626
|
+
"""Animated sigma indicator - typewriter style when active."""
|
|
407
627
|
|
|
408
628
|
def __init__(self, *args, **kwargs):
|
|
409
629
|
super().__init__(*args, **kwargs)
|
|
410
630
|
self.active = False
|
|
631
|
+
self.mode = "idle" # idle, thinking, tool
|
|
411
632
|
self.frame = 0
|
|
412
633
|
self.timer = None
|
|
413
634
|
|
|
414
635
|
def on_mount(self):
|
|
415
636
|
self.update(Text.from_markup(f"[bold blue]{SIGMA}[/bold blue]"))
|
|
416
637
|
|
|
417
|
-
def set_active(self, active: bool):
|
|
638
|
+
def set_active(self, active: bool, mode: str = "thinking"):
|
|
418
639
|
self.active = active
|
|
640
|
+
self.mode = mode if active else "idle"
|
|
419
641
|
if active and not self.timer:
|
|
420
|
-
self.
|
|
642
|
+
self.frame = 0
|
|
643
|
+
interval = 0.08 if mode == "thinking" else 0.12
|
|
644
|
+
self.timer = self.set_interval(interval, self._animate)
|
|
421
645
|
elif not active and self.timer:
|
|
422
646
|
self.timer.stop()
|
|
423
647
|
self.timer = None
|
|
424
648
|
self.update(Text.from_markup(f"[bold blue]{SIGMA}[/bold blue]"))
|
|
425
649
|
|
|
426
|
-
def
|
|
427
|
-
|
|
428
|
-
|
|
650
|
+
def _animate(self):
|
|
651
|
+
if self.mode == "thinking":
|
|
652
|
+
# Typewriter effect: s -> si -> sig -> sigm -> sigma -> sigm -> ...
|
|
653
|
+
self.frame = (self.frame + 1) % len(SIGMA_FRAMES)
|
|
654
|
+
self.update(Text.from_markup(SIGMA_FRAMES[self.frame]))
|
|
655
|
+
else:
|
|
656
|
+
# Pulse effect for tool calls
|
|
657
|
+
self.frame = (self.frame + 1) % len(SIGMA_PULSE_FRAMES)
|
|
658
|
+
self.update(Text.from_markup(SIGMA_PULSE_FRAMES[self.frame]))
|
|
429
659
|
|
|
430
660
|
|
|
431
661
|
class TickerHighlight(Static):
|
|
@@ -538,20 +768,28 @@ class SigmaApp(App):
|
|
|
538
768
|
chat.write_welcome()
|
|
539
769
|
|
|
540
770
|
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 •
|
|
771
|
+
chat.write_system(f"{SIGMA} Provider: [bold]{provider}[/bold] | Model: [bold]{self.settings.default_model}[/bold]")
|
|
772
|
+
chat.write_system(f"{SIGMA} Type [cyan]/help[/cyan] for commands • [cyan]/keys[/cyan] to set up API keys")
|
|
543
773
|
chat.write_system("")
|
|
544
774
|
|
|
545
775
|
self._init_llm()
|
|
546
776
|
self.query_one("#prompt-input", Input).focus()
|
|
547
777
|
|
|
548
778
|
def _init_llm(self):
|
|
779
|
+
"""Initialize the LLM client with proper error handling."""
|
|
549
780
|
try:
|
|
550
781
|
self.llm = get_llm(self.settings.default_provider, self.settings.default_model)
|
|
782
|
+
except SigmaError as e:
|
|
783
|
+
chat = self.query_one("#chat-log", ChatLog)
|
|
784
|
+
chat.write_error(f"[E{e.code}] {e.message}")
|
|
785
|
+
if e.details.get("hint"):
|
|
786
|
+
chat.write_system(f"[dim]Hint: {e.details['hint']}[/dim]")
|
|
787
|
+
self.llm = None
|
|
551
788
|
except Exception as e:
|
|
552
789
|
chat = self.query_one("#chat-log", ChatLog)
|
|
553
|
-
chat.write_error(f"
|
|
554
|
-
chat.write_system("Use /keys to configure API keys")
|
|
790
|
+
chat.write_error(f"[E{ErrorCode.PROVIDER_ERROR}] Failed to initialize: {str(e)[:100]}")
|
|
791
|
+
chat.write_system("[dim]Use /keys to configure API keys[/dim]")
|
|
792
|
+
self.llm = None
|
|
555
793
|
|
|
556
794
|
@on(Input.Changed)
|
|
557
795
|
def on_input_change(self, event: Input.Changed):
|
|
@@ -687,37 +925,87 @@ class SigmaApp(App):
|
|
|
687
925
|
))
|
|
688
926
|
|
|
689
927
|
def _show_keys(self, chat: ChatLog):
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
928
|
+
"""Show comprehensive API key management interface."""
|
|
929
|
+
keys_help = f"""
|
|
930
|
+
[bold cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold cyan]
|
|
931
|
+
[bold] {SIGMA} API KEY MANAGER [/bold]
|
|
932
|
+
[bold cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold cyan]
|
|
933
|
+
|
|
934
|
+
[bold yellow]QUICK SETUP[/bold yellow]
|
|
935
|
+
Set a key: [cyan]/setkey <provider> <your-api-key>[/cyan]
|
|
936
|
+
|
|
937
|
+
[bold yellow]LLM PROVIDERS[/bold yellow]
|
|
938
|
+
[bold]google[/bold] → https://aistudio.google.com/apikey
|
|
939
|
+
[bold]openai[/bold] → https://platform.openai.com/api-keys
|
|
940
|
+
[bold]anthropic[/bold] → https://console.anthropic.com/settings/keys
|
|
941
|
+
[bold]groq[/bold] → https://console.groq.com/keys [dim](free!)[/dim]
|
|
942
|
+
[bold]xai[/bold] → https://console.x.ai
|
|
943
|
+
|
|
944
|
+
[bold yellow]DATA PROVIDERS[/bold yellow] [dim](optional - enhances data quality)[/dim]
|
|
945
|
+
[bold]polygon[/bold] → https://polygon.io/dashboard/api-keys
|
|
946
|
+
|
|
947
|
+
[bold yellow]EXAMPLES[/bold yellow]
|
|
948
|
+
/setkey google AIzaSyB...
|
|
949
|
+
/setkey openai sk-proj-...
|
|
950
|
+
/setkey polygon abc123...
|
|
951
|
+
/provider groq [dim]← switch to groq[/dim]
|
|
693
952
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
953
|
+
[bold yellow]TIPS[/bold yellow]
|
|
954
|
+
• [green]Groq[/green] is free and fast - great for starting out
|
|
955
|
+
• [green]Ollama[/green] runs locally - no key needed (/provider ollama)
|
|
956
|
+
• Keys are stored in [dim]~/.sigma/config.env[/dim]
|
|
957
|
+
"""
|
|
958
|
+
chat.write(Panel(
|
|
959
|
+
Text.from_markup(keys_help),
|
|
960
|
+
title=f"[bold cyan]{SIGMA} API Keys[/bold cyan]",
|
|
961
|
+
border_style="cyan",
|
|
962
|
+
padding=(0, 1),
|
|
963
|
+
))
|
|
697
964
|
self._show_status(chat)
|
|
698
965
|
|
|
699
966
|
def _show_status(self, chat: ChatLog):
|
|
700
|
-
|
|
701
|
-
table
|
|
702
|
-
table.add_column("")
|
|
967
|
+
"""Show current configuration status."""
|
|
968
|
+
table = Table(show_header=True, box=None, padding=(0, 2), title=f"{SIGMA} Current Status")
|
|
969
|
+
table.add_column("Setting", style="bold")
|
|
970
|
+
table.add_column("Value")
|
|
971
|
+
table.add_column("Status")
|
|
703
972
|
|
|
704
973
|
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
|
-
|
|
710
|
-
|
|
711
|
-
("
|
|
712
|
-
("
|
|
713
|
-
("
|
|
714
|
-
("
|
|
974
|
+
table.add_row("Provider", provider, "[green][*][/green] Active")
|
|
975
|
+
table.add_row("Model", self.settings.default_model, "")
|
|
976
|
+
table.add_row("", "", "")
|
|
977
|
+
|
|
978
|
+
# LLM Keys
|
|
979
|
+
llm_keys = [
|
|
980
|
+
("Google", self.settings.google_api_key, "google"),
|
|
981
|
+
("OpenAI", self.settings.openai_api_key, "openai"),
|
|
982
|
+
("Anthropic", self.settings.anthropic_api_key, "anthropic"),
|
|
983
|
+
("Groq", self.settings.groq_api_key, "groq"),
|
|
984
|
+
("xAI", self.settings.xai_api_key, "xai"),
|
|
715
985
|
]
|
|
716
|
-
for name, key in
|
|
717
|
-
|
|
718
|
-
|
|
986
|
+
for name, key, prov in llm_keys:
|
|
987
|
+
if key:
|
|
988
|
+
masked = key[:8] + "..." + key[-4:] if len(key) > 12 else "***"
|
|
989
|
+
status = "[green][ok][/green]"
|
|
990
|
+
else:
|
|
991
|
+
masked = "[dim]not set[/dim]"
|
|
992
|
+
status = "[dim]--[/dim]"
|
|
993
|
+
|
|
994
|
+
# Highlight active provider
|
|
995
|
+
if prov == provider:
|
|
996
|
+
name = f"[bold cyan]{name}[/bold cyan]"
|
|
997
|
+
table.add_row(f" {name}", Text.from_markup(masked), Text.from_markup(status))
|
|
998
|
+
|
|
999
|
+
table.add_row("", "", "")
|
|
719
1000
|
|
|
720
|
-
|
|
1001
|
+
# Data Keys
|
|
1002
|
+
polygon_key = getattr(self.settings, 'polygon_api_key', None)
|
|
1003
|
+
if polygon_key:
|
|
1004
|
+
table.add_row(" Polygon", polygon_key[:8] + "...", Text.from_markup("[green][ok][/green]"))
|
|
1005
|
+
else:
|
|
1006
|
+
table.add_row(" Polygon", Text.from_markup("[dim]not set[/dim]"), Text.from_markup("[dim]optional[/dim]"))
|
|
1007
|
+
|
|
1008
|
+
chat.write(Panel(table, border_style="dim"))
|
|
721
1009
|
|
|
722
1010
|
def _show_models(self, chat: ChatLog):
|
|
723
1011
|
table = Table(title=f"{SIGMA} Models", show_header=True, border_style="dim")
|
|
@@ -756,18 +1044,46 @@ Example: /setkey google AIzaSy...
|
|
|
756
1044
|
chat.write_system(f"Model: {model}")
|
|
757
1045
|
|
|
758
1046
|
def _set_key(self, provider: str, key: str, chat: ChatLog):
|
|
1047
|
+
"""Save an API key for a provider."""
|
|
1048
|
+
# Normalize provider name
|
|
1049
|
+
provider = provider.lower().strip()
|
|
1050
|
+
valid_providers = ["google", "openai", "anthropic", "groq", "xai", "polygon", "alphavantage", "exa"]
|
|
1051
|
+
|
|
1052
|
+
if provider not in valid_providers:
|
|
1053
|
+
chat.write_error(f"[E{ErrorCode.INVALID_INPUT}] Unknown provider: {provider}")
|
|
1054
|
+
chat.write_system(f"Valid providers: {', '.join(valid_providers)}")
|
|
1055
|
+
return
|
|
1056
|
+
|
|
1057
|
+
# Basic key validation
|
|
1058
|
+
key = key.strip()
|
|
1059
|
+
if len(key) < 10:
|
|
1060
|
+
chat.write_error(f"[E{ErrorCode.INVALID_INPUT}] API key seems too short")
|
|
1061
|
+
return
|
|
1062
|
+
|
|
759
1063
|
try:
|
|
760
|
-
save_api_key(
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
self.
|
|
1064
|
+
success = save_api_key(provider, key)
|
|
1065
|
+
if success:
|
|
1066
|
+
# Reload settings
|
|
1067
|
+
self.settings = get_settings()
|
|
1068
|
+
|
|
1069
|
+
# Show success with masked key
|
|
1070
|
+
masked = key[:6] + "..." + key[-4:] if len(key) > 10 else "***"
|
|
1071
|
+
chat.write_system(f"[green][ok][/green] {SIGMA} Key saved for [bold]{provider}[/bold]: {masked}")
|
|
1072
|
+
|
|
1073
|
+
# Auto-switch to this provider if it's an LLM provider and we don't have an LLM
|
|
1074
|
+
llm_providers = ["google", "openai", "anthropic", "groq", "xai"]
|
|
1075
|
+
if provider in llm_providers:
|
|
1076
|
+
if not self.llm or provider == getattr(self.settings.default_provider, 'value', ''):
|
|
1077
|
+
self._switch_provider(provider, chat)
|
|
1078
|
+
else:
|
|
1079
|
+
chat.write_error(f"[E{ErrorCode.UNKNOWN_ERROR}] Failed to save key")
|
|
764
1080
|
except Exception as e:
|
|
765
|
-
chat.write_error(str(e))
|
|
1081
|
+
chat.write_error(f"[E{ErrorCode.UNKNOWN_ERROR}] {str(e)}")
|
|
766
1082
|
|
|
767
1083
|
@work(exclusive=True)
|
|
768
1084
|
async def _process_query(self, query: str, chat: ChatLog):
|
|
769
1085
|
if not self.llm:
|
|
770
|
-
chat.write_error("No LLM. Use /keys to
|
|
1086
|
+
chat.write_error(f"[E{ErrorCode.API_KEY_MISSING}] No LLM configured. Use /keys to set up.")
|
|
771
1087
|
return
|
|
772
1088
|
|
|
773
1089
|
self.is_processing = True
|
|
@@ -777,7 +1093,7 @@ Example: /setkey google AIzaSy...
|
|
|
777
1093
|
|
|
778
1094
|
# Clear ticker highlight and start sigma animation
|
|
779
1095
|
ticker_highlight.update("")
|
|
780
|
-
indicator.set_active(True)
|
|
1096
|
+
indicator.set_active(True, mode="thinking")
|
|
781
1097
|
|
|
782
1098
|
try:
|
|
783
1099
|
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
@@ -808,10 +1124,24 @@ Example: /setkey google AIzaSy...
|
|
|
808
1124
|
if len(self.conversation) > 20:
|
|
809
1125
|
self.conversation = self.conversation[-20:]
|
|
810
1126
|
else:
|
|
811
|
-
chat.write_error("No response")
|
|
1127
|
+
chat.write_error(f"[E{ErrorCode.RESPONSE_INVALID}] No response received")
|
|
1128
|
+
|
|
1129
|
+
except SigmaError as e:
|
|
1130
|
+
tool_display.clear()
|
|
1131
|
+
# Format SigmaError nicely
|
|
1132
|
+
chat.write_error(f"[E{e.code}] {e.message}")
|
|
1133
|
+
if e.details.get("hint"):
|
|
1134
|
+
chat.write_system(f"[dim]Hint: {e.details['hint']}[/dim]")
|
|
812
1135
|
except Exception as e:
|
|
813
1136
|
tool_display.clear()
|
|
814
|
-
|
|
1137
|
+
# Try to parse common API errors
|
|
1138
|
+
error_str = str(e)
|
|
1139
|
+
if "401" in error_str or "invalid" in error_str.lower() and "key" in error_str.lower():
|
|
1140
|
+
chat.write_error(f"[E{ErrorCode.API_KEY_INVALID}] API key is invalid. Use /keys to update.")
|
|
1141
|
+
elif "429" in error_str or "rate" in error_str.lower():
|
|
1142
|
+
chat.write_error(f"[E{ErrorCode.API_KEY_RATE_LIMITED}] Rate limit hit. Wait a moment and try again.")
|
|
1143
|
+
else:
|
|
1144
|
+
chat.write_error(f"[E{ErrorCode.PROVIDER_ERROR}] {error_str[:200]}")
|
|
815
1145
|
finally:
|
|
816
1146
|
indicator.set_active(False)
|
|
817
1147
|
self.is_processing = False
|