Navegador

navegador / navegador / llm.py
Blame History Raw 333 lines
1
"""
2
LLM backend abstraction — unified provider interface with auto-discovery.
3
4
Provides a common interface for multiple LLM providers (Anthropic, OpenAI,
5
Ollama). SDK imports are lazy and guarded so that missing optional dependencies
6
produce a clear, actionable ImportError rather than a confusing traceback.
7
8
Usage::
9
10
from navegador.llm import get_provider, auto_provider, discover_providers
11
12
# Explicit provider
13
provider = get_provider("anthropic", model="claude-3-5-haiku-20241022")
14
response = provider.complete("Explain this function: ...")
15
16
# Auto-detect the first available SDK
17
provider = auto_provider()
18
19
# See what is installed
20
available = discover_providers() # e.g. ["anthropic", "openai"]
21
"""
22
23
from __future__ import annotations
24
25
from abc import ABC, abstractmethod
26
27
# ── Abstract base ─────────────────────────────────────────────────────────────
28
29
30
class LLMProvider(ABC):
31
"""Abstract interface that every concrete LLM provider must satisfy."""
32
33
@abstractmethod
34
def complete(self, prompt: str, **kwargs) -> str:
35
"""
36
Send *prompt* to the model and return the completion as a string.
37
38
Args:
39
prompt: The user/system prompt text.
40
**kwargs: Provider-specific options (temperature, max_tokens, …).
41
42
Returns:
43
The model's text response.
44
"""
45
46
@abstractmethod
47
def embed(self, text: str) -> list[float]:
48
"""
49
Return an embedding vector for *text*.
50
51
Args:
52
text: The input string to embed.
53
54
Returns:
55
A list of floats representing the embedding.
56
"""
57
58
@property
59
@abstractmethod
60
def name(self) -> str:
61
"""Short identifier for the provider, e.g. ``"anthropic"``."""
62
63
@property
64
@abstractmethod
65
def model(self) -> str:
66
"""Model identifier used for API calls, e.g. ``"claude-3-5-haiku-20241022"``."""
67
68
69
# ── Concrete providers ────────────────────────────────────────────────────────
70
71
72
class AnthropicProvider(LLMProvider):
73
"""
74
LLM provider backed by the ``anthropic`` Python SDK.
75
76
Install::
77
78
pip install anthropic
79
80
Args:
81
model: Anthropic model ID (default ``"claude-3-5-haiku-20241022"``).
82
"""
83
84
_DEFAULT_MODEL = "claude-3-5-haiku-20241022"
85
86
def __init__(self, model: str = "") -> None:
87
try:
88
import anthropic # noqa: F401
89
except ImportError as exc:
90
raise ImportError(
91
"The 'anthropic' package is required to use AnthropicProvider. "
92
"Install it with: pip install anthropic"
93
) from exc
94
95
self._model = model or self._DEFAULT_MODEL
96
97
import anthropic
98
99
self._client = anthropic.Anthropic()
100
101
@property
102
def name(self) -> str:
103
return "anthropic"
104
105
@property
106
def model(self) -> str:
107
return self._model
108
109
def complete(self, prompt: str, **kwargs) -> str:
110
"""Call the Anthropic Messages API and return the first text block."""
111
max_tokens = kwargs.pop("max_tokens", 1024)
112
message = self._client.messages.create(
113
model=self._model,
114
max_tokens=max_tokens,
115
messages=[{"role": "user", "content": prompt}],
116
**kwargs,
117
)
118
return message.content[0].text
119
120
def embed(self, text: str) -> list[float]:
121
"""
122
Anthropic does not currently expose a public embeddings API.
123
124
Raises:
125
NotImplementedError: Always.
126
"""
127
raise NotImplementedError(
128
"Anthropic does not provide a public embeddings API. "
129
"Use OpenAIProvider or OllamaProvider for embeddings."
130
)
131
132
133
class OpenAIProvider(LLMProvider):
134
"""
135
LLM provider backed by the ``openai`` Python SDK.
136
137
Install::
138
139
pip install openai
140
141
Args:
142
model: OpenAI model ID (default ``"gpt-4o-mini"``).
143
"""
144
145
_DEFAULT_MODEL = "gpt-4o-mini"
146
147
def __init__(self, model: str = "") -> None:
148
try:
149
import openai # noqa: F401
150
except ImportError as exc:
151
raise ImportError(
152
"The 'openai' package is required to use OpenAIProvider. "
153
"Install it with: pip install openai"
154
) from exc
155
156
self._model = model or self._DEFAULT_MODEL
157
158
import openai
159
160
self._client = openai.OpenAI()
161
162
@property
163
def name(self) -> str:
164
return "openai"
165
166
@property
167
def model(self) -> str:
168
return self._model
169
170
def complete(self, prompt: str, **kwargs) -> str:
171
"""Call the OpenAI Chat Completions API and return the assistant message."""
172
response = self._client.chat.completions.create(
173
model=self._model,
174
messages=[{"role": "user", "content": prompt}],
175
**kwargs,
176
)
177
return response.choices[0].message.content
178
179
def embed(self, text: str) -> list[float]:
180
"""Call the OpenAI Embeddings API and return the embedding vector."""
181
embed_model = "text-embedding-3-small"
182
response = self._client.embeddings.create(input=text, model=embed_model)
183
return response.data[0].embedding
184
185
186
class OllamaProvider(LLMProvider):
187
"""
188
LLM provider backed by the ``ollama`` Python SDK (local models via Ollama).
189
190
Install::
191
192
pip install ollama
193
194
The Ollama server must be running locally (``ollama serve``).
195
196
Args:
197
model: Ollama model tag (default ``"llama3.2"``).
198
"""
199
200
_DEFAULT_MODEL = "llama3.2"
201
202
def __init__(self, model: str = "") -> None:
203
try:
204
import ollama # noqa: F401
205
except ImportError as exc:
206
raise ImportError(
207
"The 'ollama' package is required to use OllamaProvider. "
208
"Install it with: pip install ollama"
209
) from exc
210
211
self._model = model or self._DEFAULT_MODEL
212
213
import ollama
214
215
self._client = ollama.Client()
216
217
@property
218
def name(self) -> str:
219
return "ollama"
220
221
@property
222
def model(self) -> str:
223
return self._model
224
225
def complete(self, prompt: str, **kwargs) -> str:
226
"""Call the Ollama chat API and return the assistant message content."""
227
response = self._client.chat(
228
model=self._model,
229
messages=[{"role": "user", "content": prompt}],
230
**kwargs,
231
)
232
return response["message"]["content"]
233
234
def embed(self, text: str) -> list[float]:
235
"""Call the Ollama embeddings API and return the embedding vector."""
236
response = self._client.embeddings(model=self._model, prompt=text)
237
return response["embedding"]
238
239
240
# ── Discovery & factory ───────────────────────────────────────────────────────
241
242
# Ordered list of known providers — also defines auto_provider priority.
243
_PROVIDER_NAMES: list[str] = ["anthropic", "openai", "ollama"]
244
245
_PROVIDER_SDK_MAP: dict[str, str] = {
246
"anthropic": "anthropic",
247
"openai": "openai",
248
"ollama": "ollama",
249
}
250
251
_PROVIDER_CLASS_MAP: dict[str, type[LLMProvider]] = {
252
"anthropic": AnthropicProvider,
253
"openai": OpenAIProvider,
254
"ollama": OllamaProvider,
255
}
256
257
258
def discover_providers() -> list[str]:
259
"""
260
Return a list of provider names whose SDKs are currently importable.
261
262
The list preserves the canonical priority order:
263
``["anthropic", "openai", "ollama"]``.
264
265
Returns:
266
List of available provider name strings.
267
"""
268
available: list[str] = []
269
for provider_name in _PROVIDER_NAMES:
270
sdk_name = _PROVIDER_SDK_MAP[provider_name]
271
try:
272
__import__(sdk_name)
273
available.append(provider_name)
274
except ImportError:
275
pass
276
return available
277
278
279
def get_provider(name: str, model: str = "") -> LLMProvider:
280
"""
281
Instantiate and return the named LLM provider.
282
283
Args:
284
name: One of ``"anthropic"``, ``"openai"``, or ``"ollama"``.
285
model: Optional model ID to pass to the provider constructor.
286
Falls back to each provider's built-in default.
287
288
Returns:
289
An :class:`LLMProvider` instance.
290
291
Raises:
292
ValueError: If *name* does not correspond to a known provider.
293
ImportError: If the underlying SDK is not installed.
294
"""
295
if name not in _PROVIDER_CLASS_MAP:
296
raise ValueError(
297
f"Unknown LLM provider: {name!r}. Valid options are: {sorted(_PROVIDER_CLASS_MAP)}"
298
)
299
cls = _PROVIDER_CLASS_MAP[name]
300
return cls(model=model)
301
302
303
def auto_provider(model: str = "") -> LLMProvider:
304
"""
305
Return the first available LLM provider based on installed SDKs.
306
307
Priority order: anthropic → openai → ollama.
308
309
Args:
310
model: Optional model ID forwarded to the provider constructor.
311
312
Returns:
313
An :class:`LLMProvider` instance for the first available SDK.
314
315
Raises:
316
RuntimeError: If no supported LLM SDK is installed.
317
"""
318
for provider_name in _PROVIDER_NAMES:
319
sdk_name = _PROVIDER_SDK_MAP[provider_name]
320
try:
321
__import__(sdk_name)
322
except ImportError:
323
continue
324
return get_provider(provider_name, model=model)
325
326
raise RuntimeError(
327
"No LLM SDK is installed. Install at least one of: "
328
"anthropic, openai, ollama.\n"
329
" pip install anthropic # Anthropic Claude\n"
330
" pip install openai # OpenAI GPT\n"
331
" pip install ollama # Ollama (local models)"
332
)
333

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button