Spaces:
Runtime error
Runtime error
Speed up knowledge graph builds by extracting pages concurrently
Browse files- pipeline/kg.py +42 -7
pipeline/kg.py
CHANGED
|
@@ -44,6 +44,7 @@ import time
|
|
| 44 |
import urllib.error
|
| 45 |
import urllib.request
|
| 46 |
from collections import deque
|
|
|
|
| 47 |
from dataclasses import dataclass, field
|
| 48 |
from typing import Optional
|
| 49 |
|
|
@@ -98,6 +99,19 @@ _RETRY_STATUSES = frozenset({429, 503})
|
|
| 98 |
_RETRY_ATTEMPTS = 4
|
| 99 |
_RETRY_BASE_DELAY = 1.5 # seconds; doubled each attempt
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
# Instruction for triple extraction. We ask for typed entities AND triples and
|
| 102 |
# force a JSON response via responseSchema for deterministic parsing.
|
| 103 |
_TRIPLE_PROMPT = (
|
|
@@ -906,13 +920,21 @@ def build_graph(
|
|
| 906 |
api_key : str
|
| 907 |
A Gemini API key (required; this is the opt-in online feature).
|
| 908 |
triple_model, embed_model : str, optional
|
| 909 |
-
Model overrides; default to Flash +
|
|
|
|
| 910 |
max_pages : int, optional
|
| 911 |
Cap the number of non-empty pages processed (cost control). None = all.
|
| 912 |
embed : bool
|
| 913 |
If True (default) also embed the nodes so semantic search works; if
|
| 914 |
False the graph supports lexical search only (cheaper).
|
| 915 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 916 |
Returns a graph (possibly empty if the document yields no facts). Raises a
|
| 917 |
single-line RuntimeError only for a missing key or a hard Gemini failure.
|
| 918 |
"""
|
|
@@ -928,15 +950,28 @@ def build_graph(
|
|
| 928 |
|
| 929 |
graph = KnowledgeGraph()
|
| 930 |
last_error: Optional[Exception] = None
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 935 |
try:
|
| 936 |
-
|
| 937 |
except RuntimeError as exc:
|
| 938 |
-
|
| 939 |
last_error = exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 940 |
continue
|
| 941 |
graph.pages_built += 1
|
| 942 |
# Register typed entities first so types are known, then triples.
|
|
|
|
| 44 |
import urllib.error
|
| 45 |
import urllib.request
|
| 46 |
from collections import deque
|
| 47 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 48 |
from dataclasses import dataclass, field
|
| 49 |
from typing import Optional
|
| 50 |
|
|
|
|
| 99 |
_RETRY_ATTEMPTS = 4
|
| 100 |
_RETRY_BASE_DELAY = 1.5 # seconds; doubled each attempt
|
| 101 |
|
| 102 |
+
# Build scalability: per-page triple extraction is one (I/O-bound) Gemini call
|
| 103 |
+
# each, so they're fanned across this SHARED, bounded pool instead of running
|
| 104 |
+
# strictly sequentially — a multi-page build is ~Nx faster on a key with real
|
| 105 |
+
# throughput. The pool is module-level on purpose: being shared, it also caps the
|
| 106 |
+
# TOTAL number of concurrent Gemini calls across simultaneous builds/users, so one
|
| 107 |
+
# big document can't blow the rate limit for everyone. Overshoot of the provider's
|
| 108 |
+
# rate limit is absorbed by the 429/503 backoff in _post_json. Graph mutation stays
|
| 109 |
+
# single-threaded (results are collected, then applied in deterministic page order).
|
| 110 |
+
_BUILD_CONCURRENCY = 5
|
| 111 |
+
_EXTRACT_POOL = ThreadPoolExecutor(
|
| 112 |
+
max_workers=_BUILD_CONCURRENCY, thread_name_prefix="kg-extract"
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
# Instruction for triple extraction. We ask for typed entities AND triples and
|
| 116 |
# force a JSON response via responseSchema for deterministic parsing.
|
| 117 |
_TRIPLE_PROMPT = (
|
|
|
|
| 920 |
api_key : str
|
| 921 |
A Gemini API key (required; this is the opt-in online feature).
|
| 922 |
triple_model, embed_model : str, optional
|
| 923 |
+
Model overrides; default to Flash + a discovered embedding model
|
| 924 |
+
(``_pick_embedding_model``, prefers gemini-embedding-001).
|
| 925 |
max_pages : int, optional
|
| 926 |
Cap the number of non-empty pages processed (cost control). None = all.
|
| 927 |
embed : bool
|
| 928 |
If True (default) also embed the nodes so semantic search works; if
|
| 929 |
False the graph supports lexical search only (cheaper).
|
| 930 |
|
| 931 |
+
Per-page triple extraction is fanned across a shared bounded pool
|
| 932 |
+
(``_EXTRACT_POOL``), so a multi-page build is much faster on a key with real
|
| 933 |
+
throughput; the shared pool also caps total concurrent Gemini calls. Each
|
| 934 |
+
page is isolated — a single failed page is skipped and counted
|
| 935 |
+
(``pages_failed``); only an all-pages failure raises. Embedding failure is
|
| 936 |
+
non-fatal (``embed_error`` set, lexical search retained).
|
| 937 |
+
|
| 938 |
Returns a graph (possibly empty if the document yields no facts). Raises a
|
| 939 |
single-line RuntimeError only for a missing key or a hard Gemini failure.
|
| 940 |
"""
|
|
|
|
| 950 |
|
| 951 |
graph = KnowledgeGraph()
|
| 952 |
last_error: Optional[Exception] = None
|
| 953 |
+
|
| 954 |
+
# Fan the per-page Gemini calls across the shared pool (concurrency), then
|
| 955 |
+
# build the graph single-threaded below in PAGE ORDER so node ids stay
|
| 956 |
+
# deterministic and the adjacency dicts are never mutated from two threads.
|
| 957 |
+
def _extract_one(text):
|
| 958 |
+
return extract_triples(text, api_key=api_key, model=triple_model, timeout=timeout)
|
| 959 |
+
|
| 960 |
+
futures = [(page_no, _EXTRACT_POOL.submit(_extract_one, text)) for page_no, text in usable]
|
| 961 |
+
results: dict = {}
|
| 962 |
+
for page_no, fut in futures:
|
| 963 |
+
# Isolate each page: one rate-limited / safety-blocked page (common on the
|
| 964 |
+
# free tier) must NOT discard every other page (and its API spend).
|
| 965 |
try:
|
| 966 |
+
results[page_no] = fut.result()
|
| 967 |
except RuntimeError as exc:
|
| 968 |
+
results[page_no] = exc
|
| 969 |
last_error = exc
|
| 970 |
+
|
| 971 |
+
for page_no, _text in usable:
|
| 972 |
+
data = results.get(page_no)
|
| 973 |
+
if not isinstance(data, dict): # a skipped page (exception) or missing
|
| 974 |
+
graph.pages_failed += 1
|
| 975 |
continue
|
| 976 |
graph.pages_built += 1
|
| 977 |
# Register typed entities first so types are known, then triples.
|