NeerajCodz commited on
Commit
e5b4f8d
·
1 Parent(s): 101ad87

feat: add embeddings service and update plugins with NVIDIA support

Browse files
backend/app/api/routes/plugins.py CHANGED
@@ -237,6 +237,7 @@ PLUGIN_REGISTRY = {
237
  _installed_plugins: set[str] = {
238
  "google-api",
239
  "groq-api",
 
240
  "mcp-browser",
241
  "mcp-search",
242
  "mcp-html",
 
237
  _installed_plugins: set[str] = {
238
  "google-api",
239
  "groq-api",
240
+ "nvidia-api",
241
  "mcp-browser",
242
  "mcp-search",
243
  "mcp-html",
backend/app/core/embeddings.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Embeddings service for semantic search and similarity matching."""
2
+
3
+ import hashlib
4
+ import json
5
+ import logging
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+ import httpx
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Default embedding dimension for fallback
14
+ DEFAULT_EMBEDDING_DIM = 768
15
+
16
+
17
+ class EmbeddingsService:
18
+ """Service for generating embeddings using multiple providers."""
19
+
20
+ def __init__(
21
+ self,
22
+ provider: str = "openai",
23
+ model: str = "text-embedding-3-small",
24
+ api_key: str | None = None,
25
+ ):
26
+ """
27
+ Initialize embeddings service.
28
+
29
+ Args:
30
+ provider: Provider to use ('openai', 'google')
31
+ model: Model name for embeddings
32
+ api_key: API key for the provider
33
+ """
34
+ self.provider = provider
35
+ self.model = model
36
+ self.api_key = api_key
37
+ self._cache: dict[str, np.ndarray] = {} # In-memory cache
38
+
39
+ def _hash_text(self, text: str) -> str:
40
+ """Create a hash of text for cache key."""
41
+ return hashlib.sha256(text.encode()).hexdigest()[:32]
42
+
43
+ def _fallback_embedding(self, text: str, dimension: int = DEFAULT_EMBEDDING_DIM) -> np.ndarray:
44
+ """Generate a deterministic fallback embedding when providers fail."""
45
+ # Simple character-based embedding for fallback
46
+ values = [((ord(ch) % 97) / 97.0) for ch in text[:dimension]]
47
+ if not values:
48
+ values = [0.0]
49
+
50
+ # Repeat to fill dimension
51
+ repeats = (dimension + len(values) - 1) // len(values)
52
+ vector = (values * repeats)[:dimension]
53
+
54
+ return np.array(vector, dtype=np.float32)
55
+
56
+ async def embed_text(
57
+ self,
58
+ text: str,
59
+ task_type: str = "document",
60
+ ) -> np.ndarray:
61
+ """
62
+ Generate embedding for a single text.
63
+
64
+ Args:
65
+ text: Text to embed
66
+ task_type: Type of task ('document' or 'query')
67
+
68
+ Returns:
69
+ Embedding vector as numpy array
70
+ """
71
+ # Check cache
72
+ cache_key = self._hash_text(f"{self.provider}:{self.model}:{task_type}:{text}")
73
+ if cache_key in self._cache:
74
+ logger.debug(f"Embedding cache hit for text length {len(text)}")
75
+ return self._cache[cache_key]
76
+
77
+ try:
78
+ if self.provider == "openai":
79
+ embedding = await self._embed_openai(text)
80
+ elif self.provider == "google":
81
+ embedding = await self._embed_google(text, task_type)
82
+ else:
83
+ logger.warning(f"Unknown provider {self.provider}, using fallback")
84
+ embedding = self._fallback_embedding(text)
85
+
86
+ # Cache the result
87
+ self._cache[cache_key] = embedding
88
+ return embedding
89
+
90
+ except Exception as e:
91
+ logger.warning(f"Embedding failed: {e}, using fallback")
92
+ embedding = self._fallback_embedding(text)
93
+ self._cache[cache_key] = embedding
94
+ return embedding
95
+
96
+ async def _embed_openai(self, text: str) -> np.ndarray:
97
+ """Generate embedding using OpenAI API."""
98
+ if not self.api_key:
99
+ raise ValueError("OpenAI API key not provided")
100
+
101
+ url = "https://api.openai.com/v1/embeddings"
102
+ headers = {
103
+ "Authorization": f"Bearer {self.api_key}",
104
+ "Content-Type": "application/json",
105
+ }
106
+ payload = {
107
+ "model": self.model,
108
+ "input": text,
109
+ }
110
+
111
+ async with httpx.AsyncClient(timeout=30.0) as client:
112
+ response = await client.post(url, headers=headers, json=payload)
113
+ response.raise_for_status()
114
+ data = response.json()
115
+ embedding = data["data"][0]["embedding"]
116
+ return np.array(embedding, dtype=np.float32)
117
+
118
+ async def _embed_google(self, text: str, task_type: str = "document") -> np.ndarray:
119
+ """Generate embedding using Google Gemini API."""
120
+ if not self.api_key:
121
+ raise ValueError("Google API key not provided")
122
+
123
+ # Map task types to Google's task types
124
+ google_task_type = "RETRIEVAL_DOCUMENT" if task_type == "document" else "RETRIEVAL_QUERY"
125
+
126
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model}:embedContent"
127
+ params = {"key": self.api_key}
128
+ payload = {
129
+ "content": {"parts": [{"text": text}]},
130
+ "taskType": google_task_type,
131
+ }
132
+
133
+ async with httpx.AsyncClient(timeout=30.0) as client:
134
+ response = await client.post(url, params=params, json=payload)
135
+ response.raise_for_status()
136
+ data = response.json()
137
+ embedding = data["embedding"]["values"]
138
+ return np.array(embedding, dtype=np.float32)
139
+
140
+ async def embed_batch(self, texts: list[str]) -> np.ndarray:
141
+ """
142
+ Generate embeddings for multiple texts.
143
+
144
+ Args:
145
+ texts: List of texts to embed
146
+
147
+ Returns:
148
+ 2D numpy array of embeddings
149
+ """
150
+ if not texts:
151
+ return np.array([])
152
+
153
+ embeddings = []
154
+ for text in texts:
155
+ embedding = await self.embed_text(text)
156
+ embeddings.append(embedding)
157
+
158
+ return np.vstack(embeddings)
159
+
160
+ async def embed_query(self, query: str) -> np.ndarray:
161
+ """
162
+ Generate embedding for a search query.
163
+
164
+ Args:
165
+ query: Search query text
166
+
167
+ Returns:
168
+ Embedding vector as numpy array
169
+ """
170
+ return await self.embed_text(query, task_type="query")
171
+
172
+ def cosine_similarity(self, a: np.ndarray, b: np.ndarray) -> float:
173
+ """
174
+ Calculate cosine similarity between two vectors.
175
+
176
+ Args:
177
+ a: First vector
178
+ b: Second vector
179
+
180
+ Returns:
181
+ Cosine similarity score (0-1)
182
+ """
183
+ dot_product = np.dot(a, b)
184
+ norm_a = np.linalg.norm(a)
185
+ norm_b = np.linalg.norm(b)
186
+
187
+ if norm_a == 0 or norm_b == 0:
188
+ return 0.0
189
+
190
+ return float(dot_product / (norm_a * norm_b))
191
+
192
+ def find_most_similar(
193
+ self,
194
+ query_embedding: np.ndarray,
195
+ embeddings: list[np.ndarray],
196
+ top_k: int = 5,
197
+ ) -> list[tuple[int, float]]:
198
+ """
199
+ Find most similar embeddings to a query.
200
+
201
+ Args:
202
+ query_embedding: Query embedding vector
203
+ embeddings: List of embedding vectors to search
204
+ top_k: Number of top results to return
205
+
206
+ Returns:
207
+ List of (index, similarity_score) tuples, sorted by similarity
208
+ """
209
+ similarities = []
210
+ for idx, emb in enumerate(embeddings):
211
+ sim = self.cosine_similarity(query_embedding, emb)
212
+ similarities.append((idx, sim))
213
+
214
+ # Sort by similarity (descending)
215
+ similarities.sort(key=lambda x: x[1], reverse=True)
216
+ return similarities[:top_k]
217
+
218
+ def clear_cache(self) -> None:
219
+ """Clear the embedding cache."""
220
+ self._cache.clear()
221
+ logger.info("Embedding cache cleared")
222
+
223
+
224
+ # Factory function to create embeddings service
225
+ def create_embeddings_service(
226
+ provider: str = "openai",
227
+ model: str | None = None,
228
+ api_key: str | None = None,
229
+ ) -> EmbeddingsService:
230
+ """
231
+ Create an embeddings service instance.
232
+
233
+ Args:
234
+ provider: Provider name ('openai', 'google')
235
+ model: Model name (uses provider default if None)
236
+ api_key: API key for the provider
237
+
238
+ Returns:
239
+ EmbeddingsService instance
240
+ """
241
+ if model is None:
242
+ if provider == "openai":
243
+ model = "text-embedding-3-small"
244
+ elif provider == "google":
245
+ model = "text-embedding-004"
246
+ else:
247
+ raise ValueError(f"Unknown provider: {provider}")
248
+
249
+ return EmbeddingsService(provider=provider, model=model, api_key=api_key)