-
Notifications
You must be signed in to change notification settings - Fork 228
Expand file tree
/
Copy pathopenai_provider.py
More file actions
99 lines (83 loc) · 3.52 KB
/
Copy pathopenai_provider.py
File metadata and controls
99 lines (83 loc) · 3.52 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
"""OpenAI-based embedding provider for cloud or API-backed semantic indexing."""
from __future__ import annotations
import asyncio
import os
from typing import Any
from basic_memory.repository.embedding_provider import EmbeddingProvider
from basic_memory.repository.semantic_errors import SemanticDependenciesMissingError
class OpenAIEmbeddingProvider(EmbeddingProvider):
"""Embedding provider backed by OpenAI's embeddings API."""
def __init__(
self,
model_name: str = "text-embedding-3-small",
*,
batch_size: int = 64,
dimensions: int = 1536,
api_key: str | None = None,
base_url: str | None = None,
timeout: float = 30.0,
) -> None:
self.model_name = model_name
self.dimensions = dimensions
self.batch_size = batch_size
self._api_key = api_key
self._base_url = base_url
self._timeout = timeout
self._client: Any | None = None
self._client_lock = asyncio.Lock()
async def _get_client(self) -> Any:
if self._client is not None:
return self._client
async with self._client_lock:
if self._client is not None:
return self._client
try:
from openai import AsyncOpenAI # type: ignore[import-not-found]
except ImportError as exc: # pragma: no cover - covered via monkeypatch tests
raise SemanticDependenciesMissingError(
"OpenAI dependency is missing. "
"Install/update basic-memory to include semantic dependencies: "
"pip install -U basic-memory"
) from exc
api_key = self._api_key or os.getenv("OPENAI_API_KEY")
if not api_key:
raise SemanticDependenciesMissingError(
"OpenAI embedding provider requires OPENAI_API_KEY."
)
self._client = AsyncOpenAI(
api_key=api_key,
base_url=self._base_url,
timeout=self._timeout,
)
return self._client
async def embed_documents(self, texts: list[str]) -> list[list[float]]:
if not texts:
return []
client = await self._get_client()
all_vectors: list[list[float]] = []
for start in range(0, len(texts), self.batch_size):
batch = texts[start : start + self.batch_size]
response = await client.embeddings.create(
model=self.model_name,
input=batch,
)
vectors_by_index: dict[int, list[float]] = {
int(item.index): [float(value) for value in item.embedding]
for item in response.data
}
for index in range(len(batch)):
vector = vectors_by_index.get(index)
if vector is None:
raise RuntimeError(
"OpenAI embedding response is missing expected vector index."
)
all_vectors.append(vector)
if all_vectors and len(all_vectors[0]) != self.dimensions:
raise RuntimeError(
f"Embedding model returned {len(all_vectors[0])}-dimensional vectors "
f"but provider was configured for {self.dimensions} dimensions."
)
return all_vectors
async def embed_query(self, text: str) -> list[float]:
vectors = await self.embed_documents([text])
return vectors[0] if vectors else [0.0] * self.dimensions