stackConsult commited on
Commit
93eb4a4
·
verified ·
1 Parent(s): 536d6c7

Add multi-LLM provider support (OpenAI, Ollama, HuggingFace) with unified interface for seamless switching

Browse files
Files changed (1) hide show
  1. smolagents/llm_provider.py +334 -0
smolagents/llm_provider.py ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Multi-LLM Provider Support
2
+
3
+ Unified interface for OpenAI, Ollama, and HuggingFace Inference API.
4
+ Enables seamless switching between LLM backends with parameterized configuration.
5
+ """
6
+
7
+ import os
8
+ from abc import ABC, abstractmethod
9
+ from typing import Optional, List, Dict, Any
10
+ from dataclasses import dataclass
11
+
12
+
13
+ @dataclass
14
+ class LLMResponse:
15
+ """Standardized LLM response"""
16
+ text: str
17
+ model: str
18
+ provider: str
19
+ tokens_used: Optional[int] = None
20
+ metadata: Optional[Dict[str, Any]] = None
21
+
22
+
23
+ class LLMProvider(ABC):
24
+ """Abstract base class for LLM providers"""
25
+
26
+ def __init__(self, model_name: str, **kwargs):
27
+ self.model_name = model_name
28
+ self.config = kwargs
29
+
30
+ @abstractmethod
31
+ def generate(self, prompt: str, **kwargs) -> LLMResponse:
32
+ """Generate text from prompt"""
33
+ pass
34
+
35
+ @abstractmethod
36
+ def score_response(self, prompt: str, response: str) -> float:
37
+ """Score quality of a response (0-1)"""
38
+ pass
39
+
40
+
41
+ class OpenAIProvider(LLMProvider):
42
+ """OpenAI API provider"""
43
+
44
+ def __init__(self, model_name: str = "gpt-4", api_key: Optional[str] = None, **kwargs):
45
+ super().__init__(model_name, **kwargs)
46
+ self.api_key = api_key or os.getenv("OPENAI_API_KEY")
47
+
48
+ try:
49
+ from openai import OpenAI
50
+ self.client = OpenAI(api_key=self.api_key)
51
+ self.available = True
52
+ except ImportError:
53
+ print("Warning: openai package not installed. Install with: pip install openai")
54
+ self.available = False
55
+ except Exception as e:
56
+ print(f"Warning: Could not initialize OpenAI client: {e}")
57
+ self.available = False
58
+
59
+ def generate(self, prompt: str, temperature: float = 0.7, max_tokens: int = 1000, **kwargs) -> LLMResponse:
60
+ if not self.available:
61
+ return LLMResponse(
62
+ text="OpenAI provider not available. Please install openai package and set API key.",
63
+ model=self.model_name,
64
+ provider="openai"
65
+ )
66
+
67
+ try:
68
+ response = self.client.chat.completions.create(
69
+ model=self.model_name,
70
+ messages=[{"role": "user", "content": prompt}],
71
+ temperature=temperature,
72
+ max_tokens=max_tokens,
73
+ **kwargs
74
+ )
75
+
76
+ return LLMResponse(
77
+ text=response.choices[0].message.content,
78
+ model=self.model_name,
79
+ provider="openai",
80
+ tokens_used=response.usage.total_tokens if hasattr(response, 'usage') else None,
81
+ metadata={"finish_reason": response.choices[0].finish_reason}
82
+ )
83
+ except Exception as e:
84
+ return LLMResponse(
85
+ text=f"Error generating response: {str(e)}",
86
+ model=self.model_name,
87
+ provider="openai"
88
+ )
89
+
90
+ def score_response(self, prompt: str, response: str) -> float:
91
+ """Score response quality using a simple prompt"""
92
+ if not self.available:
93
+ return 0.5
94
+
95
+ try:
96
+ score_prompt = f"""Rate the quality of this response on a scale of 0.0 to 1.0.
97
+
98
+ Prompt: {prompt}
99
+
100
+ Response: {response}
101
+
102
+ Provide only a number between 0.0 and 1.0:"""
103
+
104
+ result = self.generate(score_prompt, temperature=0.1, max_tokens=10)
105
+
106
+ # Extract number from response
107
+ try:
108
+ score = float(result.text.strip())
109
+ return max(0.0, min(1.0, score))
110
+ except ValueError:
111
+ return 0.5
112
+ except Exception:
113
+ return 0.5
114
+
115
+
116
+ class OllamaProvider(LLMProvider):
117
+ """Ollama local LLM provider"""
118
+
119
+ def __init__(self, model_name: str = "llama2", base_url: str = "http://localhost:11434", **kwargs):
120
+ super().__init__(model_name, **kwargs)
121
+ self.base_url = base_url
122
+
123
+ try:
124
+ import requests
125
+ self.requests = requests
126
+ # Test connection
127
+ response = requests.get(f"{base_url}/api/tags", timeout=2)
128
+ self.available = response.status_code == 200
129
+ except ImportError:
130
+ print("Warning: requests package not installed. Install with: pip install requests")
131
+ self.available = False
132
+ except Exception as e:
133
+ print(f"Warning: Could not connect to Ollama at {base_url}: {e}")
134
+ self.available = False
135
+
136
+ def generate(self, prompt: str, temperature: float = 0.7, max_tokens: int = 1000, **kwargs) -> LLMResponse:
137
+ if not self.available:
138
+ return LLMResponse(
139
+ text="Ollama provider not available. Please ensure Ollama is running at " + self.base_url,
140
+ model=self.model_name,
141
+ provider="ollama"
142
+ )
143
+
144
+ try:
145
+ response = self.requests.post(
146
+ f"{self.base_url}/api/generate",
147
+ json={
148
+ "model": self.model_name,
149
+ "prompt": prompt,
150
+ "temperature": temperature,
151
+ "num_predict": max_tokens,
152
+ "stream": False
153
+ },
154
+ timeout=60
155
+ )
156
+
157
+ if response.status_code == 200:
158
+ data = response.json()
159
+ return LLMResponse(
160
+ text=data.get("response", ""),
161
+ model=self.model_name,
162
+ provider="ollama",
163
+ metadata=data
164
+ )
165
+ else:
166
+ return LLMResponse(
167
+ text=f"Ollama error: {response.status_code}",
168
+ model=self.model_name,
169
+ provider="ollama"
170
+ )
171
+ except Exception as e:
172
+ return LLMResponse(
173
+ text=f"Error generating response: {str(e)}",
174
+ model=self.model_name,
175
+ provider="ollama"
176
+ )
177
+
178
+ def score_response(self, prompt: str, response: str) -> float:
179
+ """Simple heuristic scoring for Ollama"""
180
+ # Length-based heuristic (can be improved)
181
+ if len(response) < 10:
182
+ return 0.2
183
+ elif len(response) < 50:
184
+ return 0.5
185
+ elif len(response) < 200:
186
+ return 0.7
187
+ else:
188
+ return 0.9
189
+
190
+
191
+ class HuggingFaceProvider(LLMProvider):
192
+ """HuggingFace Inference API provider"""
193
+
194
+ def __init__(self, model_name: str = "HuggingFaceH4/zephyr-7b-beta", api_token: Optional[str] = None, **kwargs):
195
+ super().__init__(model_name, **kwargs)
196
+ self.api_token = api_token or os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
197
+
198
+ try:
199
+ from huggingface_hub import InferenceClient
200
+ self.client = InferenceClient(token=self.api_token)
201
+ self.available = True
202
+ except ImportError:
203
+ print("Warning: huggingface_hub not installed. Install with: pip install huggingface_hub")
204
+ self.available = False
205
+ except Exception as e:
206
+ print(f"Warning: Could not initialize HuggingFace client: {e}")
207
+ self.available = False
208
+
209
+ def generate(self, prompt: str, temperature: float = 0.7, max_tokens: int = 1000, **kwargs) -> LLMResponse:
210
+ if not self.available:
211
+ return LLMResponse(
212
+ text="HuggingFace provider not available. Please install huggingface_hub.",
213
+ model=self.model_name,
214
+ provider="huggingface"
215
+ )
216
+
217
+ try:
218
+ response = self.client.text_generation(
219
+ prompt,
220
+ model=self.model_name,
221
+ max_new_tokens=max_tokens,
222
+ temperature=temperature,
223
+ **kwargs
224
+ )
225
+
226
+ return LLMResponse(
227
+ text=response,
228
+ model=self.model_name,
229
+ provider="huggingface"
230
+ )
231
+ except Exception as e:
232
+ return LLMResponse(
233
+ text=f"Error generating response: {str(e)}",
234
+ model=self.model_name,
235
+ provider="huggingface"
236
+ )
237
+
238
+ def score_response(self, prompt: str, response: str) -> float:
239
+ """Simple heuristic scoring"""
240
+ # Basic quality metrics
241
+ score = 0.5
242
+
243
+ # Length check
244
+ if 50 <= len(response) <= 2000:
245
+ score += 0.2
246
+
247
+ # Contains relevant terms from prompt
248
+ prompt_words = set(prompt.lower().split())
249
+ response_words = set(response.lower().split())
250
+ overlap = len(prompt_words & response_words) / max(len(prompt_words), 1)
251
+ score += overlap * 0.3
252
+
253
+ return min(1.0, score)
254
+
255
+
256
+ class LLMManager:
257
+ """Unified LLM manager for seamless provider switching"""
258
+
259
+ def __init__(self, default_provider: str = "huggingface"):
260
+ self.providers: Dict[str, LLMProvider] = {}
261
+ self.default_provider = default_provider
262
+ self._initialize_providers()
263
+
264
+ def _initialize_providers(self):
265
+ """Initialize all available providers"""
266
+ # OpenAI
267
+ openai_model = os.getenv("OPENAI_MODEL", "gpt-4")
268
+ self.providers["openai"] = OpenAIProvider(model_name=openai_model)
269
+
270
+ # Ollama
271
+ ollama_model = os.getenv("OLLAMA_MODEL", "llama2")
272
+ ollama_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
273
+ self.providers["ollama"] = OllamaProvider(model_name=ollama_model, base_url=ollama_url)
274
+
275
+ # HuggingFace
276
+ hf_model = os.getenv("HF_MODEL", "HuggingFaceH4/zephyr-7b-beta")
277
+ self.providers["huggingface"] = HuggingFaceProvider(model_name=hf_model)
278
+
279
+ def generate(
280
+ self,
281
+ prompt: str,
282
+ provider: Optional[str] = None,
283
+ **kwargs
284
+ ) -> LLMResponse:
285
+ """Generate text using specified or default provider"""
286
+ provider_name = provider or self.default_provider
287
+
288
+ if provider_name not in self.providers:
289
+ return LLMResponse(
290
+ text=f"Provider '{provider_name}' not found. Available: {list(self.providers.keys())}",
291
+ model="unknown",
292
+ provider="unknown"
293
+ )
294
+
295
+ return self.providers[provider_name].generate(prompt, **kwargs)
296
+
297
+ def score_response(self, prompt: str, response: str, provider: Optional[str] = None) -> float:
298
+ """Score response quality"""
299
+ provider_name = provider or self.default_provider
300
+
301
+ if provider_name not in self.providers:
302
+ return 0.5
303
+
304
+ return self.providers[provider_name].score_response(prompt, response)
305
+
306
+ def get_available_providers(self) -> List[str]:
307
+ """Get list of available providers"""
308
+ return [
309
+ name for name, provider in self.providers.items()
310
+ if getattr(provider, 'available', True)
311
+ ]
312
+
313
+ def set_default_provider(self, provider: str):
314
+ """Set default provider"""
315
+ if provider in self.providers:
316
+ self.default_provider = provider
317
+ else:
318
+ raise ValueError(f"Provider '{provider}' not found")
319
+
320
+
321
+ # Singleton instance
322
+ _llm_manager = None
323
+
324
+ def get_llm_manager(default_provider: Optional[str] = None) -> LLMManager:
325
+ """Get or create LLM manager singleton"""
326
+ global _llm_manager
327
+
328
+ if _llm_manager is None:
329
+ provider = default_provider or os.getenv("LLM_PROVIDER", "huggingface")
330
+ _llm_manager = LLMManager(default_provider=provider)
331
+ elif default_provider:
332
+ _llm_manager.set_default_provider(default_provider)
333
+
334
+ return _llm_manager