dbagent-cli 0.1.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.
- dbagent/__init__.py +5 -0
- dbagent/agent/generator.py +264 -0
- dbagent/agent/pipeline.py +259 -0
- dbagent/agent/validator.py +116 -0
- dbagent/cli.py +717 -0
- dbagent/config.py +89 -0
- dbagent/connectors/base.py +75 -0
- dbagent/connectors/factory.py +25 -0
- dbagent/connectors/mongo.py +237 -0
- dbagent/connectors/relational.py +472 -0
- dbagent/llm/auto_setup.py +269 -0
- dbagent/llm/base.py +37 -0
- dbagent/llm/factory.py +73 -0
- dbagent/llm/gemini_provider.py +110 -0
- dbagent/llm/groq_provider.py +99 -0
- dbagent/llm/mock_provider.py +42 -0
- dbagent/llm/ollama_provider.py +145 -0
- dbagent/llm/openrouter_provider.py +100 -0
- dbagent/schema/formatter.py +123 -0
- dbagent/schema/models.py +72 -0
- dbagent/schema/selector.py +70 -0
- dbagent/ui/console.py +77 -0
- dbagent/ui/viewer.py +93 -0
- dbagent_cli-0.1.0.dist-info/METADATA +242 -0
- dbagent_cli-0.1.0.dist-info/RECORD +28 -0
- dbagent_cli-0.1.0.dist-info/WHEEL +5 -0
- dbagent_cli-0.1.0.dist-info/entry_points.txt +3 -0
- dbagent_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ollama Auto-Setup: Automatically install, start, and configure Ollama
|
|
3
|
+
so DB-Agent works 100% independently with zero manual setup.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import subprocess
|
|
10
|
+
import shutil
|
|
11
|
+
import requests
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, Tuple
|
|
14
|
+
|
|
15
|
+
# Small, fast model that's good at SQL generation (~1.6 GB)
|
|
16
|
+
DEFAULT_MODEL = "qwen2.5-coder:1.5b"
|
|
17
|
+
OLLAMA_BASE_URL = "http://localhost:11434"
|
|
18
|
+
OLLAMA_WINDOWS_INSTALLER_URL = "https://ollama.com/download/OllamaSetup.exe"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def find_ollama_binary() -> Optional[str]:
|
|
22
|
+
"""Find ollama executable on PATH or in standard Windows installation directories."""
|
|
23
|
+
which_path = shutil.which("ollama")
|
|
24
|
+
if which_path:
|
|
25
|
+
return which_path
|
|
26
|
+
|
|
27
|
+
# Common Windows install locations
|
|
28
|
+
local_app_data = os.environ.get("LOCALAPPDATA", "")
|
|
29
|
+
user_profile = os.environ.get("USERPROFILE", "")
|
|
30
|
+
candidates = [
|
|
31
|
+
Path(local_app_data) / "Programs" / "Ollama" / "ollama.exe" if local_app_data else None,
|
|
32
|
+
Path(user_profile) / "AppData" / "Local" / "Programs" / "Ollama" / "ollama.exe" if user_profile else None,
|
|
33
|
+
Path("C:/Program Files/Ollama/ollama.exe"),
|
|
34
|
+
Path("C:/Program Files (x86)/Ollama/ollama.exe"),
|
|
35
|
+
]
|
|
36
|
+
for p in candidates:
|
|
37
|
+
if p and p.exists():
|
|
38
|
+
# Add to PATH for current session
|
|
39
|
+
os.environ["PATH"] = str(p.parent) + os.pathsep + os.environ.get("PATH", "")
|
|
40
|
+
return str(p)
|
|
41
|
+
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_ollama_installed() -> bool:
|
|
46
|
+
"""Check if ollama binary is available."""
|
|
47
|
+
return find_ollama_binary() is not None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_ollama_running() -> bool:
|
|
51
|
+
"""Check if Ollama server is responding."""
|
|
52
|
+
try:
|
|
53
|
+
r = requests.get(f"{OLLAMA_BASE_URL}/api/tags", timeout=2)
|
|
54
|
+
return r.status_code == 200
|
|
55
|
+
except Exception:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_installed_models() -> list:
|
|
60
|
+
"""Get list of locally installed Ollama models."""
|
|
61
|
+
try:
|
|
62
|
+
r = requests.get(f"{OLLAMA_BASE_URL}/api/tags", timeout=4)
|
|
63
|
+
if r.status_code == 200:
|
|
64
|
+
data = r.json()
|
|
65
|
+
return [m["name"] for m in data.get("models", [])]
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def start_ollama_server() -> bool:
|
|
72
|
+
"""Start ollama serve in the background."""
|
|
73
|
+
if is_ollama_running():
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
exe = find_ollama_binary() or "ollama"
|
|
77
|
+
try:
|
|
78
|
+
if sys.platform == "win32":
|
|
79
|
+
subprocess.Popen(
|
|
80
|
+
[exe, "serve"],
|
|
81
|
+
stdout=subprocess.DEVNULL,
|
|
82
|
+
stderr=subprocess.DEVNULL,
|
|
83
|
+
creationflags=subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS,
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
subprocess.Popen(
|
|
87
|
+
[exe, "serve"],
|
|
88
|
+
stdout=subprocess.DEVNULL,
|
|
89
|
+
stderr=subprocess.DEVNULL,
|
|
90
|
+
start_new_session=True,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Wait for server to become ready
|
|
94
|
+
for _ in range(15):
|
|
95
|
+
time.sleep(1)
|
|
96
|
+
if is_ollama_running():
|
|
97
|
+
return True
|
|
98
|
+
return False
|
|
99
|
+
except Exception:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def pull_model(model_name: str, progress_callback=None) -> bool:
|
|
104
|
+
"""Pull (download) an Ollama model with progress reporting."""
|
|
105
|
+
try:
|
|
106
|
+
resp = requests.post(
|
|
107
|
+
f"{OLLAMA_BASE_URL}/api/pull",
|
|
108
|
+
json={"name": model_name},
|
|
109
|
+
stream=True,
|
|
110
|
+
timeout=900,
|
|
111
|
+
)
|
|
112
|
+
if resp.status_code != 200:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
for line in resp.iter_lines():
|
|
116
|
+
if line and progress_callback:
|
|
117
|
+
import json
|
|
118
|
+
try:
|
|
119
|
+
data = json.loads(line)
|
|
120
|
+
status = data.get("status", "")
|
|
121
|
+
total = data.get("total", 0)
|
|
122
|
+
completed = data.get("completed", 0)
|
|
123
|
+
progress_callback(status, completed, total)
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
return True
|
|
127
|
+
except Exception:
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def download_ollama_installer(dest_dir: Optional[Path] = None, progress_callback=None) -> Optional[Path]:
|
|
132
|
+
"""Download the Ollama Windows installer."""
|
|
133
|
+
if dest_dir is None:
|
|
134
|
+
dest_dir = Path.home() / ".dbagent" / "installers"
|
|
135
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
installer_path = dest_dir / "OllamaSetup.exe"
|
|
137
|
+
|
|
138
|
+
if installer_path.exists() and installer_path.stat().st_size > 10_000_000:
|
|
139
|
+
return installer_path
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
resp = requests.get(OLLAMA_WINDOWS_INSTALLER_URL, stream=True, timeout=180)
|
|
143
|
+
if resp.status_code != 200:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
total = int(resp.headers.get("content-length", 0))
|
|
147
|
+
downloaded = 0
|
|
148
|
+
|
|
149
|
+
with open(installer_path, "wb") as f:
|
|
150
|
+
for chunk in resp.iter_content(chunk_size=1024 * 128):
|
|
151
|
+
f.write(chunk)
|
|
152
|
+
downloaded += len(chunk)
|
|
153
|
+
if progress_callback and total > 0:
|
|
154
|
+
pct = int(downloaded / total * 100)
|
|
155
|
+
progress_callback(f"Downloading installer: {pct}%", downloaded, total)
|
|
156
|
+
|
|
157
|
+
return installer_path
|
|
158
|
+
except Exception:
|
|
159
|
+
if installer_path.exists():
|
|
160
|
+
try:
|
|
161
|
+
installer_path.unlink()
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def install_ollama_windows(installer_path: Path) -> bool:
|
|
168
|
+
"""Run the Ollama installer on Windows."""
|
|
169
|
+
try:
|
|
170
|
+
# Run silent installer
|
|
171
|
+
subprocess.run(
|
|
172
|
+
[str(installer_path), "/VERYSILENT", "/NORESTART"],
|
|
173
|
+
timeout=180,
|
|
174
|
+
capture_output=True,
|
|
175
|
+
)
|
|
176
|
+
# Give Windows a moment to complete writing files
|
|
177
|
+
for _ in range(10):
|
|
178
|
+
time.sleep(2)
|
|
179
|
+
if is_ollama_installed():
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
return is_ollama_installed()
|
|
183
|
+
except Exception:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def ensure_ollama_ready(
|
|
188
|
+
model: str = DEFAULT_MODEL,
|
|
189
|
+
print_fn=None,
|
|
190
|
+
) -> Tuple[bool, str]:
|
|
191
|
+
"""
|
|
192
|
+
Full auto-setup pipeline: install -> start -> pull model.
|
|
193
|
+
Returns (success, status_message).
|
|
194
|
+
"""
|
|
195
|
+
def log(msg: str, level: str = "info"):
|
|
196
|
+
if print_fn:
|
|
197
|
+
print_fn(msg, level)
|
|
198
|
+
|
|
199
|
+
# --- Step 1: Check if Ollama is installed ---
|
|
200
|
+
if not is_ollama_installed():
|
|
201
|
+
if sys.platform != "win32":
|
|
202
|
+
return False, (
|
|
203
|
+
"Ollama is not installed. Install it from https://ollama.com/download "
|
|
204
|
+
"or run: curl -fsSL https://ollama.com/install.sh | sh"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
log("[*] Ollama not found on system. Auto-downloading standalone setup...", "info")
|
|
208
|
+
|
|
209
|
+
installer = download_ollama_installer(
|
|
210
|
+
progress_callback=lambda status, c, t: log(f" {status}", "progress")
|
|
211
|
+
)
|
|
212
|
+
if not installer:
|
|
213
|
+
return False, "Failed to download Ollama installer. Please install manually from https://ollama.com/download"
|
|
214
|
+
|
|
215
|
+
log("[*] Installing Ollama standalone AI engine (one-time setup)...", "info")
|
|
216
|
+
if not install_ollama_windows(installer):
|
|
217
|
+
return False, "Ollama installation failed. Please install from https://ollama.com/download"
|
|
218
|
+
|
|
219
|
+
log("[OK] Ollama installed successfully!", "success")
|
|
220
|
+
|
|
221
|
+
# --- Step 2: Start Ollama server if not running ---
|
|
222
|
+
if not is_ollama_running():
|
|
223
|
+
log("[*] Starting local Ollama AI server...", "info")
|
|
224
|
+
if not start_ollama_server():
|
|
225
|
+
return False, "Failed to start Ollama server. Try running `ollama serve` manually."
|
|
226
|
+
log("[OK] Local Ollama server is running and ready!", "success")
|
|
227
|
+
|
|
228
|
+
# --- Step 3: Check if model is available, pull if not ---
|
|
229
|
+
installed_models = get_installed_models()
|
|
230
|
+
|
|
231
|
+
if installed_models:
|
|
232
|
+
for m in installed_models:
|
|
233
|
+
if model.split(":")[0] in m:
|
|
234
|
+
return True, f"Ollama ready with local model '{m}'."
|
|
235
|
+
return True, f"Ollama ready with local model '{installed_models[0]}'."
|
|
236
|
+
|
|
237
|
+
# No models installed — pull one
|
|
238
|
+
log(f"[*] Downloading standalone coding model '{model}' (~1.2GB, one-time)...", "info")
|
|
239
|
+
|
|
240
|
+
last_pct = [-1]
|
|
241
|
+
def progress_cb(status, completed, total):
|
|
242
|
+
if total > 0:
|
|
243
|
+
pct = int(completed / total * 100)
|
|
244
|
+
if pct != last_pct[0] and pct % 10 == 0:
|
|
245
|
+
last_pct[0] = pct
|
|
246
|
+
log(f" Downloading model: {pct}%", "progress")
|
|
247
|
+
elif status:
|
|
248
|
+
log(f" {status}", "progress")
|
|
249
|
+
|
|
250
|
+
if not pull_model(model, progress_callback=progress_cb):
|
|
251
|
+
return False, f"Failed to download model '{model}'. Check your internet connection."
|
|
252
|
+
|
|
253
|
+
log(f"[OK] Model '{model}' downloaded and ready!", "success")
|
|
254
|
+
return True, f"Ollama ready with model '{model}'."
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def get_best_available_model() -> Optional[str]:
|
|
258
|
+
"""Get the best available model that's already pulled."""
|
|
259
|
+
models = get_installed_models()
|
|
260
|
+
if not models:
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
preferred_order = ["qwen2.5-coder", "codellama", "deepseek-coder", "qwen", "llama", "mistral"]
|
|
264
|
+
for pref in preferred_order:
|
|
265
|
+
for m in models:
|
|
266
|
+
if pref in m:
|
|
267
|
+
return m
|
|
268
|
+
|
|
269
|
+
return models[0]
|
dbagent/llm/base.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base interface for LLM providers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Optional, Callable, List, Dict, Any, Generator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseLLMProvider(ABC):
|
|
10
|
+
"""Abstract base class for all AI/LLM providers."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def name(self) -> str:
|
|
15
|
+
"""Name of the provider (e.g. 'ollama', 'gemini', 'groq')."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def is_available(self) -> bool:
|
|
20
|
+
"""Check if the provider is currently reachable and configured."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def list_models(self) -> List[str]:
|
|
25
|
+
"""List available models for this provider."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def generate(
|
|
30
|
+
self,
|
|
31
|
+
prompt: str,
|
|
32
|
+
system_prompt: Optional[str] = None,
|
|
33
|
+
model: Optional[str] = None,
|
|
34
|
+
stream_callback: Optional[Callable[[str], None]] = None,
|
|
35
|
+
) -> str:
|
|
36
|
+
"""Generate response from LLM."""
|
|
37
|
+
pass
|
dbagent/llm/factory.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM Provider Factory.
|
|
3
|
+
Auto-detects available providers: Ollama (local) -> Gemini Free -> Groq Free -> OpenRouter Free -> Offline Fallback.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from dbagent.config import ConfigManager
|
|
8
|
+
from dbagent.llm.base import BaseLLMProvider
|
|
9
|
+
from dbagent.llm.ollama_provider import OllamaProvider
|
|
10
|
+
from dbagent.llm.gemini_provider import GeminiProvider
|
|
11
|
+
from dbagent.llm.groq_provider import GroqProvider
|
|
12
|
+
from dbagent.llm.openrouter_provider import OpenRouterProvider
|
|
13
|
+
from dbagent.llm.mock_provider import MockProvider
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_llm_provider(
|
|
17
|
+
provider_name: Optional[str] = None,
|
|
18
|
+
model: Optional[str] = None,
|
|
19
|
+
config_mgr: Optional[ConfigManager] = None,
|
|
20
|
+
) -> BaseLLMProvider:
|
|
21
|
+
"""Instantiate and return the requested or best available LLM provider."""
|
|
22
|
+
cfg = config_mgr or ConfigManager()
|
|
23
|
+
|
|
24
|
+
# 1. Explicit provider requested
|
|
25
|
+
if provider_name:
|
|
26
|
+
p = provider_name.lower()
|
|
27
|
+
if p == "ollama":
|
|
28
|
+
base_url = cfg.get_setting("ollama_base_url", "http://localhost:11434")
|
|
29
|
+
return OllamaProvider(base_url=base_url, default_model=model or "qwen2.5-coder")
|
|
30
|
+
elif p == "gemini":
|
|
31
|
+
key = cfg.get_setting("gemini_api_key")
|
|
32
|
+
return GeminiProvider(api_key=key, default_model=model or "gemini-3.6-flash")
|
|
33
|
+
elif p == "groq":
|
|
34
|
+
key = cfg.get_setting("groq_api_key")
|
|
35
|
+
return GroqProvider(api_key=key, default_model=model or "llama-3.3-70b-versatile")
|
|
36
|
+
elif p == "openrouter":
|
|
37
|
+
key = cfg.get_setting("openrouter_api_key")
|
|
38
|
+
return OpenRouterProvider(api_key=key, default_model=model or "meta-llama/llama-3.3-70b-instruct:free")
|
|
39
|
+
elif p in ["mock", "offline"]:
|
|
40
|
+
return MockProvider()
|
|
41
|
+
|
|
42
|
+
# 2. Check saved default provider
|
|
43
|
+
saved_default = cfg.get_setting("default_provider")
|
|
44
|
+
if saved_default and saved_default != "auto":
|
|
45
|
+
try:
|
|
46
|
+
return get_llm_provider(provider_name=saved_default, model=model, config_mgr=cfg)
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
# 3. Auto-detection priority:
|
|
51
|
+
# A. Local Ollama if running
|
|
52
|
+
ollama_url = cfg.get_setting("ollama_base_url", "http://localhost:11434")
|
|
53
|
+
ollama = OllamaProvider(base_url=ollama_url)
|
|
54
|
+
if ollama.is_available():
|
|
55
|
+
return ollama
|
|
56
|
+
|
|
57
|
+
# B. Gemini if API key is present
|
|
58
|
+
gemini_key = cfg.get_setting("gemini_api_key")
|
|
59
|
+
if gemini_key:
|
|
60
|
+
return GeminiProvider(api_key=gemini_key)
|
|
61
|
+
|
|
62
|
+
# C. Groq if API key is present
|
|
63
|
+
groq_key = cfg.get_setting("groq_api_key")
|
|
64
|
+
if groq_key:
|
|
65
|
+
return GroqProvider(api_key=groq_key)
|
|
66
|
+
|
|
67
|
+
# D. OpenRouter if API key is present
|
|
68
|
+
openrouter_key = cfg.get_setting("openrouter_api_key")
|
|
69
|
+
if openrouter_key:
|
|
70
|
+
return OpenRouterProvider(api_key=openrouter_key)
|
|
71
|
+
|
|
72
|
+
# E. Fallback to Ollama or Mock
|
|
73
|
+
return ollama
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Gemini Free Tier LLM Provider.
|
|
3
|
+
Uses Gemini 2.0 Flash / Gemini 1.5 Flash via direct HTTP API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import requests
|
|
9
|
+
from typing import Optional, Callable, List
|
|
10
|
+
from dbagent.llm.base import BaseLLMProvider
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GeminiProvider(BaseLLMProvider):
|
|
14
|
+
"""Google Gemini LLM provider (Free Tier available)."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, api_key: Optional[str] = None, default_model: str = "gemini-3.6-flash"):
|
|
17
|
+
self.api_key = api_key or os.getenv("GEMINI_API_KEY")
|
|
18
|
+
self.default_model = default_model
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def name(self) -> str:
|
|
22
|
+
return "gemini"
|
|
23
|
+
|
|
24
|
+
def is_available(self) -> bool:
|
|
25
|
+
return bool(self.api_key)
|
|
26
|
+
|
|
27
|
+
def list_models(self) -> List[str]:
|
|
28
|
+
return [
|
|
29
|
+
"gemini-3.6-flash",
|
|
30
|
+
"gemini-2.5-flash",
|
|
31
|
+
"gemini-2.5-pro",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
def generate(
|
|
35
|
+
self,
|
|
36
|
+
prompt: str,
|
|
37
|
+
system_prompt: Optional[str] = None,
|
|
38
|
+
model: Optional[str] = None,
|
|
39
|
+
stream_callback: Optional[Callable[[str], None]] = None,
|
|
40
|
+
) -> str:
|
|
41
|
+
if not self.api_key:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"Gemini API key is not configured. Set GEMINI_API_KEY environment variable or run `db-agent config`."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
target_model = model or self.default_model
|
|
47
|
+
# Strip model prefixes if provided
|
|
48
|
+
if target_model.startswith("models/"):
|
|
49
|
+
target_model = target_model[7:]
|
|
50
|
+
|
|
51
|
+
endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{target_model}:generateContent?key={self.api_key}"
|
|
52
|
+
if stream_callback:
|
|
53
|
+
endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{target_model}:streamGenerateContent?alt=sse&key={self.api_key}"
|
|
54
|
+
|
|
55
|
+
contents = []
|
|
56
|
+
if system_prompt:
|
|
57
|
+
contents.append({
|
|
58
|
+
"role": "user",
|
|
59
|
+
"parts": [{"text": f"System Instructions: {system_prompt}\n\nTask: {prompt}"}],
|
|
60
|
+
})
|
|
61
|
+
else:
|
|
62
|
+
contents.append({
|
|
63
|
+
"role": "user",
|
|
64
|
+
"parts": [{"text": prompt}],
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
payload = {
|
|
68
|
+
"contents": contents,
|
|
69
|
+
"generationConfig": {
|
|
70
|
+
"temperature": 0.1,
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
if stream_callback:
|
|
76
|
+
response = requests.post(endpoint, json=payload, stream=True, timeout=90)
|
|
77
|
+
if response.status_code != 200:
|
|
78
|
+
raise RuntimeError(f"Gemini API error ({response.status_code}): {response.text}")
|
|
79
|
+
|
|
80
|
+
full_text = []
|
|
81
|
+
for line in response.iter_lines():
|
|
82
|
+
if line:
|
|
83
|
+
decoded = line.decode("utf-8")
|
|
84
|
+
if decoded.startswith("data: "):
|
|
85
|
+
raw_json = decoded[6:]
|
|
86
|
+
try:
|
|
87
|
+
chunk = json.loads(raw_json)
|
|
88
|
+
candidates = chunk.get("candidates", [])
|
|
89
|
+
if candidates:
|
|
90
|
+
parts = candidates[0].get("content", {}).get("parts", [])
|
|
91
|
+
for p in parts:
|
|
92
|
+
t = p.get("text", "")
|
|
93
|
+
full_text.append(t)
|
|
94
|
+
stream_callback(t)
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
return "".join(full_text)
|
|
98
|
+
else:
|
|
99
|
+
response = requests.post(endpoint, json=payload, timeout=90)
|
|
100
|
+
if response.status_code == 200:
|
|
101
|
+
data = response.json()
|
|
102
|
+
candidates = data.get("candidates", [])
|
|
103
|
+
if candidates:
|
|
104
|
+
parts = candidates[0].get("content", {}).get("parts", [])
|
|
105
|
+
return "".join(p.get("text", "") for p in parts)
|
|
106
|
+
return ""
|
|
107
|
+
else:
|
|
108
|
+
raise RuntimeError(f"Gemini API error ({response.status_code}): {response.text}")
|
|
109
|
+
except Exception as e:
|
|
110
|
+
raise RuntimeError(f"Gemini generation error: {str(e)}")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Groq Free Tier LLM Provider.
|
|
3
|
+
Provides ultra-fast inference with Llama 3.3, Qwen 2.5, DeepSeek via OpenAI-compatible API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import requests
|
|
9
|
+
from typing import Optional, Callable, List
|
|
10
|
+
from dbagent.llm.base import BaseLLMProvider
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GroqProvider(BaseLLMProvider):
|
|
14
|
+
"""Groq Cloud Provider (Free Rate-Limited Tier)."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, api_key: Optional[str] = None, default_model: str = "llama-3.3-70b-versatile"):
|
|
17
|
+
self.api_key = api_key or os.getenv("GROQ_API_KEY")
|
|
18
|
+
self.default_model = default_model
|
|
19
|
+
self.endpoint = "https://api.groq.com/openai/v1/chat/completions"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> str:
|
|
23
|
+
return "groq"
|
|
24
|
+
|
|
25
|
+
def is_available(self) -> bool:
|
|
26
|
+
return bool(self.api_key)
|
|
27
|
+
|
|
28
|
+
def list_models(self) -> List[str]:
|
|
29
|
+
return [
|
|
30
|
+
"llama-3.3-70b-versatile",
|
|
31
|
+
"llama-3.1-8b-instant",
|
|
32
|
+
"qwen-2.5-32b",
|
|
33
|
+
"deepseek-r1-distill-llama-70b",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
def generate(
|
|
37
|
+
self,
|
|
38
|
+
prompt: str,
|
|
39
|
+
system_prompt: Optional[str] = None,
|
|
40
|
+
model: Optional[str] = None,
|
|
41
|
+
stream_callback: Optional[Callable[[str], None]] = None,
|
|
42
|
+
) -> str:
|
|
43
|
+
if not self.api_key:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
"Groq API key is not configured. Set GROQ_API_KEY environment variable or run `db-agent config`."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
target_model = model or self.default_model
|
|
49
|
+
|
|
50
|
+
headers = {
|
|
51
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
messages = []
|
|
56
|
+
if system_prompt:
|
|
57
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
58
|
+
messages.append({"role": "user", "content": prompt})
|
|
59
|
+
|
|
60
|
+
payload = {
|
|
61
|
+
"model": target_model,
|
|
62
|
+
"messages": messages,
|
|
63
|
+
"temperature": 0.1,
|
|
64
|
+
"stream": stream_callback is not None,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
if stream_callback:
|
|
69
|
+
response = requests.post(self.endpoint, headers=headers, json=payload, stream=True, timeout=60)
|
|
70
|
+
if response.status_code != 200:
|
|
71
|
+
raise RuntimeError(f"Groq API error ({response.status_code}): {response.text}")
|
|
72
|
+
|
|
73
|
+
full_text = []
|
|
74
|
+
for line in response.iter_lines():
|
|
75
|
+
if line:
|
|
76
|
+
decoded = line.decode("utf-8")
|
|
77
|
+
if decoded.startswith("data: "):
|
|
78
|
+
raw_json = decoded[6:].strip()
|
|
79
|
+
if raw_json == "[DONE]":
|
|
80
|
+
break
|
|
81
|
+
try:
|
|
82
|
+
chunk = json.loads(raw_json)
|
|
83
|
+
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
|
84
|
+
text_part = delta.get("content", "")
|
|
85
|
+
if text_part:
|
|
86
|
+
full_text.append(text_part)
|
|
87
|
+
stream_callback(text_part)
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
return "".join(full_text)
|
|
91
|
+
else:
|
|
92
|
+
response = requests.post(self.endpoint, headers=headers, json=payload, timeout=60)
|
|
93
|
+
if response.status_code == 200:
|
|
94
|
+
data = response.json()
|
|
95
|
+
return data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
96
|
+
else:
|
|
97
|
+
raise RuntimeError(f"Groq API error ({response.status_code}): {response.text}")
|
|
98
|
+
except Exception as e:
|
|
99
|
+
raise RuntimeError(f"Groq generation error: {str(e)}")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Offline Rule-Based Mock Provider for zero-dependency testing and fallback.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Callable, List
|
|
6
|
+
from dbagent.llm.base import BaseLLMProvider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MockProvider(BaseLLMProvider):
|
|
10
|
+
"""Fallback provider when no AI keys or local Ollama are available."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def name(self) -> str:
|
|
14
|
+
return "offline-mock"
|
|
15
|
+
|
|
16
|
+
def is_available(self) -> bool:
|
|
17
|
+
return True
|
|
18
|
+
|
|
19
|
+
def list_models(self) -> List[str]:
|
|
20
|
+
return ["offline-rule-based"]
|
|
21
|
+
|
|
22
|
+
def generate(
|
|
23
|
+
self,
|
|
24
|
+
prompt: str,
|
|
25
|
+
system_prompt: Optional[str] = None,
|
|
26
|
+
model: Optional[str] = None,
|
|
27
|
+
stream_callback: Optional[Callable[[str], None]] = None,
|
|
28
|
+
) -> str:
|
|
29
|
+
response = (
|
|
30
|
+
"```sql\n"
|
|
31
|
+
"-- [Offline Rule-Based Generator: Connect Ollama or configure Gemini/Groq for advanced AI]\n"
|
|
32
|
+
"SELECT *\n"
|
|
33
|
+
"FROM (\n"
|
|
34
|
+
" -- Query generated based on schema introspection\n"
|
|
35
|
+
" SELECT 1 AS sample_result\n"
|
|
36
|
+
");\n"
|
|
37
|
+
"```\n\n"
|
|
38
|
+
"💡 *Tip: Run `db-agent config` to configure Ollama (local offline) or free Gemini/Groq API keys for full AI capability.*"
|
|
39
|
+
)
|
|
40
|
+
if stream_callback:
|
|
41
|
+
stream_callback(response)
|
|
42
|
+
return response
|