voyageai.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import os
  2. import numpy as np
  3. import pipmaster as pm # Pipmaster for dynamic library install
  4. # Add Voyage AI import
  5. if not pm.is_installed("voyageai"):
  6. pm.install("voyageai")
  7. import voyageai
  8. from voyageai.error import (
  9. APIConnectionError,
  10. RateLimitError,
  11. ServerError,
  12. ServiceUnavailableError,
  13. Timeout,
  14. TryAgain,
  15. )
  16. from tenacity import (
  17. retry,
  18. stop_after_attempt,
  19. wait_exponential,
  20. retry_if_exception_type,
  21. )
  22. from lightrag.utils import wrap_embedding_func_with_attrs, logger
  23. # Custom exceptions for VoyageAI errors
  24. class VoyageAIError(Exception):
  25. """Generic VoyageAI API error"""
  26. pass
  27. @wrap_embedding_func_with_attrs(
  28. embedding_dim=1024, max_token_size=32000, supports_asymmetric=True
  29. )
  30. @retry(
  31. stop=stop_after_attempt(3),
  32. wait=wait_exponential(multiplier=1, min=4, max=60),
  33. retry=retry_if_exception_type(
  34. (
  35. APIConnectionError,
  36. RateLimitError,
  37. ServerError,
  38. ServiceUnavailableError,
  39. Timeout,
  40. TryAgain,
  41. )
  42. ),
  43. )
  44. async def voyageai_embed(
  45. texts: list[str],
  46. model: str = "voyage-3",
  47. api_key: str | None = None,
  48. embedding_dim: int | None = None,
  49. input_type: str | None = None,
  50. truncation: bool | None = True,
  51. context: str | None = None,
  52. ) -> np.ndarray:
  53. """Generate embeddings for a list of texts using VoyageAI's API.
  54. Args:
  55. texts: List of texts to embed.
  56. model: The VoyageAI embedding model to use. Options include:
  57. - "voyage-3": General purpose (1024 dims, 32K context)
  58. - "voyage-3-lite": Lightweight (512 dims, 32K context)
  59. - "voyage-3-large": Highest accuracy (1024 dims, 32K context)
  60. - "voyage-code-3": Code optimized (1024 dims, 32K context)
  61. - "voyage-law-2": Legal documents (1024 dims, 16K context)
  62. - "voyage-finance-2": Finance (1024 dims, 32K context)
  63. api_key: Optional VoyageAI API key. If None, falls back to the
  64. ``VOYAGE_API_KEY`` environment variable (the name VoyageAI's own
  65. SDK uses), then to ``VOYAGEAI_API_KEY`` for backward compatibility.
  66. embedding_dim: Optional Matryoshka output dimension. Only honored by
  67. models that support dimension reduction (e.g. voyage-3-large);
  68. ignored otherwise. The decorator default is 1024 to match
  69. ``voyage-3``; if you select ``voyage-3-lite`` (512 dims) override
  70. ``EMBEDDING_DIM`` accordingly so the vector store size matches.
  71. input_type: Optional input type hint for the model. Options:
  72. - "query": For search queries
  73. - "document": For documents to be indexed
  74. - None: Let the model decide (default)
  75. truncation: Whether the API should truncate texts that exceed the model's
  76. token limit. Defaults to True (matches the VoyageAI SDK default).
  77. context: Optional LightRAG embedding context. When ``input_type`` is not
  78. set, "query" maps to ``input_type="query"`` and "document" maps to
  79. ``input_type="document"``.
  80. Returns:
  81. A numpy array of embeddings, one per input text.
  82. Raises:
  83. VoyageAIError: If the API call fails or returns invalid data.
  84. """
  85. if not api_key:
  86. api_key = os.environ.get("VOYAGE_API_KEY") or os.environ.get("VOYAGEAI_API_KEY")
  87. if not api_key:
  88. logger.error(
  89. "VoyageAI API key not provided and neither VOYAGE_API_KEY nor "
  90. "VOYAGEAI_API_KEY environment variable is set"
  91. )
  92. raise ValueError(
  93. "VoyageAI API key is required: pass api_key, or set the "
  94. "VOYAGE_API_KEY (preferred) or VOYAGEAI_API_KEY environment variable"
  95. )
  96. if input_type is None and context in {"query", "document"}:
  97. input_type = context
  98. try:
  99. client = voyageai.AsyncClient(api_key=api_key)
  100. total_chars = sum(len(t) for t in texts)
  101. avg_chars = total_chars / len(texts) if texts else 0
  102. logger.debug(
  103. f"VoyageAI embedding request: {len(texts)} texts, "
  104. f"total_chars={total_chars}, avg_chars={avg_chars:.0f}, model={model}, "
  105. f"input_type={input_type}"
  106. )
  107. # Prepare API call parameters
  108. embed_params = dict(
  109. texts=texts,
  110. model=model,
  111. # Optional parameters -- if None, voyageai client uses defaults
  112. output_dimension=embedding_dim,
  113. truncation=truncation,
  114. input_type=input_type,
  115. )
  116. # Make API call with timing
  117. result = await client.embed(**embed_params)
  118. if not result.embeddings:
  119. err_msg = "VoyageAI API returned empty embeddings"
  120. logger.error(err_msg)
  121. raise VoyageAIError(err_msg)
  122. if len(result.embeddings) != len(texts):
  123. err_msg = f"VoyageAI API returned {len(result.embeddings)} embeddings for {len(texts)} texts"
  124. logger.error(err_msg)
  125. raise VoyageAIError(err_msg)
  126. # Convert to numpy array with timing
  127. embeddings = np.array(result.embeddings, dtype=np.float32)
  128. logger.debug(f"VoyageAI embeddings generated: shape {embeddings.shape}")
  129. return embeddings
  130. except Exception as e:
  131. logger.error(f"VoyageAI embedding error: {e}")
  132. raise
  133. # Optional: a helper function to get available embedding models
  134. def get_available_embedding_models() -> dict[str, dict]:
  135. """
  136. Returns a dictionary of available Voyage AI embedding models and their properties.
  137. """
  138. return {
  139. "voyage-3-large": {
  140. "context_length": 32000,
  141. "dimension": 1024,
  142. "description": "Best general-purpose and multilingual",
  143. },
  144. "voyage-3": {
  145. "context_length": 32000,
  146. "dimension": 1024,
  147. "description": "General-purpose and multilingual",
  148. },
  149. "voyage-3-lite": {
  150. "context_length": 32000,
  151. "dimension": 512,
  152. "description": "Optimized for latency and cost",
  153. },
  154. "voyage-code-3": {
  155. "context_length": 32000,
  156. "dimension": 1024,
  157. "description": "Optimized for code",
  158. },
  159. "voyage-finance-2": {
  160. "context_length": 32000,
  161. "dimension": 1024,
  162. "description": "Optimized for finance",
  163. },
  164. "voyage-law-2": {
  165. "context_length": 16000,
  166. "dimension": 1024,
  167. "description": "Optimized for legal",
  168. },
  169. "voyage-multimodal-3": {
  170. "context_length": 32000,
  171. "dimension": 1024,
  172. "description": "Multimodal text and images",
  173. },
  174. }