Spaces:
Runtime error
Runtime error
Fix knowledge graph embedding model discovery
Browse files- 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 |
-
|
| 71 |
-
# format wants the "models/" prefix in the per-request
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 —
|
| 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=
|
| 797 |
task_type="RETRIEVAL_DOCUMENT",
|
| 798 |
timeout=timeout,
|
| 799 |
)
|
| 800 |
-
graph.set_vectors(vectors, 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
|