-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathembedding.py
More file actions
106 lines (88 loc) · 3.25 KB
/
Copy pathembedding.py
File metadata and controls
106 lines (88 loc) · 3.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import time
from pathlib import Path
import numpy as np
from diskcache import Cache
from google import genai
from google.genai.errors import APIError
from google.genai.types import (
ContentEmbedding,
EmbedContentConfig,
EmbedContentResponse,
)
MODEL = "gemini-embedding-001"
BATCH_SIZE = 100
RETRYABLE_STATUS_CODES = {429, 503}
MAX_SLEEP = 60
# Values explicitly mentioned in
# https://ai.google.dev/gemini-api/docs/embeddings
VALID_DIMENSIONS = {128, 256, 512, 768, 1536, 2048, 3072}
CACHE = Cache(Path(__file__).resolve().parent / ".cache")
def _validate_dimension(dimension: int) -> None:
"""Raise ValueError if dimension is not a supported value."""
if dimension not in VALID_DIMENSIONS:
sorted_dims = sorted(VALID_DIMENSIONS)
raise ValueError(
f"Invalid dimension {dimension}. Must be one of {sorted_dims}."
)
def _normalize_embeddings(
embeddings: list[ContentEmbedding],
) -> list[list[float]]:
"""Normalize embedding objects to unit vectors."""
values = np.array([e.values for e in embeddings])
norms = np.linalg.norm(values, axis=1, keepdims=True)
return (values / norms).tolist()
def _embed_with_retry(
client: genai.Client,
model: str,
contents: list[str],
*,
dimension: int,
) -> EmbedContentResponse:
"""Call embed_content with exponential backoff on retryable errors."""
_validate_dimension(dimension)
attempt = 0
while True:
try:
return client.models.embed_content(
model=model,
contents=contents,
config=EmbedContentConfig(output_dimensionality=dimension),
)
except APIError as e:
if e.code not in RETRYABLE_STATUS_CODES:
raise
sleep_time = min(2**attempt, MAX_SLEEP)
time.sleep(sleep_time)
attempt += 1
def get_embeddings(
texts: list[str],
*,
dimension: int,
) -> list[list[float]]:
"""Get embeddings for texts, using cache when possible.
Checks the cache for each text. Uncached texts are fetched from the
Gemini API in batches, normalized, and written back to the cache.
"""
_validate_dimension(dimension)
results: dict[int, list[float]] = {}
uncached_indices: list[int] = []
for i, text in enumerate(texts):
cached = CACHE.get((text, dimension))
if cached is not None:
results[i] = cached
else:
uncached_indices.append(i)
if uncached_indices:
uncached_texts = [texts[i] for i in uncached_indices]
client = genai.Client()
all_embeddings: list[ContentEmbedding] = []
for batch_start in range(0, len(uncached_texts), BATCH_SIZE):
batch = uncached_texts[batch_start : batch_start + BATCH_SIZE]
response = _embed_with_retry(client, MODEL, batch, dimension=dimension)
assert response.embeddings is not None, "API returned no embeddings."
all_embeddings.extend(response.embeddings)
normalized = _normalize_embeddings(all_embeddings)
for idx, embedding in zip(uncached_indices, normalized):
results[idx] = embedding
CACHE.set((texts[idx], dimension), embedding)
return [results[i] for i in range(len(texts))]