Archie0099 commited on
Commit
97c3931
·
verified ·
1 Parent(s): 0992cee

Fix knowledge graph embedding model discovery

Browse files
Files changed (1) hide show
  1. pipeline/kg.py +106 -7
pipeline/kg.py CHANGED
@@ -67,9 +67,20 @@ _BATCH_EMBED_URL = _API_BASE + "/{model}:batchEmbedContents"
67
  # Triple extraction must default to a FREE-TIER model (Flash). Pro/preview
68
  # models return HTTP 429 limit:0 on the free tier — see online_ocr notes.
69
  DEFAULT_TRIPLE_MODEL = "gemini-2.5-flash"
70
- # Embeddings: text-embedding-004 is the stable, free, 768-dim model. The wire
71
- # format wants the "models/" prefix in the per-request "model" field.
72
- DEFAULT_EMBED_MODEL = "text-embedding-004"
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  # Gemini caps batchEmbedContents at 100 requests per call; chunk to stay under.
75
  _EMBED_BATCH = 100
@@ -202,6 +213,89 @@ def _resolve_model(model, default: str) -> str:
202
  return name or default
203
 
204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  # ---------------------------------------------------------------------------
206
  # Response parsers (pure functions — unit-tested with canned dicts, no network).
207
  # ---------------------------------------------------------------------------
@@ -356,9 +450,11 @@ def embed_texts(
356
  """Embed a list of strings into an ``(n, dim)`` float32 matrix via Gemini.
357
 
358
  ``task_type`` should be ``RETRIEVAL_DOCUMENT`` for the graph's nodes and
359
- ``RETRIEVAL_QUERY`` for the search query — text-embedding-004 uses it to
360
  tune the vectors for retrieval, a real accuracy win. Batches at 100 inputs
361
- per request (Gemini's cap) and concatenates in order.
 
 
362
  """
363
  if not is_configured(api_key):
364
  raise RuntimeError(
@@ -789,14 +885,17 @@ def build_graph(
789
  graph._add_triple(t["subject"], t["predicate"], t["object"], page=page_no)
790
 
791
  if embed and graph.num_nodes:
 
 
 
792
  node_texts = [graph._node_embed_text(i) for i in range(graph.num_nodes)]
793
  vectors = embed_texts(
794
  node_texts,
795
  api_key=api_key,
796
- model=embed_model,
797
  task_type="RETRIEVAL_DOCUMENT",
798
  timeout=timeout,
799
  )
800
- graph.set_vectors(vectors, model=_resolve_model(embed_model, DEFAULT_EMBED_MODEL))
801
 
802
  return graph
 
67
  # Triple extraction must default to a FREE-TIER model (Flash). Pro/preview
68
  # models return HTTP 429 limit:0 on the free tier — see online_ocr notes.
69
  DEFAULT_TRIPLE_MODEL = "gemini-2.5-flash"
70
+
71
+ # Embeddings: the wire format wants the "models/" prefix in the per-request
72
+ # "model" field. The embedding model id has CHANGED over time — Google retired
73
+ # `text-embedding-004` on v1beta (it now 404s) in favour of `gemini-embedding-001`.
74
+ # So we DON'T trust a single hard-coded id: build_graph DISCOVERS the model the
75
+ # key can actually use via ModelService.ListModels (`_pick_embedding_model`),
76
+ # trying this preference order and falling back to the default only if discovery
77
+ # fails. Order newest→oldest so a key that still has an old model also works.
78
+ DEFAULT_EMBED_MODEL = "gemini-embedding-001"
79
+ EMBED_MODEL_PREFERENCE = (
80
+ "gemini-embedding-001", # current GA model (default ~3072-dim; we L2-normalize)
81
+ "text-embedding-004", # legacy 768-dim (retired on v1beta for many keys)
82
+ "embedding-001", # older fallback
83
+ )
84
 
85
  # Gemini caps batchEmbedContents at 100 requests per call; chunk to stay under.
86
  _EMBED_BATCH = 100
 
213
  return name or default
214
 
215
 
216
+ # Process-level cache of the discovered embedding model. Only a SUCCESSFUL
217
+ # discovery is cached (so a transient failure that fell back to the default is
218
+ # retried next time). Not keyed by the API key — the available models are a
219
+ # property of the deployment, not a secret, and we never store the key itself.
220
+ _EMBED_MODEL_RESOLVED = None
221
+
222
+
223
+ def _list_embedding_models(api_key, *, timeout=30) -> list:
224
+ """Return bare ids of models the key can use for embedding.
225
+
226
+ Calls ``GET /v1beta/models`` and keeps those whose
227
+ ``supportedGenerationMethods`` include ``batchEmbedContents`` (what we call)
228
+ or ``embedContent``. Used to pick a model that actually exists, instead of
229
+ hard-coding an id that Google may retire (the cause of the text-embedding-004
230
+ 404). Mirrors ``online_ocr.list_models`` but filters for embedding support.
231
+ """
232
+ api_key = _clean_key(api_key)
233
+ request = urllib.request.Request(
234
+ _API_BASE, method="GET", headers={_API_KEY_HEADER: api_key}
235
+ )
236
+ try:
237
+ with urllib.request.urlopen(request, timeout=timeout) as response:
238
+ raw = response.read()
239
+ except urllib.error.HTTPError as err:
240
+ raise _friendly_http_error(err) from None
241
+ except urllib.error.URLError as err:
242
+ raise RuntimeError(
243
+ "Could not reach the Gemini API ({}). Check your internet "
244
+ "connection.".format(err.reason)
245
+ ) from None
246
+ except TimeoutError:
247
+ raise RuntimeError(
248
+ "The Gemini request timed out after {}s.".format(timeout)
249
+ ) from None
250
+ try:
251
+ payload = json.loads(raw.decode("utf-8", "replace"))
252
+ except (ValueError, AttributeError):
253
+ return []
254
+ out = []
255
+ for entry in (payload.get("models") or []):
256
+ if not isinstance(entry, dict):
257
+ continue
258
+ methods = entry.get("supportedGenerationMethods") or []
259
+ if "batchEmbedContents" in methods or "embedContent" in methods:
260
+ name = str(entry.get("name") or "")
261
+ if name.startswith("models/"):
262
+ name = name[len("models/"):]
263
+ if name:
264
+ out.append(name)
265
+ return out
266
+
267
+
268
+ def _pick_embedding_model(api_key, requested=None, *, timeout=30) -> str:
269
+ """Choose an embedding model id the key can actually use.
270
+
271
+ An explicit ``requested`` model wins. Otherwise discover the available
272
+ embedding models and pick by :data:`EMBED_MODEL_PREFERENCE`; on a transient
273
+ discovery failure fall back to :data:`DEFAULT_EMBED_MODEL` (without caching,
274
+ so it's retried). A successful discovery is cached for the process.
275
+ """
276
+ global _EMBED_MODEL_RESOLVED
277
+ if requested:
278
+ return _resolve_model(requested, DEFAULT_EMBED_MODEL)
279
+ if _EMBED_MODEL_RESOLVED:
280
+ return _EMBED_MODEL_RESOLVED
281
+ try:
282
+ available = set(_list_embedding_models(api_key, timeout=timeout))
283
+ except RuntimeError:
284
+ available = set()
285
+ chosen = next((m for m in EMBED_MODEL_PREFERENCE if m in available), None)
286
+ if not chosen and available:
287
+ # An unfamiliar but embedding-capable model — prefer one that looks like
288
+ # an embedding model, else just take the first deterministically.
289
+ chosen = next(
290
+ (m for m in sorted(available) if "embedding" in m.lower()),
291
+ sorted(available)[0],
292
+ )
293
+ if chosen:
294
+ _EMBED_MODEL_RESOLVED = chosen # cache only a real discovery
295
+ return chosen
296
+ return DEFAULT_EMBED_MODEL
297
+
298
+
299
  # ---------------------------------------------------------------------------
300
  # Response parsers (pure functions — unit-tested with canned dicts, no network).
301
  # ---------------------------------------------------------------------------
 
450
  """Embed a list of strings into an ``(n, dim)`` float32 matrix via Gemini.
451
 
452
  ``task_type`` should be ``RETRIEVAL_DOCUMENT`` for the graph's nodes and
453
+ ``RETRIEVAL_QUERY`` for the search query — the embedding model uses it to
454
  tune the vectors for retrieval, a real accuracy win. Batches at 100 inputs
455
+ per request (Gemini's cap) and concatenates in order. Pass an explicit
456
+ ``model`` (typically resolved by :func:`_pick_embedding_model`); the default
457
+ is only a last resort since Google retires embedding-model ids over time.
458
  """
459
  if not is_configured(api_key):
460
  raise RuntimeError(
 
885
  graph._add_triple(t["subject"], t["predicate"], t["object"], page=page_no)
886
 
887
  if embed and graph.num_nodes:
888
+ # Resolve the embedding model the key can actually use (Google retires
889
+ # ids over time), so search uses the SAME model the nodes were built with.
890
+ resolved = _pick_embedding_model(api_key, embed_model, timeout=min(timeout, 30))
891
  node_texts = [graph._node_embed_text(i) for i in range(graph.num_nodes)]
892
  vectors = embed_texts(
893
  node_texts,
894
  api_key=api_key,
895
+ model=resolved,
896
  task_type="RETRIEVAL_DOCUMENT",
897
  timeout=timeout,
898
  )
899
+ graph.set_vectors(vectors, model=resolved)
900
 
901
  return graph