sigma-terminal 3.3.1__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 +15 -15
- sigma/tools.py +277 -3
- sigma_terminal-3.4.0.dist-info/METADATA +264 -0
- {sigma_terminal-3.3.1.dist-info → sigma_terminal-3.4.0.dist-info}/RECORD +13 -13
- sigma_terminal-3.3.1.dist-info/METADATA +0 -444
- {sigma_terminal-3.3.1.dist-info → sigma_terminal-3.4.0.dist-info}/WHEEL +0 -0
- {sigma_terminal-3.3.1.dist-info → sigma_terminal-3.4.0.dist-info}/entry_points.txt +0 -0
- {sigma_terminal-3.3.1.dist-info → sigma_terminal-3.4.0.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.0."""
|
|
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.0"
|
|
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.0[/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.0 - 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][ok] 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.0."""
|
|
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.0"
|
|
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,38 @@ class LLMProvider(str, Enum):
|
|
|
24
73
|
OLLAMA = "ollama"
|
|
25
74
|
|
|
26
75
|
|
|
27
|
-
# Available models per provider
|
|
76
|
+
# Available models per provider (Feb 2026 - LATEST MODELS ONLY)
|
|
28
77
|
AVAILABLE_MODELS = {
|
|
29
|
-
"google": [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
78
|
+
"google": [
|
|
79
|
+
"gemini-3-flash", # Latest fast model
|
|
80
|
+
"gemini-3-pro", # Latest capable model
|
|
81
|
+
],
|
|
82
|
+
"openai": [
|
|
83
|
+
"gpt-5", # Latest flagship
|
|
84
|
+
"gpt-5-mini", # Latest efficient
|
|
85
|
+
"o3", # Latest reasoning
|
|
86
|
+
"o3-mini", # Fast reasoning
|
|
87
|
+
],
|
|
88
|
+
"anthropic": [
|
|
89
|
+
"claude-sonnet-4-20250514", # Latest Sonnet
|
|
90
|
+
"claude-opus-4-20250514", # Latest Opus
|
|
91
|
+
],
|
|
92
|
+
"groq": [
|
|
93
|
+
"llama-3.3-70b-versatile", # Most capable free
|
|
94
|
+
"llama-3.3-8b-instant", # Fast free
|
|
95
|
+
"mixtral-8x7b-32768", # Good balance free
|
|
96
|
+
],
|
|
97
|
+
"xai": [
|
|
98
|
+
"grok-3", # Latest full capability
|
|
99
|
+
"grok-3-mini", # Latest fast variant
|
|
100
|
+
],
|
|
101
|
+
"ollama": [
|
|
102
|
+
"llama3.3", # Latest local LLM
|
|
103
|
+
"llama3.2",
|
|
104
|
+
"mistral",
|
|
105
|
+
"phi4", # Latest Phi
|
|
106
|
+
"qwen2.5", # Latest Qwen
|
|
107
|
+
],
|
|
35
108
|
}
|
|
36
109
|
|
|
37
110
|
# Config directory
|
|
@@ -41,8 +114,40 @@ FIRST_RUN_MARKER = CONFIG_DIR / ".first_run_complete"
|
|
|
41
114
|
|
|
42
115
|
|
|
43
116
|
def is_first_run() -> bool:
|
|
44
|
-
"""
|
|
45
|
-
|
|
117
|
+
"""
|
|
118
|
+
Check if this is the first run of the application.
|
|
119
|
+
|
|
120
|
+
Returns False (not first run) if:
|
|
121
|
+
- The first run marker exists, OR
|
|
122
|
+
- A config file exists with at least one API key configured
|
|
123
|
+
|
|
124
|
+
This ensures users who upgrade from older versions or manually
|
|
125
|
+
configure their ~/.sigma/config.env don't see the setup wizard.
|
|
126
|
+
"""
|
|
127
|
+
# Check for explicit marker
|
|
128
|
+
if FIRST_RUN_MARKER.exists():
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# Check if config file exists and has API keys
|
|
132
|
+
if CONFIG_FILE.exists():
|
|
133
|
+
try:
|
|
134
|
+
content = CONFIG_FILE.read_text()
|
|
135
|
+
# Check if any API key is set (not empty)
|
|
136
|
+
api_keys = ["GOOGLE_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY",
|
|
137
|
+
"GROQ_API_KEY", "XAI_API_KEY"]
|
|
138
|
+
for key in api_keys:
|
|
139
|
+
# Look for KEY=value where value is not empty
|
|
140
|
+
for line in content.splitlines():
|
|
141
|
+
if line.startswith(f"{key}="):
|
|
142
|
+
value = line.split("=", 1)[1].strip()
|
|
143
|
+
if value and value not in ('""', "''", ""):
|
|
144
|
+
# Found a configured API key - not first run
|
|
145
|
+
mark_first_run_complete() # Create marker for future
|
|
146
|
+
return False
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
return True
|
|
46
151
|
|
|
47
152
|
|
|
48
153
|
def mark_first_run_complete() -> None:
|
|
@@ -205,22 +310,22 @@ class Settings(BaseSettings):
|
|
|
205
310
|
|
|
206
311
|
# Provider settings
|
|
207
312
|
default_provider: LLMProvider = LLMProvider.GOOGLE
|
|
208
|
-
default_model: str = Field(default="gemini-
|
|
313
|
+
default_model: str = Field(default="gemini-3-flash", alias="DEFAULT_MODEL")
|
|
209
314
|
|
|
210
|
-
# API Keys
|
|
315
|
+
# LLM API Keys
|
|
211
316
|
google_api_key: Optional[str] = Field(default=None, alias="GOOGLE_API_KEY")
|
|
212
317
|
openai_api_key: Optional[str] = Field(default=None, alias="OPENAI_API_KEY")
|
|
213
318
|
anthropic_api_key: Optional[str] = Field(default=None, alias="ANTHROPIC_API_KEY")
|
|
214
319
|
groq_api_key: Optional[str] = Field(default=None, alias="GROQ_API_KEY")
|
|
215
320
|
xai_api_key: Optional[str] = Field(default=None, alias="XAI_API_KEY")
|
|
216
321
|
|
|
217
|
-
# Model settings
|
|
218
|
-
google_model: str = "gemini-
|
|
219
|
-
openai_model: str = "gpt-
|
|
322
|
+
# Model settings - LATEST MODELS ONLY (Feb 2026)
|
|
323
|
+
google_model: str = "gemini-3-flash"
|
|
324
|
+
openai_model: str = "gpt-5"
|
|
220
325
|
anthropic_model: str = "claude-sonnet-4-20250514"
|
|
221
326
|
groq_model: str = "llama-3.3-70b-versatile"
|
|
222
|
-
xai_model: str = "grok-
|
|
223
|
-
ollama_model: str = "llama3.
|
|
327
|
+
xai_model: str = "grok-3"
|
|
328
|
+
ollama_model: str = "llama3.3"
|
|
224
329
|
|
|
225
330
|
# Ollama settings
|
|
226
331
|
ollama_host: str = "http://localhost:11434"
|
|
@@ -232,6 +337,7 @@ class Settings(BaseSettings):
|
|
|
232
337
|
|
|
233
338
|
# Data API keys
|
|
234
339
|
alpha_vantage_api_key: str = Field(default="6ER128DD3NQUPTVC", alias="ALPHA_VANTAGE_API_KEY")
|
|
340
|
+
polygon_api_key: Optional[str] = Field(default=None, alias="POLYGON_API_KEY")
|
|
235
341
|
exa_api_key: Optional[str] = Field(default=None, alias="EXA_API_KEY")
|
|
236
342
|
|
|
237
343
|
class Config:
|
|
@@ -287,38 +393,70 @@ def get_settings() -> Settings:
|
|
|
287
393
|
return Settings()
|
|
288
394
|
|
|
289
395
|
|
|
290
|
-
def save_api_key(provider: str, key: str) ->
|
|
291
|
-
"""Save an API key to the config file."""
|
|
396
|
+
def save_api_key(provider: str, key: str) -> bool:
|
|
397
|
+
"""Save an API key to the config file. Returns True on success."""
|
|
292
398
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
293
399
|
|
|
294
400
|
# Read existing config
|
|
295
401
|
config = {}
|
|
296
402
|
if CONFIG_FILE.exists():
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
line
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
403
|
+
try:
|
|
404
|
+
with open(CONFIG_FILE) as f:
|
|
405
|
+
for line in f:
|
|
406
|
+
line = line.strip()
|
|
407
|
+
if "=" in line and not line.startswith("#"):
|
|
408
|
+
k, v = line.split("=", 1)
|
|
409
|
+
config[k] = v
|
|
410
|
+
except IOError:
|
|
411
|
+
pass
|
|
303
412
|
|
|
304
|
-
#
|
|
413
|
+
# Map provider names to config keys (LLM + Data providers)
|
|
305
414
|
key_map = {
|
|
415
|
+
# LLM providers
|
|
306
416
|
"google": "GOOGLE_API_KEY",
|
|
307
417
|
"openai": "OPENAI_API_KEY",
|
|
308
418
|
"anthropic": "ANTHROPIC_API_KEY",
|
|
309
419
|
"groq": "GROQ_API_KEY",
|
|
310
420
|
"xai": "XAI_API_KEY",
|
|
421
|
+
# Data providers
|
|
422
|
+
"polygon": "POLYGON_API_KEY",
|
|
423
|
+
"alphavantage": "ALPHA_VANTAGE_API_KEY",
|
|
424
|
+
"exa": "EXA_API_KEY",
|
|
311
425
|
}
|
|
312
426
|
|
|
313
427
|
env_key = key_map.get(provider.lower())
|
|
314
|
-
if env_key:
|
|
315
|
-
|
|
428
|
+
if not env_key:
|
|
429
|
+
return False
|
|
430
|
+
|
|
431
|
+
config[env_key] = key
|
|
316
432
|
|
|
317
433
|
# Write back
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
f.write(f"{
|
|
434
|
+
try:
|
|
435
|
+
with open(CONFIG_FILE, "w") as f:
|
|
436
|
+
f.write("# Sigma Configuration\n")
|
|
437
|
+
f.write(f"# Updated: {__import__('datetime').datetime.now().isoformat()}\n\n")
|
|
438
|
+
|
|
439
|
+
# Group by type for readability
|
|
440
|
+
llm_keys = ["GOOGLE_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GROQ_API_KEY", "XAI_API_KEY"]
|
|
441
|
+
data_keys = ["POLYGON_API_KEY", "ALPHA_VANTAGE_API_KEY", "EXA_API_KEY"]
|
|
442
|
+
|
|
443
|
+
f.write("# LLM Provider Keys\n")
|
|
444
|
+
for k in llm_keys:
|
|
445
|
+
if k in config:
|
|
446
|
+
f.write(f"{k}={config[k]}\n")
|
|
447
|
+
|
|
448
|
+
f.write("\n# Data Provider Keys\n")
|
|
449
|
+
for k in data_keys:
|
|
450
|
+
if k in config:
|
|
451
|
+
f.write(f"{k}={config[k]}\n")
|
|
452
|
+
|
|
453
|
+
f.write("\n# Other Settings\n")
|
|
454
|
+
for k, v in sorted(config.items()):
|
|
455
|
+
if k not in llm_keys and k not in data_keys:
|
|
456
|
+
f.write(f"{k}={v}\n")
|
|
457
|
+
return True
|
|
458
|
+
except IOError:
|
|
459
|
+
return False
|
|
322
460
|
|
|
323
461
|
|
|
324
462
|
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
|
|
@@ -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
|
+
)
|