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/charts.py
CHANGED
|
@@ -355,44 +355,62 @@ def _calculate_macd(prices: pd.Series, fast: int = 12, slow: int = 26, signal: i
|
|
|
355
355
|
|
|
356
356
|
|
|
357
357
|
def _apply_layout(fig: go.Figure, title: str, has_volume: bool):
|
|
358
|
-
"""Apply Sigma theme to chart."""
|
|
358
|
+
"""Apply Sigma theme to chart with publication-grade styling."""
|
|
359
359
|
fig.update_layout(
|
|
360
360
|
title=dict(
|
|
361
|
-
text=title,
|
|
362
|
-
font=dict(size=
|
|
361
|
+
text=f"<b>{title}</b>",
|
|
362
|
+
font=dict(size=20, color=SIGMA_THEME["text_color"], family="SF Pro Display, -apple-system, sans-serif"),
|
|
363
363
|
x=0.5,
|
|
364
|
+
xanchor="center",
|
|
364
365
|
),
|
|
365
366
|
paper_bgcolor=SIGMA_THEME["paper_color"],
|
|
366
367
|
plot_bgcolor=SIGMA_THEME["bg_color"],
|
|
367
|
-
font=dict(color=SIGMA_THEME["text_color"], family="SF
|
|
368
|
+
font=dict(color=SIGMA_THEME["text_color"], family="SF Pro Display, -apple-system, sans-serif", size=12),
|
|
368
369
|
xaxis=dict(
|
|
369
370
|
gridcolor=SIGMA_THEME["grid_color"],
|
|
370
371
|
showgrid=True,
|
|
372
|
+
gridwidth=1,
|
|
371
373
|
zeroline=False,
|
|
374
|
+
showline=True,
|
|
375
|
+
linewidth=1,
|
|
376
|
+
linecolor=SIGMA_THEME["grid_color"],
|
|
377
|
+
tickfont=dict(size=11),
|
|
372
378
|
),
|
|
373
379
|
yaxis=dict(
|
|
374
380
|
gridcolor=SIGMA_THEME["grid_color"],
|
|
375
381
|
showgrid=True,
|
|
382
|
+
gridwidth=1,
|
|
376
383
|
zeroline=False,
|
|
384
|
+
showline=True,
|
|
385
|
+
linewidth=1,
|
|
386
|
+
linecolor=SIGMA_THEME["grid_color"],
|
|
377
387
|
title_text="Price ($)",
|
|
388
|
+
tickfont=dict(size=11),
|
|
389
|
+
tickformat="$,.2f",
|
|
378
390
|
),
|
|
379
391
|
legend=dict(
|
|
380
392
|
bgcolor="rgba(0,0,0,0)",
|
|
381
|
-
font=dict(color=SIGMA_THEME["text_color"]),
|
|
393
|
+
font=dict(color=SIGMA_THEME["text_color"], size=11),
|
|
382
394
|
orientation="h",
|
|
383
395
|
yanchor="bottom",
|
|
384
396
|
y=1.02,
|
|
385
397
|
xanchor="right",
|
|
386
398
|
x=1,
|
|
387
399
|
),
|
|
388
|
-
margin=dict(l=
|
|
400
|
+
margin=dict(l=70, r=50, t=90, b=50),
|
|
389
401
|
xaxis_rangeslider_visible=False,
|
|
390
402
|
hovermode="x unified",
|
|
403
|
+
hoverlabel=dict(
|
|
404
|
+
bgcolor=SIGMA_THEME["bg_color"],
|
|
405
|
+
font_size=12,
|
|
406
|
+
font_family="SF Mono, Menlo, monospace",
|
|
407
|
+
bordercolor=SIGMA_THEME["accent"],
|
|
408
|
+
),
|
|
391
409
|
)
|
|
392
410
|
|
|
393
411
|
|
|
394
412
|
def _save_chart(fig: go.Figure, name: str) -> str:
|
|
395
|
-
"""Save chart to file and return path."""
|
|
413
|
+
"""Save chart to file with maximum quality and return path."""
|
|
396
414
|
|
|
397
415
|
# Create charts directory
|
|
398
416
|
charts_dir = os.path.expanduser("~/.sigma/charts")
|
|
@@ -402,6 +420,13 @@ def _save_chart(fig: go.Figure, name: str) -> str:
|
|
|
402
420
|
filename = f"{name}_{timestamp}.png"
|
|
403
421
|
filepath = os.path.join(charts_dir, filename)
|
|
404
422
|
|
|
405
|
-
|
|
423
|
+
# High quality export settings
|
|
424
|
+
fig.write_image(
|
|
425
|
+
filepath,
|
|
426
|
+
width=1600, # Higher resolution
|
|
427
|
+
height=1000, # Better aspect ratio
|
|
428
|
+
scale=3, # 3x scaling for retina/high-DPI
|
|
429
|
+
engine="kaleido"
|
|
430
|
+
)
|
|
406
431
|
|
|
407
432
|
return filepath
|
sigma/cli.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""CLI entry point for Sigma v3.
|
|
1
|
+
"""CLI entry point for Sigma v3.4.1."""
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import json
|
|
@@ -17,7 +17,7 @@ from .config import (
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
__version__ = "3.
|
|
20
|
+
__version__ = "3.4.1"
|
|
21
21
|
|
|
22
22
|
console = Console()
|
|
23
23
|
|
|
@@ -25,14 +25,14 @@ console = Console()
|
|
|
25
25
|
def show_banner():
|
|
26
26
|
"""Show the Sigma banner."""
|
|
27
27
|
banner = """
|
|
28
|
-
[bold
|
|
29
|
-
[bold
|
|
30
|
-
[bold
|
|
31
|
-
[bold
|
|
32
|
-
[bold
|
|
33
|
-
[bold
|
|
28
|
+
[bold #3b82f6]███████╗██╗ ██████╗ ███╗ ███╗ █████╗ [/bold #3b82f6]
|
|
29
|
+
[bold #60a5fa]██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗[/bold #60a5fa]
|
|
30
|
+
[bold #93c5fd]███████╗██║██║ ███╗██╔████╔██║███████║[/bold #93c5fd]
|
|
31
|
+
[bold #60a5fa]╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║[/bold #60a5fa]
|
|
32
|
+
[bold #3b82f6]███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║[/bold #3b82f6]
|
|
33
|
+
[bold #1d4ed8]╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold #1d4ed8]
|
|
34
34
|
|
|
35
|
-
[dim]v3.
|
|
35
|
+
[dim]v3.4.1[/dim] [bold cyan]σ[/bold cyan] [bold]Finance Research Agent[/bold]
|
|
36
36
|
"""
|
|
37
37
|
console.print(banner)
|
|
38
38
|
|
|
@@ -41,7 +41,7 @@ def main():
|
|
|
41
41
|
"""Main CLI entry point."""
|
|
42
42
|
parser = argparse.ArgumentParser(
|
|
43
43
|
prog="sigma",
|
|
44
|
-
description="Sigma v3.
|
|
44
|
+
description="Sigma v3.4.1 - Finance Research Agent",
|
|
45
45
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
46
46
|
epilog="""
|
|
47
47
|
Examples:
|
|
@@ -148,7 +148,7 @@ Examples:
|
|
|
148
148
|
mark_first_run_complete() # Always mark complete to not ask again
|
|
149
149
|
|
|
150
150
|
if result:
|
|
151
|
-
console.print("\n[bold green]
|
|
151
|
+
console.print("\n[bold green]Setup complete![/bold green]")
|
|
152
152
|
console.print("[dim]Launching Sigma...[/dim]\n")
|
|
153
153
|
import time
|
|
154
154
|
time.sleep(1) # Brief pause for user to see message
|
sigma/config.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
"""Configuration management for Sigma v3.
|
|
1
|
+
"""Configuration management for Sigma v3.4.1."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
5
|
import subprocess
|
|
6
|
-
from enum import Enum
|
|
6
|
+
from enum import Enum, IntEnum
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Optional, Tuple
|
|
9
9
|
|
|
@@ -11,7 +11,56 @@ from pydantic import Field
|
|
|
11
11
|
from pydantic_settings import BaseSettings
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
__version__ = "3.
|
|
14
|
+
__version__ = "3.4.1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ErrorCode(IntEnum):
|
|
18
|
+
"""Error codes for consistent error handling."""
|
|
19
|
+
# General errors (1000-1099)
|
|
20
|
+
UNKNOWN_ERROR = 1000
|
|
21
|
+
INVALID_INPUT = 1001
|
|
22
|
+
TIMEOUT = 1002
|
|
23
|
+
|
|
24
|
+
# API Key errors (1100-1199)
|
|
25
|
+
API_KEY_MISSING = 1100
|
|
26
|
+
API_KEY_INVALID = 1101
|
|
27
|
+
API_KEY_EXPIRED = 1102
|
|
28
|
+
API_KEY_RATE_LIMITED = 1103
|
|
29
|
+
|
|
30
|
+
# Provider errors (1200-1299)
|
|
31
|
+
PROVIDER_UNAVAILABLE = 1200
|
|
32
|
+
PROVIDER_ERROR = 1201
|
|
33
|
+
MODEL_NOT_FOUND = 1202
|
|
34
|
+
MODEL_DEPRECATED = 1203
|
|
35
|
+
|
|
36
|
+
# Data errors (1300-1399)
|
|
37
|
+
SYMBOL_NOT_FOUND = 1300
|
|
38
|
+
DATA_UNAVAILABLE = 1301
|
|
39
|
+
MARKET_CLOSED = 1302
|
|
40
|
+
RATE_LIMIT_EXCEEDED = 1303
|
|
41
|
+
|
|
42
|
+
# Network errors (1400-1499)
|
|
43
|
+
CONNECTION_ERROR = 1400
|
|
44
|
+
REQUEST_FAILED = 1401
|
|
45
|
+
RESPONSE_INVALID = 1402
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SigmaError(Exception):
|
|
49
|
+
"""Custom exception with error codes."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, code: ErrorCode, message: str, details: Optional[dict] = None):
|
|
52
|
+
self.code = code
|
|
53
|
+
self.message = message
|
|
54
|
+
self.details = details or {}
|
|
55
|
+
super().__init__(f"[E{code}] {message}")
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
return {
|
|
59
|
+
"error_code": int(self.code),
|
|
60
|
+
"error_name": self.code.name,
|
|
61
|
+
"message": self.message,
|
|
62
|
+
"details": self.details
|
|
63
|
+
}
|
|
15
64
|
|
|
16
65
|
|
|
17
66
|
class LLMProvider(str, Enum):
|
|
@@ -24,14 +73,41 @@ class LLMProvider(str, Enum):
|
|
|
24
73
|
OLLAMA = "ollama"
|
|
25
74
|
|
|
26
75
|
|
|
27
|
-
# Available models per provider
|
|
76
|
+
# Available models per provider (Feb 2026 - REAL API NAMES)
|
|
28
77
|
AVAILABLE_MODELS = {
|
|
29
|
-
"google": [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
78
|
+
"google": [
|
|
79
|
+
"gemini-3-flash-preview", # Fast multimodal, Pro-level at Flash speed
|
|
80
|
+
"gemini-3-pro-preview", # Multimodal reasoning (1M tokens)
|
|
81
|
+
"gemini-3-pro-image-preview", # Image+text focus (65K tokens)
|
|
82
|
+
],
|
|
83
|
+
"openai": [
|
|
84
|
+
"gpt-5", # Flagship general/agentic, multimodal (256K)
|
|
85
|
+
"gpt-5-mini", # Cost-efficient variant (256K)
|
|
86
|
+
"gpt-5.2", # Enterprise knowledge work, advanced reasoning
|
|
87
|
+
"gpt-5-nano", # Ultra-cheap/lightweight
|
|
88
|
+
"o3", # Advanced reasoning
|
|
89
|
+
"o3-mini", # Fast reasoning
|
|
90
|
+
],
|
|
91
|
+
"anthropic": [
|
|
92
|
+
"claude-sonnet-4-20250514", # Latest Sonnet
|
|
93
|
+
"claude-opus-4-20250514", # Latest Opus
|
|
94
|
+
],
|
|
95
|
+
"groq": [
|
|
96
|
+
"llama-3.3-70b-versatile", # Most capable free
|
|
97
|
+
"llama-3.3-8b-instant", # Fast free
|
|
98
|
+
"mixtral-8x7b-32768", # Good balance free
|
|
99
|
+
],
|
|
100
|
+
"xai": [
|
|
101
|
+
"grok-3", # Latest full capability
|
|
102
|
+
"grok-3-mini", # Latest fast variant
|
|
103
|
+
],
|
|
104
|
+
"ollama": [
|
|
105
|
+
"llama3.3", # Latest local LLM
|
|
106
|
+
"llama3.2",
|
|
107
|
+
"mistral",
|
|
108
|
+
"phi4", # Latest Phi
|
|
109
|
+
"qwen2.5", # Latest Qwen
|
|
110
|
+
],
|
|
35
111
|
}
|
|
36
112
|
|
|
37
113
|
# Config directory
|
|
@@ -41,8 +117,40 @@ FIRST_RUN_MARKER = CONFIG_DIR / ".first_run_complete"
|
|
|
41
117
|
|
|
42
118
|
|
|
43
119
|
def is_first_run() -> bool:
|
|
44
|
-
"""
|
|
45
|
-
|
|
120
|
+
"""
|
|
121
|
+
Check if this is the first run of the application.
|
|
122
|
+
|
|
123
|
+
Returns False (not first run) if:
|
|
124
|
+
- The first run marker exists, OR
|
|
125
|
+
- A config file exists with at least one API key configured
|
|
126
|
+
|
|
127
|
+
This ensures users who upgrade from older versions or manually
|
|
128
|
+
configure their ~/.sigma/config.env don't see the setup wizard.
|
|
129
|
+
"""
|
|
130
|
+
# Check for explicit marker
|
|
131
|
+
if FIRST_RUN_MARKER.exists():
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
# Check if config file exists and has API keys
|
|
135
|
+
if CONFIG_FILE.exists():
|
|
136
|
+
try:
|
|
137
|
+
content = CONFIG_FILE.read_text()
|
|
138
|
+
# Check if any API key is set (not empty)
|
|
139
|
+
api_keys = ["GOOGLE_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY",
|
|
140
|
+
"GROQ_API_KEY", "XAI_API_KEY"]
|
|
141
|
+
for key in api_keys:
|
|
142
|
+
# Look for KEY=value where value is not empty
|
|
143
|
+
for line in content.splitlines():
|
|
144
|
+
if line.startswith(f"{key}="):
|
|
145
|
+
value = line.split("=", 1)[1].strip()
|
|
146
|
+
if value and value not in ('""', "''", ""):
|
|
147
|
+
# Found a configured API key - not first run
|
|
148
|
+
mark_first_run_complete() # Create marker for future
|
|
149
|
+
return False
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
return True
|
|
46
154
|
|
|
47
155
|
|
|
48
156
|
def mark_first_run_complete() -> None:
|
|
@@ -205,22 +313,22 @@ class Settings(BaseSettings):
|
|
|
205
313
|
|
|
206
314
|
# Provider settings
|
|
207
315
|
default_provider: LLMProvider = LLMProvider.GOOGLE
|
|
208
|
-
default_model: str = Field(default="gemini-
|
|
316
|
+
default_model: str = Field(default="gemini-3-flash-preview", alias="DEFAULT_MODEL")
|
|
209
317
|
|
|
210
|
-
# API Keys
|
|
318
|
+
# LLM API Keys
|
|
211
319
|
google_api_key: Optional[str] = Field(default=None, alias="GOOGLE_API_KEY")
|
|
212
320
|
openai_api_key: Optional[str] = Field(default=None, alias="OPENAI_API_KEY")
|
|
213
321
|
anthropic_api_key: Optional[str] = Field(default=None, alias="ANTHROPIC_API_KEY")
|
|
214
322
|
groq_api_key: Optional[str] = Field(default=None, alias="GROQ_API_KEY")
|
|
215
323
|
xai_api_key: Optional[str] = Field(default=None, alias="XAI_API_KEY")
|
|
216
324
|
|
|
217
|
-
# Model settings
|
|
218
|
-
google_model: str = "gemini-
|
|
219
|
-
openai_model: str = "gpt-
|
|
325
|
+
# Model settings - REAL API NAMES (Feb 2026)
|
|
326
|
+
google_model: str = "gemini-3-flash-preview"
|
|
327
|
+
openai_model: str = "gpt-5"
|
|
220
328
|
anthropic_model: str = "claude-sonnet-4-20250514"
|
|
221
329
|
groq_model: str = "llama-3.3-70b-versatile"
|
|
222
|
-
xai_model: str = "grok-
|
|
223
|
-
ollama_model: str = "llama3.
|
|
330
|
+
xai_model: str = "grok-3"
|
|
331
|
+
ollama_model: str = "llama3.3"
|
|
224
332
|
|
|
225
333
|
# Ollama settings
|
|
226
334
|
ollama_host: str = "http://localhost:11434"
|
|
@@ -232,6 +340,7 @@ class Settings(BaseSettings):
|
|
|
232
340
|
|
|
233
341
|
# Data API keys
|
|
234
342
|
alpha_vantage_api_key: str = Field(default="6ER128DD3NQUPTVC", alias="ALPHA_VANTAGE_API_KEY")
|
|
343
|
+
polygon_api_key: Optional[str] = Field(default=None, alias="POLYGON_API_KEY")
|
|
235
344
|
exa_api_key: Optional[str] = Field(default=None, alias="EXA_API_KEY")
|
|
236
345
|
|
|
237
346
|
class Config:
|
|
@@ -287,38 +396,70 @@ def get_settings() -> Settings:
|
|
|
287
396
|
return Settings()
|
|
288
397
|
|
|
289
398
|
|
|
290
|
-
def save_api_key(provider: str, key: str) ->
|
|
291
|
-
"""Save an API key to the config file."""
|
|
399
|
+
def save_api_key(provider: str, key: str) -> bool:
|
|
400
|
+
"""Save an API key to the config file. Returns True on success."""
|
|
292
401
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
293
402
|
|
|
294
403
|
# Read existing config
|
|
295
404
|
config = {}
|
|
296
405
|
if CONFIG_FILE.exists():
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
line
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
406
|
+
try:
|
|
407
|
+
with open(CONFIG_FILE) as f:
|
|
408
|
+
for line in f:
|
|
409
|
+
line = line.strip()
|
|
410
|
+
if "=" in line and not line.startswith("#"):
|
|
411
|
+
k, v = line.split("=", 1)
|
|
412
|
+
config[k] = v
|
|
413
|
+
except IOError:
|
|
414
|
+
pass
|
|
303
415
|
|
|
304
|
-
#
|
|
416
|
+
# Map provider names to config keys (LLM + Data providers)
|
|
305
417
|
key_map = {
|
|
418
|
+
# LLM providers
|
|
306
419
|
"google": "GOOGLE_API_KEY",
|
|
307
420
|
"openai": "OPENAI_API_KEY",
|
|
308
421
|
"anthropic": "ANTHROPIC_API_KEY",
|
|
309
422
|
"groq": "GROQ_API_KEY",
|
|
310
423
|
"xai": "XAI_API_KEY",
|
|
424
|
+
# Data providers
|
|
425
|
+
"polygon": "POLYGON_API_KEY",
|
|
426
|
+
"alphavantage": "ALPHA_VANTAGE_API_KEY",
|
|
427
|
+
"exa": "EXA_API_KEY",
|
|
311
428
|
}
|
|
312
429
|
|
|
313
430
|
env_key = key_map.get(provider.lower())
|
|
314
|
-
if env_key:
|
|
315
|
-
|
|
431
|
+
if not env_key:
|
|
432
|
+
return False
|
|
433
|
+
|
|
434
|
+
config[env_key] = key
|
|
316
435
|
|
|
317
436
|
# Write back
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
f.write(f"{
|
|
437
|
+
try:
|
|
438
|
+
with open(CONFIG_FILE, "w") as f:
|
|
439
|
+
f.write("# Sigma Configuration\n")
|
|
440
|
+
f.write(f"# Updated: {__import__('datetime').datetime.now().isoformat()}\n\n")
|
|
441
|
+
|
|
442
|
+
# Group by type for readability
|
|
443
|
+
llm_keys = ["GOOGLE_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GROQ_API_KEY", "XAI_API_KEY"]
|
|
444
|
+
data_keys = ["POLYGON_API_KEY", "ALPHA_VANTAGE_API_KEY", "EXA_API_KEY"]
|
|
445
|
+
|
|
446
|
+
f.write("# LLM Provider Keys\n")
|
|
447
|
+
for k in llm_keys:
|
|
448
|
+
if k in config:
|
|
449
|
+
f.write(f"{k}={config[k]}\n")
|
|
450
|
+
|
|
451
|
+
f.write("\n# Data Provider Keys\n")
|
|
452
|
+
for k in data_keys:
|
|
453
|
+
if k in config:
|
|
454
|
+
f.write(f"{k}={config[k]}\n")
|
|
455
|
+
|
|
456
|
+
f.write("\n# Other Settings\n")
|
|
457
|
+
for k, v in sorted(config.items()):
|
|
458
|
+
if k not in llm_keys and k not in data_keys:
|
|
459
|
+
f.write(f"{k}={v}\n")
|
|
460
|
+
return True
|
|
461
|
+
except IOError:
|
|
462
|
+
return False
|
|
322
463
|
|
|
323
464
|
|
|
324
465
|
def get_api_key(provider: str) -> Optional[str]:
|
sigma/llm.py
CHANGED
|
@@ -7,7 +7,7 @@ import time
|
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
from typing import Any, AsyncIterator, Callable, Optional
|
|
9
9
|
|
|
10
|
-
from sigma.config import LLMProvider, get_settings
|
|
10
|
+
from sigma.config import LLMProvider, get_settings, ErrorCode, SigmaError
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
# Rate limiting configuration
|
|
@@ -47,14 +47,14 @@ class RateLimiter:
|
|
|
47
47
|
self.request_count += 1
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
# Global rate limiters per provider
|
|
50
|
+
# Global rate limiters per provider (generous limits to avoid rate limiting)
|
|
51
51
|
_rate_limiters = {
|
|
52
|
-
"google": RateLimiter(requests_per_minute=
|
|
53
|
-
"openai": RateLimiter(requests_per_minute=
|
|
54
|
-
"anthropic": RateLimiter(requests_per_minute=
|
|
55
|
-
"groq": RateLimiter(requests_per_minute=30, min_interval=0.2),
|
|
56
|
-
"xai": RateLimiter(requests_per_minute=
|
|
57
|
-
"ollama": RateLimiter(requests_per_minute=
|
|
52
|
+
"google": RateLimiter(requests_per_minute=60, min_interval=0.2), # Gemini free tier is generous
|
|
53
|
+
"openai": RateLimiter(requests_per_minute=60, min_interval=0.2), # GPT-5 tier 1+
|
|
54
|
+
"anthropic": RateLimiter(requests_per_minute=40, min_interval=0.3), # Claude standard
|
|
55
|
+
"groq": RateLimiter(requests_per_minute=30, min_interval=0.2), # Groq free tier
|
|
56
|
+
"xai": RateLimiter(requests_per_minute=30, min_interval=0.3), # Grok standard
|
|
57
|
+
"ollama": RateLimiter(requests_per_minute=120, min_interval=0.05), # Local, no limits
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
|
|
@@ -601,6 +601,123 @@ class OllamaLLM(BaseLLM):
|
|
|
601
601
|
return "\n".join(lines)
|
|
602
602
|
|
|
603
603
|
|
|
604
|
+
class XaiLLM(BaseLLM):
|
|
605
|
+
"""xAI Grok client (uses OpenAI-compatible API)."""
|
|
606
|
+
|
|
607
|
+
provider_name = "xai"
|
|
608
|
+
|
|
609
|
+
def __init__(self, api_key: str, model: str):
|
|
610
|
+
from openai import AsyncOpenAI
|
|
611
|
+
self.client = AsyncOpenAI(
|
|
612
|
+
api_key=api_key,
|
|
613
|
+
base_url="https://api.x.ai/v1"
|
|
614
|
+
)
|
|
615
|
+
self.model = model
|
|
616
|
+
|
|
617
|
+
async def generate(
|
|
618
|
+
self,
|
|
619
|
+
messages: list[dict],
|
|
620
|
+
tools: Optional[list[dict]] = None,
|
|
621
|
+
on_tool_call: Optional[Callable] = None,
|
|
622
|
+
) -> str:
|
|
623
|
+
await self._rate_limit()
|
|
624
|
+
|
|
625
|
+
try:
|
|
626
|
+
kwargs = {
|
|
627
|
+
"model": self.model,
|
|
628
|
+
"messages": messages,
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if tools:
|
|
632
|
+
kwargs["tools"] = tools
|
|
633
|
+
kwargs["tool_choice"] = "auto"
|
|
634
|
+
|
|
635
|
+
response = await self.client.chat.completions.create(**kwargs)
|
|
636
|
+
message = response.choices[0].message
|
|
637
|
+
|
|
638
|
+
# Handle tool calls
|
|
639
|
+
if message.tool_calls and on_tool_call:
|
|
640
|
+
tool_results = []
|
|
641
|
+
for tc in message.tool_calls:
|
|
642
|
+
args = json.loads(tc.function.arguments)
|
|
643
|
+
result = await on_tool_call(tc.function.name, args)
|
|
644
|
+
tool_results.append({
|
|
645
|
+
"tool_call_id": tc.id,
|
|
646
|
+
"role": "tool",
|
|
647
|
+
"content": json.dumps(result)
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
# Continue with tool results
|
|
651
|
+
messages = messages + [message.model_dump()] + tool_results
|
|
652
|
+
return await self.generate(messages, tools, on_tool_call)
|
|
653
|
+
|
|
654
|
+
return message.content or ""
|
|
655
|
+
|
|
656
|
+
except Exception as e:
|
|
657
|
+
error_str = str(e)
|
|
658
|
+
if "401" in error_str or "invalid_api_key" in error_str:
|
|
659
|
+
raise SigmaError(
|
|
660
|
+
ErrorCode.API_KEY_INVALID,
|
|
661
|
+
"xAI API key is invalid",
|
|
662
|
+
{"provider": "xai"}
|
|
663
|
+
)
|
|
664
|
+
elif "429" in error_str or "rate_limit" in error_str:
|
|
665
|
+
raise SigmaError(
|
|
666
|
+
ErrorCode.API_KEY_RATE_LIMITED,
|
|
667
|
+
"xAI rate limit exceeded. Please wait.",
|
|
668
|
+
{"provider": "xai"}
|
|
669
|
+
)
|
|
670
|
+
raise
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _parse_api_error(error: Exception, provider: str) -> SigmaError:
|
|
674
|
+
"""Parse API errors into SigmaError with proper codes."""
|
|
675
|
+
error_str = str(error).lower()
|
|
676
|
+
|
|
677
|
+
if "401" in error_str or "invalid_api_key" in error_str or "api key not valid" in error_str:
|
|
678
|
+
return SigmaError(
|
|
679
|
+
ErrorCode.API_KEY_INVALID,
|
|
680
|
+
f"{provider.title()} API key is invalid. Check your key at /keys",
|
|
681
|
+
{"provider": provider, "original_error": str(error)[:200]}
|
|
682
|
+
)
|
|
683
|
+
elif "403" in error_str or "forbidden" in error_str:
|
|
684
|
+
return SigmaError(
|
|
685
|
+
ErrorCode.API_KEY_INVALID,
|
|
686
|
+
f"{provider.title()} API key doesn't have access to this model",
|
|
687
|
+
{"provider": provider}
|
|
688
|
+
)
|
|
689
|
+
elif "429" in error_str or "rate_limit" in error_str or "quota" in error_str:
|
|
690
|
+
return SigmaError(
|
|
691
|
+
ErrorCode.API_KEY_RATE_LIMITED,
|
|
692
|
+
f"{provider.title()} rate limit exceeded. Wait a moment.",
|
|
693
|
+
{"provider": provider}
|
|
694
|
+
)
|
|
695
|
+
elif "404" in error_str or "not found" in error_str or "does not exist" in error_str:
|
|
696
|
+
return SigmaError(
|
|
697
|
+
ErrorCode.MODEL_NOT_FOUND,
|
|
698
|
+
f"Model not found. Try /models to see available options.",
|
|
699
|
+
{"provider": provider}
|
|
700
|
+
)
|
|
701
|
+
elif "timeout" in error_str:
|
|
702
|
+
return SigmaError(
|
|
703
|
+
ErrorCode.TIMEOUT,
|
|
704
|
+
"Request timed out. Try a simpler query.",
|
|
705
|
+
{"provider": provider}
|
|
706
|
+
)
|
|
707
|
+
elif "connection" in error_str:
|
|
708
|
+
return SigmaError(
|
|
709
|
+
ErrorCode.CONNECTION_ERROR,
|
|
710
|
+
f"Cannot connect to {provider.title()}. Check internet.",
|
|
711
|
+
{"provider": provider}
|
|
712
|
+
)
|
|
713
|
+
else:
|
|
714
|
+
return SigmaError(
|
|
715
|
+
ErrorCode.PROVIDER_ERROR,
|
|
716
|
+
f"{provider.title()} error: {str(error)[:150]}",
|
|
717
|
+
{"provider": provider, "original_error": str(error)[:300]}
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
|
|
604
721
|
def get_llm(provider: LLMProvider, model: Optional[str] = None) -> BaseLLM:
|
|
605
722
|
"""Get LLM client for a provider."""
|
|
606
723
|
settings = get_settings()
|
|
@@ -611,29 +728,59 @@ def get_llm(provider: LLMProvider, model: Optional[str] = None) -> BaseLLM:
|
|
|
611
728
|
if provider == LLMProvider.GOOGLE:
|
|
612
729
|
api_key = settings.google_api_key
|
|
613
730
|
if not api_key:
|
|
614
|
-
raise
|
|
731
|
+
raise SigmaError(
|
|
732
|
+
ErrorCode.API_KEY_MISSING,
|
|
733
|
+
"Google API key not configured. Use /keys to set up.",
|
|
734
|
+
{"provider": "google", "hint": "Get key at: https://aistudio.google.com/apikey"}
|
|
735
|
+
)
|
|
615
736
|
return GoogleLLM(api_key, model)
|
|
616
737
|
|
|
617
738
|
elif provider == LLMProvider.OPENAI:
|
|
618
739
|
api_key = settings.openai_api_key
|
|
619
740
|
if not api_key:
|
|
620
|
-
raise
|
|
741
|
+
raise SigmaError(
|
|
742
|
+
ErrorCode.API_KEY_MISSING,
|
|
743
|
+
"OpenAI API key not configured. Use /keys to set up.",
|
|
744
|
+
{"provider": "openai", "hint": "Get key at: https://platform.openai.com/api-keys"}
|
|
745
|
+
)
|
|
621
746
|
return OpenAILLM(api_key, model)
|
|
622
747
|
|
|
623
748
|
elif provider == LLMProvider.ANTHROPIC:
|
|
624
749
|
api_key = settings.anthropic_api_key
|
|
625
750
|
if not api_key:
|
|
626
|
-
raise
|
|
751
|
+
raise SigmaError(
|
|
752
|
+
ErrorCode.API_KEY_MISSING,
|
|
753
|
+
"Anthropic API key not configured. Use /keys to set up.",
|
|
754
|
+
{"provider": "anthropic", "hint": "Get key at: https://console.anthropic.com/settings/keys"}
|
|
755
|
+
)
|
|
627
756
|
return AnthropicLLM(api_key, model)
|
|
628
757
|
|
|
629
758
|
elif provider == LLMProvider.GROQ:
|
|
630
759
|
api_key = settings.groq_api_key
|
|
631
760
|
if not api_key:
|
|
632
|
-
raise
|
|
761
|
+
raise SigmaError(
|
|
762
|
+
ErrorCode.API_KEY_MISSING,
|
|
763
|
+
"Groq API key not configured. Use /keys to set up.",
|
|
764
|
+
{"provider": "groq", "hint": "Get key at: https://console.groq.com/keys"}
|
|
765
|
+
)
|
|
633
766
|
return GroqLLM(api_key, model)
|
|
634
767
|
|
|
768
|
+
elif provider == LLMProvider.XAI:
|
|
769
|
+
api_key = settings.xai_api_key
|
|
770
|
+
if not api_key:
|
|
771
|
+
raise SigmaError(
|
|
772
|
+
ErrorCode.API_KEY_MISSING,
|
|
773
|
+
"xAI API key not configured. Use /keys to set up.",
|
|
774
|
+
{"provider": "xai", "hint": "Get key at: https://console.x.ai"}
|
|
775
|
+
)
|
|
776
|
+
return XaiLLM(api_key, model)
|
|
777
|
+
|
|
635
778
|
elif provider == LLMProvider.OLLAMA:
|
|
636
779
|
return OllamaLLM(settings.ollama_host, model)
|
|
637
780
|
|
|
638
781
|
else:
|
|
639
|
-
raise
|
|
782
|
+
raise SigmaError(
|
|
783
|
+
ErrorCode.PROVIDER_UNAVAILABLE,
|
|
784
|
+
f"Unsupported provider: {provider}",
|
|
785
|
+
{"provider": str(provider)}
|
|
786
|
+
)
|