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.
@@ -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