opspilot-ai 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.
- opspilot/__init__.py +0 -0
- opspilot/agents/fixer.py +46 -0
- opspilot/agents/planner.py +74 -0
- opspilot/agents/remediation.py +200 -0
- opspilot/agents/verifier.py +67 -0
- opspilot/cli.py +360 -0
- opspilot/config.py +22 -0
- opspilot/context/__init__.py +26 -0
- opspilot/context/deployment_history.py +347 -0
- opspilot/context/deps.py +14 -0
- opspilot/context/docker.py +17 -0
- opspilot/context/env.py +17 -0
- opspilot/context/logs.py +16 -0
- opspilot/context/production_logs.py +262 -0
- opspilot/context/project.py +19 -0
- opspilot/diffs/redis.py +23 -0
- opspilot/graph/engine.py +33 -0
- opspilot/graph/nodes.py +41 -0
- opspilot/memory.py +24 -0
- opspilot/memory_redis.py +322 -0
- opspilot/state.py +18 -0
- opspilot/tools/__init__.py +52 -0
- opspilot/tools/dep_tools.py +5 -0
- opspilot/tools/env_tools.py +5 -0
- opspilot/tools/log_tools.py +11 -0
- opspilot/tools/pattern_analysis.py +194 -0
- opspilot/utils/__init__.py +1 -0
- opspilot/utils/llm.py +23 -0
- opspilot/utils/llm_providers.py +499 -0
- opspilot_ai-0.1.0.dist-info/METADATA +408 -0
- opspilot_ai-0.1.0.dist-info/RECORD +35 -0
- opspilot_ai-0.1.0.dist-info/WHEEL +5 -0
- opspilot_ai-0.1.0.dist-info/entry_points.txt +2 -0
- opspilot_ai-0.1.0.dist-info/licenses/LICENSE +21 -0
- opspilot_ai-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""Multi-provider LLM system with automatic fallback.
|
|
2
|
+
|
|
3
|
+
Supports:
|
|
4
|
+
- Ollama (local, free)
|
|
5
|
+
- OpenAI (cloud, paid)
|
|
6
|
+
- Anthropic Claude (cloud, paid)
|
|
7
|
+
- Google Gemini (cloud, free tier)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import shutil
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, Optional, List
|
|
16
|
+
import requests
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LLMProvider:
|
|
20
|
+
"""Base class for LLM providers."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, timeout: int = 60):
|
|
23
|
+
self.timeout = timeout
|
|
24
|
+
|
|
25
|
+
def is_available(self) -> bool:
|
|
26
|
+
"""Check if provider is available."""
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
|
|
29
|
+
def call(self, prompt: str) -> str:
|
|
30
|
+
"""Call the LLM with a prompt."""
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
def parse_json(self, raw: str) -> Dict:
|
|
34
|
+
"""Parse JSON from LLM output."""
|
|
35
|
+
# Try parsing as-is first
|
|
36
|
+
try:
|
|
37
|
+
return json.loads(raw)
|
|
38
|
+
except json.JSONDecodeError:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
# Try extracting JSON from text
|
|
42
|
+
start_idx = raw.find('{')
|
|
43
|
+
end_idx = raw.rfind('}')
|
|
44
|
+
|
|
45
|
+
if start_idx != -1 and end_idx != -1 and start_idx < end_idx:
|
|
46
|
+
json_str = raw[start_idx:end_idx + 1]
|
|
47
|
+
try:
|
|
48
|
+
return json.loads(json_str)
|
|
49
|
+
except json.JSONDecodeError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
raise json.JSONDecodeError(
|
|
53
|
+
f"Could not parse LLM output as JSON",
|
|
54
|
+
raw[:100],
|
|
55
|
+
0
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class OllamaProvider(LLMProvider):
|
|
60
|
+
"""Ollama local LLM provider."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, model: str = "llama3", timeout: int = 60):
|
|
63
|
+
super().__init__(timeout)
|
|
64
|
+
self.model = model
|
|
65
|
+
|
|
66
|
+
def _resolve_ollama_path(self) -> Optional[str]:
|
|
67
|
+
"""Resolve Ollama binary path."""
|
|
68
|
+
ollama_path = shutil.which("ollama")
|
|
69
|
+
if ollama_path:
|
|
70
|
+
return ollama_path
|
|
71
|
+
|
|
72
|
+
# Windows fallback
|
|
73
|
+
fallback = Path.home() / "AppData/Local/Programs/Ollama/ollama.exe"
|
|
74
|
+
if fallback.exists():
|
|
75
|
+
return str(fallback)
|
|
76
|
+
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
def is_available(self) -> bool:
|
|
80
|
+
"""Check if Ollama is available."""
|
|
81
|
+
try:
|
|
82
|
+
ollama_path = self._resolve_ollama_path()
|
|
83
|
+
if not ollama_path:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
result = subprocess.run(
|
|
87
|
+
[ollama_path, "list"],
|
|
88
|
+
capture_output=True,
|
|
89
|
+
timeout=5,
|
|
90
|
+
check=True
|
|
91
|
+
)
|
|
92
|
+
return result.returncode == 0
|
|
93
|
+
except Exception:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
def call(self, prompt: str) -> str:
|
|
97
|
+
"""Call Ollama LLM."""
|
|
98
|
+
ollama_path = self._resolve_ollama_path()
|
|
99
|
+
if not ollama_path:
|
|
100
|
+
raise RuntimeError("Ollama not found")
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
process = subprocess.run(
|
|
104
|
+
[ollama_path, "run", self.model],
|
|
105
|
+
input=prompt,
|
|
106
|
+
capture_output=True,
|
|
107
|
+
text=True,
|
|
108
|
+
encoding="utf-8",
|
|
109
|
+
timeout=self.timeout
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if process.returncode != 0:
|
|
113
|
+
raise RuntimeError(f"Ollama failed: {process.stderr.strip()}")
|
|
114
|
+
|
|
115
|
+
return process.stdout.strip()
|
|
116
|
+
|
|
117
|
+
except subprocess.TimeoutExpired:
|
|
118
|
+
raise RuntimeError(f"Ollama inference timed out after {self.timeout}s")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class OpenRouterProvider(LLMProvider):
|
|
122
|
+
"""OpenRouter provider - access to many free open-source models."""
|
|
123
|
+
|
|
124
|
+
def __init__(self, model: str = "google/gemini-2.0-flash-exp:free", timeout: int = 60):
|
|
125
|
+
super().__init__(timeout)
|
|
126
|
+
self.model = model
|
|
127
|
+
self.api_key = os.getenv("OPENROUTER_API_KEY") # Free tier available
|
|
128
|
+
self.base_url = "https://openrouter.ai/api/v1/chat/completions"
|
|
129
|
+
|
|
130
|
+
def is_available(self) -> bool:
|
|
131
|
+
"""Check if OpenRouter API key is configured."""
|
|
132
|
+
return self.api_key is not None
|
|
133
|
+
|
|
134
|
+
def call(self, prompt: str) -> str:
|
|
135
|
+
"""Call OpenRouter API with free models."""
|
|
136
|
+
if not self.api_key:
|
|
137
|
+
raise RuntimeError("OPENROUTER_API_KEY environment variable not set")
|
|
138
|
+
|
|
139
|
+
headers = {
|
|
140
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
"HTTP-Referer": "https://github.com/opspilot", # Required by OpenRouter
|
|
143
|
+
"X-Title": "OpsPilot"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
payload = {
|
|
147
|
+
"model": self.model,
|
|
148
|
+
"messages": [
|
|
149
|
+
{"role": "user", "content": prompt}
|
|
150
|
+
],
|
|
151
|
+
"temperature": 0.3,
|
|
152
|
+
"max_tokens": 2000
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
response = requests.post(
|
|
157
|
+
self.base_url,
|
|
158
|
+
headers=headers,
|
|
159
|
+
json=payload,
|
|
160
|
+
timeout=self.timeout
|
|
161
|
+
)
|
|
162
|
+
response.raise_for_status()
|
|
163
|
+
|
|
164
|
+
data = response.json()
|
|
165
|
+
return data["choices"][0]["message"]["content"]
|
|
166
|
+
|
|
167
|
+
except requests.RequestException as e:
|
|
168
|
+
raise RuntimeError(f"OpenRouter API call failed: {e}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class HuggingFaceProvider(LLMProvider):
|
|
172
|
+
"""HuggingFace Inference API provider - free tier for open models."""
|
|
173
|
+
|
|
174
|
+
def __init__(self, model: str = "mistralai/Mistral-7B-Instruct-v0.2", timeout: int = 60):
|
|
175
|
+
super().__init__(timeout)
|
|
176
|
+
self.model = model
|
|
177
|
+
self.api_key = os.getenv("HUGGINGFACE_API_KEY") # Free tier available
|
|
178
|
+
self.base_url = f"https://api-inference.huggingface.co/models/{self.model}"
|
|
179
|
+
|
|
180
|
+
def is_available(self) -> bool:
|
|
181
|
+
"""Check if HuggingFace API key is configured."""
|
|
182
|
+
return self.api_key is not None
|
|
183
|
+
|
|
184
|
+
def call(self, prompt: str) -> str:
|
|
185
|
+
"""Call HuggingFace Inference API."""
|
|
186
|
+
if not self.api_key:
|
|
187
|
+
raise RuntimeError("HUGGINGFACE_API_KEY environment variable not set")
|
|
188
|
+
|
|
189
|
+
headers = {
|
|
190
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
191
|
+
"Content-Type": "application/json"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
payload = {
|
|
195
|
+
"inputs": prompt,
|
|
196
|
+
"parameters": {
|
|
197
|
+
"temperature": 0.3,
|
|
198
|
+
"max_new_tokens": 2000,
|
|
199
|
+
"return_full_text": False
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
response = requests.post(
|
|
205
|
+
self.base_url,
|
|
206
|
+
headers=headers,
|
|
207
|
+
json=payload,
|
|
208
|
+
timeout=self.timeout
|
|
209
|
+
)
|
|
210
|
+
response.raise_for_status()
|
|
211
|
+
|
|
212
|
+
data = response.json()
|
|
213
|
+
# HuggingFace returns different formats
|
|
214
|
+
if isinstance(data, list):
|
|
215
|
+
return data[0]["generated_text"]
|
|
216
|
+
elif isinstance(data, dict) and "generated_text" in data:
|
|
217
|
+
return data["generated_text"]
|
|
218
|
+
else:
|
|
219
|
+
return str(data)
|
|
220
|
+
|
|
221
|
+
except requests.RequestException as e:
|
|
222
|
+
raise RuntimeError(f"HuggingFace API call failed: {e}")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class AnthropicProvider(LLMProvider):
|
|
226
|
+
"""Anthropic Claude provider."""
|
|
227
|
+
|
|
228
|
+
def __init__(self, model: str = "claude-3-5-haiku-20241022", timeout: int = 60):
|
|
229
|
+
super().__init__(timeout)
|
|
230
|
+
self.model = model
|
|
231
|
+
self.api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
232
|
+
self.base_url = "https://api.anthropic.com/v1/messages"
|
|
233
|
+
|
|
234
|
+
def is_available(self) -> bool:
|
|
235
|
+
"""Check if Anthropic API key is configured."""
|
|
236
|
+
return self.api_key is not None
|
|
237
|
+
|
|
238
|
+
def call(self, prompt: str) -> str:
|
|
239
|
+
"""Call Anthropic API."""
|
|
240
|
+
if not self.api_key:
|
|
241
|
+
raise RuntimeError("ANTHROPIC_API_KEY environment variable not set")
|
|
242
|
+
|
|
243
|
+
headers = {
|
|
244
|
+
"x-api-key": self.api_key,
|
|
245
|
+
"anthropic-version": "2023-06-01",
|
|
246
|
+
"Content-Type": "application/json"
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
payload = {
|
|
250
|
+
"model": self.model,
|
|
251
|
+
"max_tokens": 2000,
|
|
252
|
+
"messages": [
|
|
253
|
+
{"role": "user", "content": prompt}
|
|
254
|
+
]
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
response = requests.post(
|
|
259
|
+
self.base_url,
|
|
260
|
+
headers=headers,
|
|
261
|
+
json=payload,
|
|
262
|
+
timeout=self.timeout
|
|
263
|
+
)
|
|
264
|
+
response.raise_for_status()
|
|
265
|
+
|
|
266
|
+
data = response.json()
|
|
267
|
+
return data["content"][0]["text"]
|
|
268
|
+
|
|
269
|
+
except requests.RequestException as e:
|
|
270
|
+
raise RuntimeError(f"Anthropic API call failed: {e}")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class GeminiProvider(LLMProvider):
|
|
274
|
+
"""Google Gemini provider."""
|
|
275
|
+
|
|
276
|
+
def __init__(self, model: str = "gemini-2.0-flash-exp", timeout: int = 60):
|
|
277
|
+
super().__init__(timeout)
|
|
278
|
+
self.model = model
|
|
279
|
+
self.api_key = os.getenv("GOOGLE_API_KEY")
|
|
280
|
+
|
|
281
|
+
def is_available(self) -> bool:
|
|
282
|
+
"""Check if Google API key is configured."""
|
|
283
|
+
return self.api_key is not None
|
|
284
|
+
|
|
285
|
+
def call(self, prompt: str) -> str:
|
|
286
|
+
"""Call Google Gemini API."""
|
|
287
|
+
if not self.api_key:
|
|
288
|
+
raise RuntimeError("GOOGLE_API_KEY environment variable not set")
|
|
289
|
+
|
|
290
|
+
url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model}:generateContent?key={self.api_key}"
|
|
291
|
+
|
|
292
|
+
payload = {
|
|
293
|
+
"contents": [{
|
|
294
|
+
"parts": [{
|
|
295
|
+
"text": prompt
|
|
296
|
+
}]
|
|
297
|
+
}],
|
|
298
|
+
"generationConfig": {
|
|
299
|
+
"temperature": 0.3,
|
|
300
|
+
"maxOutputTokens": 2000
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
response = requests.post(
|
|
306
|
+
url,
|
|
307
|
+
json=payload,
|
|
308
|
+
timeout=self.timeout
|
|
309
|
+
)
|
|
310
|
+
response.raise_for_status()
|
|
311
|
+
|
|
312
|
+
data = response.json()
|
|
313
|
+
return data["candidates"][0]["content"]["parts"][0]["text"]
|
|
314
|
+
|
|
315
|
+
except requests.RequestException as e:
|
|
316
|
+
raise RuntimeError(f"Google Gemini API call failed: {e}")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class LLMRouter:
|
|
320
|
+
"""Smart LLM router with automatic fallback."""
|
|
321
|
+
|
|
322
|
+
def __init__(self, prefer_local: bool = True):
|
|
323
|
+
"""
|
|
324
|
+
Initialize LLM router.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
prefer_local: If True, try Ollama first (free, local)
|
|
328
|
+
"""
|
|
329
|
+
self.prefer_local = prefer_local
|
|
330
|
+
self.providers = self._initialize_providers()
|
|
331
|
+
self.last_successful_provider = None
|
|
332
|
+
|
|
333
|
+
def _initialize_providers(self) -> List[LLMProvider]:
|
|
334
|
+
"""Initialize providers in priority order (all free/open-source)."""
|
|
335
|
+
if self.prefer_local:
|
|
336
|
+
# Local first, then free cloud providers
|
|
337
|
+
return [
|
|
338
|
+
OllamaProvider(), # Free, local, private
|
|
339
|
+
GeminiProvider(), # Free tier (Google)
|
|
340
|
+
OpenRouterProvider(), # Free models via OpenRouter
|
|
341
|
+
HuggingFaceProvider() # Free tier (HuggingFace)
|
|
342
|
+
]
|
|
343
|
+
else:
|
|
344
|
+
# Cloud first (faster), then local
|
|
345
|
+
return [
|
|
346
|
+
GeminiProvider(), # Free tier, fastest
|
|
347
|
+
OpenRouterProvider(), # Free models, many options
|
|
348
|
+
HuggingFaceProvider(), # Free tier, open models
|
|
349
|
+
OllamaProvider() # Free, local fallback
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
def get_available_providers(self) -> List[str]:
|
|
353
|
+
"""Get list of available provider names."""
|
|
354
|
+
available = []
|
|
355
|
+
for provider in self.providers:
|
|
356
|
+
if provider.is_available():
|
|
357
|
+
available.append(provider.__class__.__name__)
|
|
358
|
+
return available
|
|
359
|
+
|
|
360
|
+
def call(self, prompt: str, timeout: int = 60) -> str:
|
|
361
|
+
"""
|
|
362
|
+
Call LLM with automatic fallback.
|
|
363
|
+
|
|
364
|
+
Tries providers in order until one succeeds.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
prompt: The prompt to send
|
|
368
|
+
timeout: Timeout in seconds
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
LLM response text
|
|
372
|
+
|
|
373
|
+
Raises:
|
|
374
|
+
RuntimeError: If all providers fail
|
|
375
|
+
"""
|
|
376
|
+
errors = []
|
|
377
|
+
|
|
378
|
+
# Try last successful provider first (if any)
|
|
379
|
+
if self.last_successful_provider:
|
|
380
|
+
try:
|
|
381
|
+
self.last_successful_provider.timeout = timeout
|
|
382
|
+
result = self.last_successful_provider.call(prompt)
|
|
383
|
+
return result
|
|
384
|
+
except Exception as e:
|
|
385
|
+
errors.append(f"{self.last_successful_provider.__class__.__name__}: {e}")
|
|
386
|
+
# Continue to other providers
|
|
387
|
+
|
|
388
|
+
# Try all providers
|
|
389
|
+
for provider in self.providers:
|
|
390
|
+
if not provider.is_available():
|
|
391
|
+
errors.append(f"{provider.__class__.__name__}: Not available")
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
provider.timeout = timeout
|
|
396
|
+
result = provider.call(prompt)
|
|
397
|
+
|
|
398
|
+
# Success! Remember this provider
|
|
399
|
+
self.last_successful_provider = provider
|
|
400
|
+
print(f"[LLM] Using {provider.__class__.__name__}")
|
|
401
|
+
return result
|
|
402
|
+
|
|
403
|
+
except Exception as e:
|
|
404
|
+
errors.append(f"{provider.__class__.__name__}: {e}")
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
# All providers failed
|
|
408
|
+
error_summary = "\n".join(errors)
|
|
409
|
+
raise RuntimeError(
|
|
410
|
+
f"All LLM providers failed:\n{error_summary}\n\n"
|
|
411
|
+
f"Setup instructions (all FREE/open-source):\n"
|
|
412
|
+
f"1. Ollama (local): Install from https://ollama.ai/ → ollama pull llama3\n"
|
|
413
|
+
f"2. Google Gemini (free tier): Get key at https://aistudio.google.com/ → export GOOGLE_API_KEY=...\n"
|
|
414
|
+
f"3. OpenRouter (free models): Get key at https://openrouter.ai/ → export OPENROUTER_API_KEY=...\n"
|
|
415
|
+
f"4. HuggingFace (free tier): Get key at https://huggingface.co/settings/tokens → export HUGGINGFACE_API_KEY=..."
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
def safe_json_parse(self, raw: str, retry_timeout: int = 15) -> Dict:
|
|
419
|
+
"""
|
|
420
|
+
Parse JSON from LLM output with retry.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
raw: Raw LLM output
|
|
424
|
+
retry_timeout: Timeout for retry call
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Parsed JSON dict
|
|
428
|
+
"""
|
|
429
|
+
# Try parsing with base provider logic
|
|
430
|
+
try:
|
|
431
|
+
provider = self.last_successful_provider or self.providers[0]
|
|
432
|
+
return provider.parse_json(raw)
|
|
433
|
+
except json.JSONDecodeError:
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
# Last resort: ask LLM to fix it
|
|
437
|
+
correction_prompt = f"""
|
|
438
|
+
The following output was NOT valid JSON.
|
|
439
|
+
|
|
440
|
+
Return ONLY valid JSON.
|
|
441
|
+
No explanation.
|
|
442
|
+
No markdown.
|
|
443
|
+
|
|
444
|
+
INVALID OUTPUT:
|
|
445
|
+
{raw}
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
try:
|
|
449
|
+
corrected = self.call(correction_prompt, timeout=retry_timeout)
|
|
450
|
+
provider = self.last_successful_provider or self.providers[0]
|
|
451
|
+
return provider.parse_json(corrected)
|
|
452
|
+
except Exception:
|
|
453
|
+
raise json.JSONDecodeError(
|
|
454
|
+
f"Could not parse LLM output as JSON",
|
|
455
|
+
raw[:100],
|
|
456
|
+
0
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
# Global router instance
|
|
461
|
+
_global_router = None
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def get_llm_router(prefer_local: bool = True) -> LLMRouter:
|
|
465
|
+
"""Get global LLM router instance."""
|
|
466
|
+
global _global_router
|
|
467
|
+
if _global_router is None:
|
|
468
|
+
_global_router = LLMRouter(prefer_local=prefer_local)
|
|
469
|
+
return _global_router
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def call_llama(prompt: str, timeout: int = 60) -> str:
|
|
473
|
+
"""
|
|
474
|
+
Call LLM with automatic fallback (backward compatible).
|
|
475
|
+
|
|
476
|
+
This function maintains backward compatibility with existing code.
|
|
477
|
+
"""
|
|
478
|
+
router = get_llm_router()
|
|
479
|
+
return router.call(prompt, timeout)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def safe_json_parse(raw: str, retry_timeout: int = 15) -> Dict:
|
|
483
|
+
"""
|
|
484
|
+
Parse JSON from LLM output (backward compatible).
|
|
485
|
+
"""
|
|
486
|
+
router = get_llm_router()
|
|
487
|
+
return router.safe_json_parse(raw, retry_timeout)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def check_ollama_available() -> bool:
|
|
491
|
+
"""Check if Ollama is available (backward compatible)."""
|
|
492
|
+
provider = OllamaProvider()
|
|
493
|
+
return provider.is_available()
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def check_any_llm_available() -> bool:
|
|
497
|
+
"""Check if ANY LLM provider is available."""
|
|
498
|
+
router = get_llm_router()
|
|
499
|
+
return len(router.get_available_providers()) > 0
|