diff --git a/.doctrees/apis/components/components.model_client.anthropic_client.doctree b/.doctrees/apis/components/components.model_client.anthropic_client.doctree index 792bb03d..1358c42c 100644 Binary files a/.doctrees/apis/components/components.model_client.anthropic_client.doctree and b/.doctrees/apis/components/components.model_client.anthropic_client.doctree differ diff --git a/.doctrees/apis/components/components.model_client.cohere_client.doctree b/.doctrees/apis/components/components.model_client.cohere_client.doctree index f74db120..272dacbf 100644 Binary files a/.doctrees/apis/components/components.model_client.cohere_client.doctree and b/.doctrees/apis/components/components.model_client.cohere_client.doctree differ diff --git a/.doctrees/apis/components/components.model_client.doctree b/.doctrees/apis/components/components.model_client.doctree index a0150f07..ce6c851f 100644 Binary files a/.doctrees/apis/components/components.model_client.doctree and b/.doctrees/apis/components/components.model_client.doctree differ diff --git a/.doctrees/apis/components/components.model_client.groq_client.doctree b/.doctrees/apis/components/components.model_client.groq_client.doctree index c354c44e..19c807e8 100644 Binary files a/.doctrees/apis/components/components.model_client.groq_client.doctree and b/.doctrees/apis/components/components.model_client.groq_client.doctree differ diff --git a/.doctrees/apis/components/components.model_client.openai_client.doctree b/.doctrees/apis/components/components.model_client.openai_client.doctree index 2942081c..17d31e95 100644 Binary files a/.doctrees/apis/components/components.model_client.openai_client.doctree and b/.doctrees/apis/components/components.model_client.openai_client.doctree differ diff --git a/.doctrees/apis/components/components.retriever.doctree b/.doctrees/apis/components/components.retriever.doctree index 49171eae..e74c684f 100644 Binary files a/.doctrees/apis/components/components.retriever.doctree and b/.doctrees/apis/components/components.retriever.doctree differ diff --git a/.doctrees/apis/components/components.retriever.faiss_retriever.doctree b/.doctrees/apis/components/components.retriever.faiss_retriever.doctree index 4c937cc7..af7f69f3 100644 Binary files a/.doctrees/apis/components/components.retriever.faiss_retriever.doctree and b/.doctrees/apis/components/components.retriever.faiss_retriever.doctree differ diff --git a/.doctrees/apis/components/components.retriever.postgres_retriever.doctree b/.doctrees/apis/components/components.retriever.postgres_retriever.doctree index f18fbb94..d4a2358f 100644 Binary files a/.doctrees/apis/components/components.retriever.postgres_retriever.doctree and b/.doctrees/apis/components/components.retriever.postgres_retriever.doctree differ diff --git a/.doctrees/apis/components/index.doctree b/.doctrees/apis/components/index.doctree index 30680f90..6b59811e 100644 Binary files a/.doctrees/apis/components/index.doctree and b/.doctrees/apis/components/index.doctree differ diff --git a/.doctrees/apis/core/core.types.doctree b/.doctrees/apis/core/core.types.doctree index 630a70e3..14ed5102 100644 Binary files a/.doctrees/apis/core/core.types.doctree and b/.doctrees/apis/core/core.types.doctree differ diff --git a/.doctrees/apis/index.doctree b/.doctrees/apis/index.doctree index ceaa5f0c..5b755c37 100644 Binary files a/.doctrees/apis/index.doctree and b/.doctrees/apis/index.doctree differ diff --git a/.doctrees/apis/utils/utils.lazy_import.doctree b/.doctrees/apis/utils/utils.lazy_import.doctree index dd543b75..5d2d6f7c 100644 Binary files a/.doctrees/apis/utils/utils.lazy_import.doctree and b/.doctrees/apis/utils/utils.lazy_import.doctree differ diff --git a/.doctrees/developer_notes/lightrag_design_philosophy.doctree b/.doctrees/developer_notes/lightrag_design_philosophy.doctree index 622db6cf..d6c7e98d 100644 Binary files a/.doctrees/developer_notes/lightrag_design_philosophy.doctree and b/.doctrees/developer_notes/lightrag_design_philosophy.doctree differ diff --git a/.doctrees/environment.pickle b/.doctrees/environment.pickle index 5ece64e4..39629ccf 100644 Binary files a/.doctrees/environment.pickle and b/.doctrees/environment.pickle differ diff --git a/.doctrees/get_started/lightrag_in_10_mins.doctree b/.doctrees/get_started/lightrag_in_10_mins.doctree index 6126060a..929e0a32 100644 Binary files a/.doctrees/get_started/lightrag_in_10_mins.doctree and b/.doctrees/get_started/lightrag_in_10_mins.doctree differ diff --git a/.doctrees/index.doctree b/.doctrees/index.doctree index e64b85c2..4fb655b3 100644 Binary files a/.doctrees/index.doctree and b/.doctrees/index.doctree differ diff --git a/_modules/components/model_client/anthropic_client.html b/_modules/components/model_client/anthropic_client.html new file mode 100644 index 00000000..1739bad8 --- /dev/null +++ b/_modules/components/model_client/anthropic_client.html @@ -0,0 +1,642 @@ + + + + + + + + + + components.model_client.anthropic_client — LightRAG documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for components.model_client.anthropic_client

+"""Anthropic ModelClient integration."""
+
+import os
+from typing import Dict, Optional, Any, Callable
+import backoff
+import logging
+
+
+from lightrag.core.model_client import ModelClient
+from lightrag.core.types import ModelType
+
+# optional import
+from lightrag.utils.lazy_import import safe_import, OptionalPackages
+
+anthropic = safe_import(
+    OptionalPackages.ANTHROPIC.value[0], OptionalPackages.ANTHROPIC.value[1]
+)
+
+from anthropic import (
+    RateLimitError,
+    APITimeoutError,
+    InternalServerError,
+    UnprocessableEntityError,
+    BadRequestError,
+)
+from anthropic.types import Message
+
+log = logging.getLogger(__name__)
+
+
+
+[docs] +def get_first_message_content(completion: Message) -> str: + r"""When we only need the content of the first message. + It is the default parser for chat completion.""" + return completion.content[0].text
+ + + +__all__ = ["AnthropicAPIClient", "get_first_message_content"] + + +# NOTE: using customize parser might make the new_component more complex when we have to handle a callable +
+[docs] +class AnthropicAPIClient(ModelClient): + __doc__ = r"""A component wrapper for the Anthropic API client. + + Visit https://docs.anthropic.com/en/docs/intro-to-claude for more api details. + """ + + def __init__( + self, + api_key: Optional[str] = None, + chat_completion_parser: Callable[[Message], Any] = None, + ): + r"""It is recommended to set the ANTHROPIC_API_KEY environment variable instead of passing it as an argument.""" + super().__init__() + self._api_key = api_key + self.sync_client = self.init_sync_client() + self.async_client = None # only initialize if the async call is called + self.tested_llm_models = ["claude-3-opus-20240229"] + self.chat_completion_parser = ( + chat_completion_parser or get_first_message_content + ) + +
+[docs] + def init_sync_client(self): + api_key = self._api_key or os.getenv("ANTHROPIC_API_KEY") + if not api_key: + raise ValueError("Environment variable ANTHROPIC_API_KEY must be set") + return anthropic.Anthropic(api_key=api_key)
+ + +
+[docs] + def init_async_client(self): + api_key = self._api_key or os.getenv("ANTHROPIC_API_KEY") + if not api_key: + raise ValueError("Environment variable ANTHROPIC_API_KEY must be set") + return anthropic.AsyncAnthropic(api_key=api_key)
+ + +
+[docs] + def parse_chat_completion(self, completion: Message) -> str: + log.debug(f"completion: {completion}") + return completion.content[0].text
+ + + # TODO: potentially use <SYS></SYS> to separate the system and user messages. This requires user to follow it. If it is not found, then we will only use user message. +
+[docs] + def convert_inputs_to_api_kwargs( + self, + input: Optional[Any] = None, + model_kwargs: Dict = {}, + model_type: ModelType = ModelType.UNDEFINED, + ) -> dict: + r"""Anthropic API messages separates the system and the user messages. + + As we focus on one prompt, we have to use the user message as the input. + + api: https://docs.anthropic.com/en/api/messages + """ + api_kwargs = model_kwargs.copy() + if model_type == ModelType.LLM: + api_kwargs["messages"] = [ + {"role": "user", "content": input}, + ] + # if input and input != "": + # api_kwargs["system"] = input + else: + raise ValueError(f"Model type {model_type} not supported") + return api_kwargs
+ + +
+[docs] + @backoff.on_exception( + backoff.expo, + ( + APITimeoutError, + InternalServerError, + RateLimitError, + UnprocessableEntityError, + BadRequestError, + ), + max_time=5, + ) + def call(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED): + """ + kwargs is the combined input and model_kwargs + """ + if model_type == ModelType.EMBEDDER: + raise ValueError(f"Model type {model_type} not supported") + elif model_type == ModelType.LLM: + return self.sync_client.messages.create(**api_kwargs) + else: + raise ValueError(f"model_type {model_type} is not supported")
+ + +
+[docs] + @backoff.on_exception( + backoff.expo, + ( + APITimeoutError, + InternalServerError, + RateLimitError, + UnprocessableEntityError, + BadRequestError, + ), + max_time=5, + ) + async def acall( + self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED + ): + """ + kwargs is the combined input and model_kwargs + """ + if self.async_client is None: + self.async_client = self.init_async_client() + if model_type == ModelType.EMBEDDER: + raise ValueError(f"Model type {model_type} not supported") + elif model_type == ModelType.LLM: + return await self.async_client.messages.create(**api_kwargs) + else: + raise ValueError(f"model_type {model_type} is not supported")
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/components/model_client/cohere_client.html b/_modules/components/model_client/cohere_client.html new file mode 100644 index 00000000..f66e4fef --- /dev/null +++ b/_modules/components/model_client/cohere_client.html @@ -0,0 +1,625 @@ + + + + + + + + + + components.model_client.cohere_client — LightRAG documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for components.model_client.cohere_client

+"""Cohere ModelClient integration."""
+
+import os
+from typing import Dict, Optional, Any
+import backoff
+from lightrag.utils.lazy_import import safe_import, OptionalPackages
+
+cohere = safe_import(OptionalPackages.COHERE.value[0], OptionalPackages.COHERE.value[1])
+from cohere import (
+    BadRequestError,
+    InternalServerError,
+)
+
+
+from lightrag.core.model_client import ModelClient
+from lightrag.core.types import ModelType
+
+
+
+[docs] +class CohereAPIClient(ModelClient): + __doc__ = r"""A component wrapper for the Cohere API. + + Visit https://docs.cohere.com/ for more api details. + + References: + - Cohere reranker: https://docs.cohere.com/reference/rerank + + Tested Cohere models: 6/16/2024 + - rerank-english-v3.0, rerank-multilingual-v3.0, rerank-english-v2.0, rerank-multilingual-v2.0 + + .. note:: + For all ModelClient integration, such as CohereAPIClient, if you want to subclass CohereAPIClient, you need to import it from the module directly. + + ``from lightrag.components.model_client.cohere_client import CohereAPIClient`` + + instead of using the lazy import with: + + ``from lightrag.components.model_client import CohereAPIClient`` + """ + + def __init__(self, api_key: Optional[str] = None): + r"""It is recommended to set the GROQ_API_KEY environment variable instead of passing it as an argument. + + Args: + api_key (Optional[str], optional): Groq API key. Defaults to None. + """ + super().__init__() + self._api_key = api_key + self.init_sync_client() + + self.async_client = None # only initialize if the async call is called + +
+[docs] + def init_sync_client(self): + api_key = self._api_key or os.getenv("COHERE_API_KEY") + if not api_key: + raise ValueError("Environment variable COHERE_API_KEY must be set") + self.sync_client = cohere.Client(api_key=api_key)
+ + +
+[docs] + def init_async_client(self): + api_key = self._api_key or os.getenv("COHERE_API_KEY") + if not api_key: + raise ValueError("Environment variable COHERE_API_KEY must be set") + self.async_client = cohere.AsyncClient(api_key=api_key)
+ + +
+[docs] + def convert_inputs_to_api_kwargs( + self, + input: Optional[Any] = None, # for retriever, it is a list of string. + model_kwargs: Dict = {}, + model_type: ModelType = ModelType.UNDEFINED, + ) -> Dict: + r""" + For rerank model, expect model_kwargs to have the following keys: + model: str, + query: str, + documents: List[str], + top_n: int, + """ + final_model_kwargs = model_kwargs.copy() + if model_type == ModelType.RERANKER: + final_model_kwargs["query"] = input + if "model" not in final_model_kwargs: + raise ValueError("model must be specified") + if "documents" not in final_model_kwargs: + raise ValueError("documents must be specified") + if "top_k" not in final_model_kwargs: + raise ValueError("top_k must be specified") + + # convert top_k to the api specific, which is top_n + final_model_kwargs["top_n"] = final_model_kwargs.pop("top_k") + return final_model_kwargs + else: + raise ValueError(f"model_type {model_type} is not supported")
+ + +
+[docs] + @backoff.on_exception( + backoff.expo, + ( + BadRequestError, + InternalServerError, + ), + max_time=5, + ) + def call(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED): + assert ( + "model" in api_kwargs + ), f"model must be specified in api_kwargs: {api_kwargs}" + if ( + model_type == ModelType.RERANKER + ): # query -> # scores for top_k documents, index for the top_k documents, return as tuple + + response = self.sync_client.rerank(**api_kwargs) + top_k_scores = [result.relevance_score for result in response.results] + top_k_indices = [result.index for result in response.results] + return top_k_indices, top_k_scores + else: + raise ValueError(f"model_type {model_type} is not supported")
+ + +
+[docs] + @backoff.on_exception( + backoff.expo, + ( + BadRequestError, + InternalServerError, + ), + max_time=5, + ) + async def acall( + self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED + ): + if self.async_client is None: + self.init_async_client() + if "model" not in api_kwargs: + raise ValueError("model must be specified") + if model_type == ModelType.RERANKER: + response = await self.async_client.rerank(**api_kwargs) + top_k_scores = [result.relevance_score for result in response.results] + top_k_indices = [result.index for result in response.results] + return top_k_indices, top_k_scores + else: + raise ValueError(f"model_type {model_type} is not supported")
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/components/model_client/google_client.html b/_modules/components/model_client/google_client.html index 962f348f..c867ce31 100644 --- a/_modules/components/model_client/google_client.html +++ b/_modules/components/model_client/google_client.html @@ -481,7 +481,8 @@

Source code for components.model_client.google_client

if isinstance(input, str): input = [input] # convert input to input - assert isinstance(input, Sequence), "input must be a sequence of text" + if not isinstance(input, Sequence): + raise TypeError("input must be a sequence of text") final_model_kwargs["input"] = input elif model_type == ModelType.LLM: diff --git a/_modules/components/model_client/groq_client.html b/_modules/components/model_client/groq_client.html new file mode 100644 index 00000000..6bfce716 --- /dev/null +++ b/_modules/components/model_client/groq_client.html @@ -0,0 +1,614 @@ + + + + + + + + + + components.model_client.groq_client — LightRAG documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for components.model_client.groq_client

+"""Groq ModelClient integration."""
+
+import os
+from typing import Dict, Sequence, Optional, Any
+import backoff
+from lightrag.core.model_client import ModelClient
+from lightrag.core.types import ModelType
+
+
+from lightrag.utils.lazy_import import safe_import, OptionalPackages
+
+# optional import
+groq = safe_import(OptionalPackages.GROQ.value[0], OptionalPackages.GROQ.value[1])
+
+from groq import Groq, AsyncGroq
+from groq import (
+    APITimeoutError,
+    InternalServerError,
+    RateLimitError,
+    UnprocessableEntityError,
+)
+
+
+
+[docs] +class GroqAPIClient(ModelClient): + __doc__ = r"""A component wrapper for the Groq API client. + + Visit https://console.groq.com/docs/ for more api details. + Check https://console.groq.com/docs/models for the available models. + + Tested Groq models: 4/22/2024 + - llama3-8b-8192 + - llama3-70b-8192 + - mixtral-8x7b-32768 + - gemma-7b-it + """ + + def __init__(self, api_key: Optional[str] = None): + r"""It is recommended to set the GROQ_API_KEY environment variable instead of passing it as an argument. + + Args: + api_key (Optional[str], optional): Groq API key. Defaults to None. + """ + super().__init__() + self._api_key = api_key + self.init_sync_client() + + self.async_client = None # only initialize if the async call is called + +
+[docs] + def init_sync_client(self): + api_key = self._api_key or os.getenv("GROQ_API_KEY") + if not api_key: + raise ValueError("Environment variable GROQ_API_KEY must be set") + self.sync_client = Groq(api_key=api_key)
+ + +
+[docs] + def init_async_client(self): + api_key = self._api_key or os.getenv("GROQ_API_KEY") + if not api_key: + raise ValueError("Environment variable GROQ_API_KEY must be set") + self.async_client = AsyncGroq(api_key=api_key)
+ + +
+[docs] + def parse_chat_completion(self, completion: Any) -> str: + """ + Parse the completion to a string output. + """ + return completion.choices[0].message.content
+ + +
+[docs] + def convert_inputs_to_api_kwargs( + self, + input: Optional[Any] = None, + model_kwargs: Dict = {}, + model_type: ModelType = ModelType.UNDEFINED, + ) -> Dict: + final_model_kwargs = model_kwargs.copy() + if model_type == ModelType.LLM: + messages: Sequence[Dict[str, str]] = [] + if input is not None and input != "": + messages.append({"role": "system", "content": input}) + final_model_kwargs["messages"] = messages + else: + raise ValueError(f"model_type {model_type} is not supported") + return final_model_kwargs
+ + +
+[docs] + @backoff.on_exception( + backoff.expo, + ( + APITimeoutError, + InternalServerError, + RateLimitError, + UnprocessableEntityError, + ), + max_time=5, + ) + def call(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED): + assert ( + "model" in api_kwargs + ), f"model must be specified in api_kwargs: {api_kwargs}" + if model_type == ModelType.LLM: + completion = self.sync_client.chat.completions.create(**api_kwargs) + return completion + else: + raise ValueError(f"model_type {model_type} is not supported")
+ + +
+[docs] + @backoff.on_exception( + backoff.expo, + ( + APITimeoutError, + InternalServerError, + RateLimitError, + UnprocessableEntityError, + ), + max_time=5, + ) + async def acall( + self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED + ): + if self.async_client is None: + self.init_async_client() + assert "model" in api_kwargs, "model must be specified" + if model_type == ModelType.LLM: + completion = await self.async_client.chat.completions.create(**api_kwargs) + return completion + else: + raise ValueError(f"model_type {model_type} is not supported")
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/components/model_client/openai_client.html b/_modules/components/model_client/openai_client.html new file mode 100644 index 00000000..810b1e30 --- /dev/null +++ b/_modules/components/model_client/openai_client.html @@ -0,0 +1,737 @@ + + + + + + + + + + components.model_client.openai_client — LightRAG documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for components.model_client.openai_client

+"""OpenAI ModelClient integration."""
+
+import os
+from typing import Dict, Sequence, Optional, List, Any, TypeVar, Callable
+
+import logging
+import backoff
+
+
+from lightrag.core.model_client import ModelClient
+from lightrag.core.types import ModelType, EmbedderOutput, TokenLogProb
+from lightrag.components.model_client.utils import parse_embedding_response
+
+# optional import
+from lightrag.utils.lazy_import import safe_import, OptionalPackages
+
+
+openai = safe_import(OptionalPackages.OPENAI.value[0], OptionalPackages.OPENAI.value[1])
+
+from openai import OpenAI, AsyncOpenAI
+from openai import (
+    APITimeoutError,
+    InternalServerError,
+    RateLimitError,
+    UnprocessableEntityError,
+    BadRequestError,
+)
+from openai.types import Completion, CreateEmbeddingResponse
+
+
+log = logging.getLogger(__name__)
+T = TypeVar("T")
+
+
+# completion parsing functions and you can combine them into one singple chat completion parser
+
+[docs] +def get_first_message_content(completion: Completion) -> str: + r"""When we only need the content of the first message. + It is the default parser for chat completion.""" + return completion.choices[0].message.content
+ + + +
+[docs] +def get_all_messages_content(completion: Completion) -> List[str]: + r"""When the n > 1, get all the messages content.""" + return [c.message.content for c in completion.choices]
+ + + +
+[docs] +def get_probabilities(completion: Completion) -> List[List[TokenLogProb]]: + r"""Get the probabilities of each token in the completion.""" + log_probs = [] + for c in completion.choices: + content = c.logprobs.content + print(content) + log_probs_for_choice = [] + for openai_token_logprob in content: + token = openai_token_logprob.token + logprob = openai_token_logprob.logprob + log_probs_for_choice.append(TokenLogProb(token=token, logprob=logprob)) + log_probs.append(log_probs_for_choice) + return log_probs
+ + + +
+[docs] +class OpenAIClient(ModelClient): + __doc__ = r"""A component wrapper for the OpenAI API client. + + Support both embedding and chat completion API. + + Users (1) simplify use ``Embedder`` and ``Generator`` components by passing OpenAIClient() as the model_client. + (2) can use this as an example to create their own API client or extend this class(copying and modifing the code) in their own project. + + Note: + We suggest users not to use `response_format` to enforce output data type or `tools` and `tool_choice` in your model_kwargs when calling the API. + We do not know how OpenAI is doing the formating or what prompt they have added. + Instead + - use :ref:`OutputParser<components-output_parsers>` for response parsing and formating. + + Args: + api_key (Optional[str], optional): OpenAI API key. Defaults to None. + chat_completion_parser (Callable[[Completion], Any], optional): A function to parse the chat completion to a str. Defaults to None. + Default is `get_first_message_content`. + + References: + - Embeddings models: https://platform.openai.com/docs/guides/embeddings + - Chat models: https://platform.openai.com/docs/guides/text-generation + - OpenAI docs: https://platform.openai.com/docs/introduction + """ + + def __init__( + self, + api_key: Optional[str] = None, + chat_completion_parser: Callable[[Completion], Any] = None, + ): + r"""It is recommended to set the OPENAI_API_KEY environment variable instead of passing it as an argument. + + Args: + api_key (Optional[str], optional): OpenAI API key. Defaults to None. + """ + super().__init__() + self._api_key = api_key + self.sync_client = self.init_sync_client() + self.async_client = None # only initialize if the async call is called + self.chat_completion_parser = ( + chat_completion_parser or get_first_message_content + ) + +
+[docs] + def init_sync_client(self): + api_key = self._api_key or os.getenv("OPENAI_API_KEY") + if not api_key: + raise ValueError("Environment variable OPENAI_API_KEY must be set") + return OpenAI(api_key=api_key)
+ + +
+[docs] + def init_async_client(self): + api_key = self._api_key or os.getenv("OPENAI_API_KEY") + if not api_key: + raise ValueError("Environment variable OPENAI_API_KEY must be set") + return AsyncOpenAI(api_key=api_key)
+ + +
+[docs] + def parse_chat_completion(self, completion: Completion) -> Any: + """Parse the completion to a str.""" + log.debug(f"completion: {completion}") + return self.chat_completion_parser(completion)
+ + +
+[docs] + def parse_embedding_response( + self, response: CreateEmbeddingResponse + ) -> EmbedderOutput: + r"""Parse the embedding response to a structure LightRAG components can understand. + + Should be called in ``Embedder``. + """ + try: + return parse_embedding_response(response) + except Exception as e: + log.error(f"Error parsing the embedding response: {e}") + return EmbedderOutput(data=[], error=str(e), raw_response=response)
+ + +
+[docs] + def convert_inputs_to_api_kwargs( + self, + input: Optional[Any] = None, + model_kwargs: Dict = {}, + model_type: ModelType = ModelType.UNDEFINED, + ) -> Dict: + r""" + Specify the API input type and output api_kwargs that will be used in _call and _acall methods. + Convert the Component's standard input, and system_input(chat model) and model_kwargs into API-specific format + """ + final_model_kwargs = model_kwargs.copy() + if model_type == ModelType.EMBEDDER: + if isinstance(input, str): + input = [input] + # convert input to input + if not isinstance(input, Sequence): + raise TypeError("input must be a sequence of text") + final_model_kwargs["input"] = input + elif model_type == ModelType.LLM: + # convert input to messages + messages: List[Dict[str, str]] = [] + if input is not None and input != "": + messages.append({"role": "system", "content": input}) + final_model_kwargs["messages"] = messages + else: + raise ValueError(f"model_type {model_type} is not supported") + return final_model_kwargs
+ + +
+[docs] + @backoff.on_exception( + backoff.expo, + ( + APITimeoutError, + InternalServerError, + RateLimitError, + UnprocessableEntityError, + BadRequestError, + ), + max_time=5, + ) + def call(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED): + """ + kwargs is the combined input and model_kwargs + """ + log.info(f"api_kwargs: {api_kwargs}") + if model_type == ModelType.EMBEDDER: + return self.sync_client.embeddings.create(**api_kwargs) + elif model_type == ModelType.LLM: + return self.sync_client.chat.completions.create(**api_kwargs) + else: + raise ValueError(f"model_type {model_type} is not supported")
+ + +
+[docs] + @backoff.on_exception( + backoff.expo, + ( + APITimeoutError, + InternalServerError, + RateLimitError, + UnprocessableEntityError, + BadRequestError, + ), + max_time=5, + ) + async def acall( + self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED + ): + """ + kwargs is the combined input and model_kwargs + """ + if self.async_client is None: + self.async_client = self.init_async_client() + if model_type == ModelType.EMBEDDER: + return await self.async_client.embeddings.create(**api_kwargs) + elif model_type == ModelType.LLM: + return await self.async_client.chat.completions.create(**api_kwargs) + else: + raise ValueError(f"model_type {model_type} is not supported")
+ + +
+[docs] + @classmethod + def from_dict(cls: type[T], data: Dict[str, Any]) -> T: + obj = super().from_dict(data) + # recreate the existing clients + obj.sync_client = obj.init_sync_client() + obj.async_client = obj.init_async_client() + return obj
+ + +
+[docs] + def to_dict(self) -> Dict[str, Any]: + r"""Convert the component to a dictionary.""" + # TODO: not exclude but save yes or no for recreating the clients + exclude = [ + "sync_client", + "async_client", + ] # unserializable object + output = super().to_dict(exclude=exclude) + return output
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/components/model_client/utils.html b/_modules/components/model_client/utils.html index b688cbc0..39272ee1 100644 --- a/_modules/components/model_client/utils.html +++ b/_modules/components/model_client/utils.html @@ -404,7 +404,6 @@

Source code for components.model_client.utils

 "Helpers for model client for integrating models and parsing the output."
-
 from lightrag.core.types import EmbedderOutput, Embedding, Usage
 
 
diff --git a/_modules/components/retriever/faiss_retriever.html b/_modules/components/retriever/faiss_retriever.html
new file mode 100644
index 00000000..c9470a17
--- /dev/null
+++ b/_modules/components/retriever/faiss_retriever.html
@@ -0,0 +1,831 @@
+
+
+
+
+
+
+  
+    
+    
+    components.retriever.faiss_retriever — LightRAG  documentation
+  
+  
+  
+  
+  
+  
+  
+
+
+
+  
+  
+  
+
+
+
+    
+    
+    
+    
+    
+  
+  
+  
+
+  
+
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+  
+  
+  
+  
+  
+  
+
+  
+  
+  
+  
+  
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for components.retriever.faiss_retriever

+"""Semantic search/embedding-based retriever using FAISS."""
+
+from typing import (
+    List,
+    Optional,
+    Sequence,
+    Union,
+    Dict,
+    overload,
+    Literal,
+    Any,
+    Callable,
+)
+import numpy as np
+import logging
+import os
+
+import faiss
+
+
+from lightrag.core.retriever import Retriever
+from lightrag.core.embedder import Embedder
+from lightrag.core.types import (
+    RetrieverOutput,
+    RetrieverOutputType,
+    RetrieverStrQueryType,
+    EmbedderOutputType,
+)
+from lightrag.core.functional import normalize_np_array, is_normalized
+
+from lightrag.utils.lazy_import import safe_import, OptionalPackages
+
+safe_import(OptionalPackages.FAISS.value[0], OptionalPackages.FAISS.value[1])
+
+log = logging.getLogger(__name__)
+
+FAISSRetrieverDocumentEmbeddingType = Union[List[float], np.ndarray]  # single embedding
+FAISSRetrieverDocumentsType = Sequence[FAISSRetrieverDocumentEmbeddingType]
+
+FAISSRetrieverEmbeddingQueryType = Union[
+    List[float], List[List[float]], np.ndarray
+]  # single embedding or list of embeddings
+FAISSRetrieverQueryType = Union[RetrieverStrQueryType, FAISSRetrieverEmbeddingQueryType]
+FAISSRetrieverQueriesType = Sequence[FAISSRetrieverQueryType]
+FAISSRetrieverQueriesStrType = Sequence[RetrieverStrQueryType]
+FAISSRetrieverQueriesEmbeddingType = Sequence[FAISSRetrieverEmbeddingQueryType]
+
+os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
+
+
+
+[docs] +class FAISSRetriever( + Retriever[FAISSRetrieverDocumentEmbeddingType, FAISSRetrieverQueryType] +): + __doc__ = r"""Semantic search/embedding-based retriever using FAISS. + + To use the retriever, you can either pass the index embeddings from the :meth:`__init__` or use the :meth:`build_index_from_documents` method. + + + Args: + embedder (Embedder, optimal): The embedder component to use for converting the queries in string format to embeddings. + Ensure the vectorizer is exactly the same as the one used to the embeddings in the index. + top_k (int, optional): Number of chunks to retrieve. Defaults to 5. + dimensions (Optional[int], optional): Dimension of the embeddings. Defaults to None. It can automatically infer the dimensions from the first chunk. + documents (Optional[FAISSRetrieverDocumentType], optional): List of embeddings. Format can be List[List[float]] or List[np.ndarray]. Defaults to None. + metric (Literal["cosine", "euclidean", "prob"], optional): The metric to use for the retrieval. Defaults to "prob" which converts cosine similarity to probability. + + How FAISS works: + + The retriever uses in-memory Faiss index to retrieve the top k chunks + d: dimension of the vectors + xb: number of vectors to put in the index + xq: number of queries + The data type dtype must be float32. + + Note: When the num of chunks are less than top_k, the last columns will be -1 + + Other index options: + - faiss.IndexFlatL2: L2 or Euclidean distance, [-inf, inf] + - faiss.IndexFlatIP: Inner product of embeddings (inner product of normalized vectors will be cosine similarity, [-1, 1]) + + We choose cosine similarity and convert it to range [0, 1] by adding 1 and dividing by 2 to simulate probability in [0, 1] + + References: + - FAISS: https://github.com/facebookresearch/faiss + """ + + def __init__( + self, + embedder: Optional[Embedder] = None, + top_k: int = 5, + dimensions: Optional[int] = None, + documents: Optional[Any] = None, + document_map_func: Optional[ + Callable[[Any], FAISSRetrieverDocumentEmbeddingType] + ] = None, + metric: Literal["cosine", "euclidean", "prob"] = "prob", + ): + super().__init__() + + self.reset_index() + + self.dimensions = dimensions + self.embedder = embedder # used to vectorize the queries + self.top_k = top_k + self.metric = metric + if self.metric == "cosine" or self.metric == "prob": + self._faiss_index_type = faiss.IndexFlatIP + self._needs_normalized_embeddings = True + elif self.metric == "euclidean": + self._faiss_index_type = faiss.IndexFlatL2 + self._needs_normalized_embeddings = False + else: + raise ValueError(f"Invalid metric: {self.metric}") + + if documents: + self.documents = documents + self.build_index_from_documents(documents, document_map_func) + +
+[docs] + def reset_index(self): + self.index = None + self.total_documents: int = 0 + self.documents: Sequence[Any] = None + self.xb: np.ndarray = None + self.dimensions: Optional[int] = None + self.indexed: bool = False
+ + + def _preprare_faiss_index_from_np_array(self, xb: np.ndarray): + r"""Prepare the faiss index from the numpy array.""" + if not self.dimensions: + self.dimensions = self.xb.shape[1] + else: + assert ( + self.dimensions == self.xb.shape[1] + ), f"Dimension mismatch: {self.dimensions} != {self.xb.shape[1]}" + self.total_documents = xb.shape[0] + + self.index = self._faiss_index_type(self.dimensions) + self.index.add(xb) + self.indexed = True + +
+[docs] + def build_index_from_documents( + self, + documents: Sequence[Any], + document_map_func: Optional[ + Callable[[Any], FAISSRetrieverDocumentEmbeddingType] + ] = None, + ): + r"""Build index from embeddings. + + Args: + documents: List of embeddings. Format can be List[List[float]] or List[np.ndarray] + + If you are using Document format, pass them as [doc.vector for doc in documents] + """ + if document_map_func: + assert callable(document_map_func), "document_map_func should be callable" + documents = [document_map_func(doc) for doc in documents] + try: + self.documents = documents + + # convert to numpy array + if not isinstance(documents, np.ndarray) and isinstance( + documents[0], Sequence + ): + # ensure all the embeddings are of the same size + assert all( + len(doc) == len(documents[0]) for doc in documents + ), "All embeddings should be of the same size" + self.xb = np.array(documents, dtype=np.float32) + else: + self.xb = documents + if self._needs_normalized_embeddings: + first_vector = self.xb[0] + if not is_normalized(first_vector): + log.warning( + "Embeddings are not normalized, normalizing the embeddings" + ) + self.xb = normalize_np_array(self.xb) + + self._preprare_faiss_index_from_np_array(self.xb) + log.info(f"Index built with {self.total_documents} chunks") + except Exception as e: + log.error(f"Error building index: {e}, resetting the index") + # reset the index + self.reset_index() + raise e
+ + + def _convert_cosine_similarity_to_probability(self, D: np.ndarray) -> np.ndarray: + D = (D + 1) / 2 + D = np.round(D, 3) + return D + + def _to_retriever_output( + self, Ind: np.ndarray, D: np.ndarray + ) -> RetrieverOutputType: + r"""Convert the indices and distances to RetrieverOutputType format.""" + output: RetrieverOutputType = [] + # Step 1: Filter out the -1, -1 columns along with its scores when top_k > len(chunks) + if -1 in Ind: + valid_columns = ~np.any(Ind == -1, axis=0) + + D = D[:, valid_columns] + Ind = Ind[:, valid_columns] + # Step 2: processing rows (one query at a time) + for row in zip(Ind, D): + indices, distances = row + # convert from numpy to list + retrieved_documents_indices = indices.tolist() + retrieved_documents_scores = distances.tolist() + output.append( + RetrieverOutput( + doc_indices=retrieved_documents_indices, + doc_scores=retrieved_documents_scores, + ) + ) + + return output + +
+[docs] + def retrieve_embedding_queries( + self, + input: FAISSRetrieverQueriesEmbeddingType, + top_k: Optional[int] = None, + ) -> RetrieverOutputType: + if not self.indexed or self.index.ntotal == 0: + raise ValueError( + "Index is empty. Please set the chunks to build the index from" + ) + # check if the input is List, convert to numpy array + try: + if not isinstance(input, np.ndarray): + xq = np.array(input, dtype=np.float32) + else: + xq = input + except Exception as e: + log.error(f"Error converting input to numpy array: {e}") + raise e + + D, Ind = self.index.search(xq, top_k if top_k else self.top_k) + if self.metric == "prob": + D = self._convert_cosine_similarity_to_probability(D) + output: RetrieverOutputType = self._to_retriever_output(Ind, D) + return output
+ + +
+[docs] + def retrieve_string_queries( + self, + input: Union[str, List[str]], + top_k: Optional[int] = None, + ) -> RetrieverOutputType: + r"""Retrieve the top k chunks given the query or queries in string format. + + Args: + input: The query or list of queries in string format. Note: ensure the maximum number of queries fits into the embedder. + top_k: The number of chunks to retrieve. When top_k is not provided, it will use the default top_k set during initialization. + + When top_k is not provided, it will use the default top_k set during initialization. + """ + if not self.indexed or self.index.ntotal == 0: + raise ValueError( + "Index is empty. Please set the chunks to build the index from" + ) + queries = [input] if isinstance(input, str) else input + # filter out empty queries + valid_queries: List[str] = [] + record_map: Dict[int, int] = ( + {} + ) # final index : the position in the initial queries + for i, q in enumerate(queries): + if not q: + log.warning("Empty query found, skipping") + continue + valid_queries.append(q) + record_map[len(valid_queries) - 1] = i + # embed the queries, assume the length fits into a batch. + try: + embeddings: EmbedderOutputType = self.embedder(valid_queries) + queries_embeddings: List[List[float]] = [ + data.embedding for data in embeddings.data + ] + + except Exception as e: + log.error(f"Error embedding queries: {e}") + raise e + xq = np.array(queries_embeddings, dtype=np.float32) + D, Ind = self.index.search(xq, top_k if top_k else self.top_k) + D = self._convert_cosine_similarity_to_probability(D) + + output: RetrieverOutputType = [ + RetrieverOutput(doc_indices=[], query=query) for query in queries + ] + retrieved_output: RetrieverOutputType = self._to_retriever_output(Ind, D) + + # fill in the doc_indices and score for valid queries + for i, per_query_output in enumerate(retrieved_output): + initial_index = record_map[i] + output[initial_index].doc_indices = per_query_output.doc_indices + output[initial_index].doc_scores = per_query_output.doc_scores + + return output
+ + + @overload + def call( + self, + input: FAISSRetrieverQueriesEmbeddingType, + top_k: Optional[int] = None, + ) -> RetrieverOutputType: + r"""Retrieve the top k chunks given the query or queries in embedding format.""" + ... + + @overload + def call( + self, + input: FAISSRetrieverQueriesStrType, + top_k: Optional[int] = None, + ) -> RetrieverOutputType: + r"""Retrieve the top k chunks given the query or queries in string format.""" + ... + +
+[docs] + def call( + self, + input: FAISSRetrieverQueriesType, + top_k: Optional[int] = None, + ) -> RetrieverOutputType: + r"""Retrieve the top k chunks given the query or queries in embedding or string format.""" + assert ( + self.indexed + ), "Index is not built. Please build the index using build_index_from_documents" + if isinstance(input, str) or ( + isinstance(input, Sequence) and isinstance(input[0], str) + ): + assert self.embedder, "Embedder is not provided" + return self.retrieve_string_queries(input, top_k) + else: + return self.retrieve_embedding_queries(input, top_k)
+ + + def _extra_repr(self) -> str: + s = f"top_k={self.top_k}" + if self.metric: + s += f", metric={self.metric}" + if self.dimensions: + s += f", dimensions={self.dimensions}" + if self.documents: + s += f", total_documents={self.total_documents}" + return s
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/components/retriever/postgres_retriever.html b/_modules/components/retriever/postgres_retriever.html new file mode 100644 index 00000000..3a102901 --- /dev/null +++ b/_modules/components/retriever/postgres_retriever.html @@ -0,0 +1,728 @@ + + + + + + + + + + components.retriever.postgres_retriever — LightRAG documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for components.retriever.postgres_retriever

+"""Leverage a postgres database to store and retrieve documents."""
+
+from typing import List, Optional, Any
+from enum import Enum
+import numpy as np
+import logging
+
+from lightrag.core.retriever import (
+    Retriever,
+)
+from lightrag.core.embedder import Embedder
+
+from lightrag.core.types import (
+    RetrieverOutput,
+    RetrieverStrQueryType,
+    RetrieverStrQueriesType,
+    Document,
+)
+from lightrag.database.sqlalchemy.sqlachemy_manager import DatabaseManager
+
+log = logging.getLogger(__name__)
+
+
+
+[docs] +class DistanceToOperator(Enum): + __doc__ = r"""Enum for the distance to operator. + + About pgvector: + + 1. L2 distance: <->, inner product (<#>), cosine distance (<=>), and L1 distance (<+>, added in 0.7.0) + """ + L2 = "<->" + INNER_PRODUCT = ( + "<#>" # cosine similarity when the vector is normalized, in range [-1, 1] + ) + COSINE = "<=>" # cosine distance, in range [0, 1] = 1 - cosine_similarity + L1 = "<+>"
+ + + +
+[docs] +class PostgresRetriever(Retriever[Any, RetrieverStrQueryType]): + __doc__ = r"""Use a postgres database to store and retrieve documents. + + Users can follow this example and to customize the prompt or additionally ask it to output score along with the indices. + + Args: + top_k (Optional[int], optional): top k documents to fetch. Defaults to 1. + database_url (str): the database url to connect to. Defaults to postgresql://postgres:password@localhost:5432/vector_db. + + References: + [1] pgvector extension: https://github.com/pgvector/pgvector + """ + + def __init__( + self, + embedder: Embedder, + top_k: Optional[int] = 1, + database_url: str = None, + table_name: str = "document", + distance_operator: DistanceToOperator = DistanceToOperator.INNER_PRODUCT, + ): + super().__init__() + self.top_k = top_k + self.table_name = table_name + db_name = "vector_db" + self.database_url = ( + database_url or f"postgresql://postgres:password@localhost:5432/{db_name}" + ) + self.db_manager = DatabaseManager(self.database_url) + self.embedder = embedder + self.distance_operator = distance_operator + self.db_score_prob_fun_map = { + DistanceToOperator.COSINE: self._convert_cosine_distance_to_probability, + DistanceToOperator.L2: self._convert_l2_distance_to_probability, + DistanceToOperator.INNER_PRODUCT: self._convert_cosine_similarity_to_probability, + } + self.score_prob_fun = ( + self.db_score_prob_fun_map[self.distance_operator] + if self.distance_operator in self.db_score_prob_fun_map + else None + ) + +
+[docs] + @classmethod + def format_vector_search_query( + cls, + table_name: str, + vector_column: str, + query_embedding: List[float], + top_k: int, + distance_operator: DistanceToOperator, + sort_desc: bool = True, + ) -> str: + """ + Formats a SQL query string to select all columns from a table, order the results + by the distance or similarity score to a provided embedding, and also return + that score. + + Args: + table_name (str): The name of the table to query. + column (str): The name of the column containing the vector data. + query_embedding (list or str): The embedding vector to compare against. + top_k (int): The number of top results to return. + + Returns: + str: A formatted SQL query string that includes the score. + """ + + # Convert the list embedding to a string format suitable for SQL + if isinstance(query_embedding, list): + embedding_str = str(query_embedding).replace( + " ", "" + ) # Remove spaces for cleaner SQL + else: + embedding_str = query_embedding + + # Determine sorting order + order_by = "DESC" if sort_desc else "ASC" + + # SQL query that includes the score in the selected columns + sql_query = f""" + SELECT *, ({vector_column} {distance_operator.value} '{embedding_str}') AS score + FROM {table_name} + ORDER BY score {order_by} + LIMIT {top_k}; + """ + return sql_query
+ + +
+[docs] + def retrieve_by_sql(self, query: str) -> List[str]: + """Retrieve documents from the postgres database.""" + + results = self.db_manager.execute_query(query) + print(results) + return results
+ + + def _convert_cosine_similarity_to_probability( + self, cosine_similarity: List[float] + ) -> List[float]: + """Convert cosine similarity to probability.""" + return [(1 + cosine_similarity) / 2 for cosine_similarity in cosine_similarity] + + def _convert_l2_distance_to_probability( + self, l2_distance: List[float] + ) -> List[float]: + """Convert L2 distance to probability. + + note: + + Ensure the vector is normalized so that the l2_distance will be in range [0, 2] + """ + distance = np.array(l2_distance) + # clip to ensure the distance is in range [0, 2] + distance = np.clip(distance, 0, 2) + # convert to probability + prob_score = 1 - distance / 2 + return prob_score.tolist() + + def _convert_cosine_distance_to_probability( + self, cosine_distance: List[float] + ) -> List[float]: + """Convert cosine distance to probability.""" + return [(1 - cosine_distance) for cosine_distance in cosine_distance] + +
+[docs] + def call( + self, + input: RetrieverStrQueriesType, + top_k: Optional[int] = None, + **kwargs, + ) -> List[RetrieverOutput]: + top_k = top_k or self.top_k + queries: List[str] = input if isinstance(input, list) else [input] + retrieved_outputs: List[RetrieverOutput] = [] + queries_embeddings = self.embedder(queries) + + sort_desc = False + if ( + self.distance_operator == DistanceToOperator.INNER_PRODUCT + ): # cosine similarity + sort_desc = True + + for idx, query in enumerate(queries): + query_embedding = queries_embeddings.data[idx].embedding + query_str = self.format_vector_search_query( + table_name=self.table_name, + vector_column="vector", + query_embedding=query_embedding, + top_k=top_k, + distance_operator=self.distance_operator, + sort_desc=sort_desc, + ) + retrieved_documents = self.retrieve_by_sql(query_str) + doc_indices = [doc["id"] for doc in retrieved_documents] + doc_scores = [doc["score"] for doc in retrieved_documents] + doc_scores_prob = ( + self.score_prob_fun(doc_scores) if self.score_prob_fun else None + ) + documents: List[Document] = [] + for doc in retrieved_documents: + documents.append(Document.from_dict(doc)) + retrieved_outputs.append( + RetrieverOutput( + doc_indices=doc_indices, + doc_scores=doc_scores_prob if doc_scores_prob else doc_scores, + query=query, + documents=documents, + ) + ) + return retrieved_outputs
+
+ + + +# if __name__ == "__main__": +# from lightrag.core.embedder import Embedder +# from lightrag.core.types import Document +# from lightrag.database.sqlalchemy.pipeline.default_config import default_config +# from lightrag.utils import setup_env # noqa + +# documents = [ +# { +# "meta_data": {"title": "Li Yin's profile"}, +# "text": "My name is Li Yin, I love rock climbing" +# + "lots of nonsense text" * 500, +# "id": "doc1", +# }, +# { +# "meta_data": {"title": "Interviewing Li Yin"}, +# "text": "lots of more nonsense text" * 250 +# + "Li Yin is a software developer and AI researcher" +# + "lots of more nonsense text" * 250, +# "id": "doc2", +# }, +# ] +# db_name = "vector_db" +# postgres_url = f"postgresql://postgres:password@localhost:5432/{db_name}" + +# vector_config = default_config["to_embeddings"]["component_config"]["embedder"][ +# "component_config" +# ] +# eb = Embedder.from_config(vector_config) +# pr = PostgresRetriever( +# embedder=eb, +# database_url=postgres_url, +# top_k=2, +# distance_operator=DistanceToOperator.INNER_PRODUCT, +# ) +# output = pr("What did the author do?") +# print(output) +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/index.html b/_modules/index.html index 8a6d3ab8..9e68acb5 100644 --- a/_modules/index.html +++ b/_modules/index.html @@ -404,11 +404,17 @@

All modules for which code is available

  • components.data_process.data_components
  • components.data_process.text_splitter
  • components.memory.memory
  • +
  • components.model_client.anthropic_client
  • +
  • components.model_client.cohere_client
  • components.model_client.google_client
  • +
  • components.model_client.groq_client
  • +
  • components.model_client.openai_client
  • components.model_client.utils
  • components.output_parsers.outputs
  • components.retriever.bm25_retriever
  • +
  • components.retriever.faiss_retriever
  • components.retriever.llm_retriever
  • +
  • components.retriever.postgres_retriever
  • components.retriever.reranker_retriever
  • core.base_data_class
  • core.component
  • diff --git a/_modules/utils/lazy_import.html b/_modules/utils/lazy_import.html index 7245149c..6105efa3 100644 --- a/_modules/utils/lazy_import.html +++ b/_modules/utils/lazy_import.html @@ -407,6 +407,7 @@

    Source code for utils.lazy_import

     
     import importlib
     import logging
    +from types import ModuleType
     
     
     from enum import Enum
    @@ -475,7 +476,16 @@ 

    Source code for utils.lazy_import

             optional_package (OptionalPackages): The optional package to import, it helps define the package name and error message.
         """
     
    -    def __init__(self, import_path: str, optional_package: OptionalPackages):
    +    def __init__(
    +        self, import_path: str, optional_package: OptionalPackages, *args, **kwargs
    +    ):
    +        if args or kwargs:
    +            raise TypeError(
    +                "LazyImport does not support subclassing or additional arguments. "
    +                "Import the class directly from its specific module instead. For example, "
    +                "from lightrag.components.model_client.cohere_client import CohereAPIClient"
    +                "instead of using the lazy import with: from lightrag.components.model_client import CohereAPIClient"
    +            )
             self.import_path = import_path
             self.optional_package = optional_package
             self.module = None
    @@ -516,10 +526,34 @@ 

    Source code for utils.lazy_import

     
     
    [docs] -def safe_import(module_name, install_message): +def safe_import(module_name: str, install_message: str) -> ModuleType: """Safely import a module and raise an ImportError with the install message if the module is not found. Mainly used internally to import optional packages only when needed. + + Example: + + 1. Tests + + .. code-block:: python + + try: + numpy = safe_import("numpy", "Please install numpy with: pip install numpy") + print(numpy.__version__) + except ImportError as e: + print(e) + + When numpy is not installed, it will raise an ImportError with the install message. + When numpy is installed, it will print the numpy version. + + 2. Use it to delay the import of optional packages in the library. + + .. code-block:: python + + from lightrag.utils.lazy_import import safe_import, OptionalPackages + + numpy = safe_import(OptionalPackages.NUMPY.value[0], OptionalPackages.NUMPY.value[1]) + """ try: return importlib.import_module(module_name) diff --git a/_modules/utils/setup_env.html b/_modules/utils/setup_env.html index 65b46ec9..38244eea 100644 --- a/_modules/utils/setup_env.html +++ b/_modules/utils/setup_env.html @@ -404,6 +404,10 @@

    Source code for utils.setup_env

     import dotenv
    +import os
    +import logging
    +
    +log = logging.getLogger(__name__)
     
     
     
    @@ -411,7 +415,14 @@

    Source code for utils.setup_env

     def setup_env(dotenv_path: str = ".env"):
         """Load environment variables from .env file."""
     
    -    dotenv.load_dotenv(dotenv_path=dotenv_path)
    + if not os.path.exists(dotenv_path): + raise FileNotFoundError(f"File not found: {dotenv_path}") + + try: + dotenv.load_dotenv(dotenv_path=dotenv_path, verbose=True, override=False) + except Exception as e: + log.error(f"Error loading .env file: {e}") + raise e
    diff --git a/_sources/apis/components/components.model_client.anthropic_client.rst.txt b/_sources/apis/components/components.model_client.anthropic_client.rst.txt index f3af8985..bd2f49f8 100644 --- a/_sources/apis/components/components.model_client.anthropic_client.rst.txt +++ b/_sources/apis/components/components.model_client.anthropic_client.rst.txt @@ -7,3 +7,17 @@ anthropic_client :members: :undoc-members: :show-inheritance: + + + .. rubric:: Functions + + .. autosummary:: + + get_first_message_content + + .. rubric:: Classes + + .. autosummary:: + + AnthropicAPIClient + diff --git a/_sources/apis/components/components.model_client.cohere_client.rst.txt b/_sources/apis/components/components.model_client.cohere_client.rst.txt index 762def3a..be9569be 100644 --- a/_sources/apis/components/components.model_client.cohere_client.rst.txt +++ b/_sources/apis/components/components.model_client.cohere_client.rst.txt @@ -7,3 +7,11 @@ cohere_client :members: :undoc-members: :show-inheritance: + + + .. rubric:: Classes + + .. autosummary:: + + CohereAPIClient + diff --git a/_sources/apis/components/components.model_client.groq_client.rst.txt b/_sources/apis/components/components.model_client.groq_client.rst.txt index f7e0925a..6dd213a2 100644 --- a/_sources/apis/components/components.model_client.groq_client.rst.txt +++ b/_sources/apis/components/components.model_client.groq_client.rst.txt @@ -7,3 +7,11 @@ groq_client :members: :undoc-members: :show-inheritance: + + + .. rubric:: Classes + + .. autosummary:: + + GroqAPIClient + diff --git a/_sources/apis/components/components.model_client.openai_client.rst.txt b/_sources/apis/components/components.model_client.openai_client.rst.txt index 1c00b378..4af3f5ae 100644 --- a/_sources/apis/components/components.model_client.openai_client.rst.txt +++ b/_sources/apis/components/components.model_client.openai_client.rst.txt @@ -7,3 +7,19 @@ openai_client :members: :undoc-members: :show-inheritance: + + + .. rubric:: Functions + + .. autosummary:: + + get_all_messages_content + get_first_message_content + get_probabilities + + .. rubric:: Classes + + .. autosummary:: + + OpenAIClient + diff --git a/_sources/apis/components/components.retriever.faiss_retriever.rst.txt b/_sources/apis/components/components.retriever.faiss_retriever.rst.txt index a9ed294b..cd22e668 100644 --- a/_sources/apis/components/components.retriever.faiss_retriever.rst.txt +++ b/_sources/apis/components/components.retriever.faiss_retriever.rst.txt @@ -7,3 +7,11 @@ faiss_retriever :members: :undoc-members: :show-inheritance: + + + .. rubric:: Classes + + .. autosummary:: + + FAISSRetriever + diff --git a/_sources/apis/components/components.retriever.postgres_retriever.rst.txt b/_sources/apis/components/components.retriever.postgres_retriever.rst.txt index cdbb70d0..8aa1d9eb 100644 --- a/_sources/apis/components/components.retriever.postgres_retriever.rst.txt +++ b/_sources/apis/components/components.retriever.postgres_retriever.rst.txt @@ -7,3 +7,12 @@ postgres_retriever :members: :undoc-members: :show-inheritance: + + + .. rubric:: Classes + + .. autosummary:: + + DistanceToOperator + PostgresRetriever + diff --git a/_sources/developer_notes/lightrag_design_philosophy.rst.txt b/_sources/developer_notes/lightrag_design_philosophy.rst.txt index f2fb932c..e7242a0d 100644 --- a/_sources/developer_notes/lightrag_design_philosophy.rst.txt +++ b/_sources/developer_notes/lightrag_design_philosophy.rst.txt @@ -6,11 +6,11 @@ Right from the begining, `LightRAG` follows three fundamental principles. Principle 1: Simplicity over Complexity ----------------------------------------------------------------------- -We put these three hard rules while designing LightRAG: + We put these three hard rules while designing LightRAG: - Every layer of abstraction needs to be adjusted and overall we do not allow more than 3 layers of abstraction. - We minimize the lines of code instead of maximizing the lines of code. -- Go *deep* and *wide* in order to *simplify*. The clarity we achieve is not the result of being easy, but the result of being deep. +- Go *deep* and *wide* in order to *simplify*. The clarity we achieve is not the result of being easy. diff --git a/_sources/get_started/lightrag_in_10_mins.rst.txt b/_sources/get_started/lightrag_in_10_mins.rst.txt index 1bdec107..d046c77f 100644 --- a/_sources/get_started/lightrag_in_10_mins.rst.txt +++ b/_sources/get_started/lightrag_in_10_mins.rst.txt @@ -1,13 +1,15 @@ LightRAG in 10 minutes ============================= -[Li] +Coming soon... -Will use end to end trec classifier as an example to demonstrate: +We will showcase a use case end-to-end, including task pipeline, configuration, logging and tracing, evaluation, and optimization. -1. Look at the data and task, create `data class`, `prompt`, and `task`, and set up `log` and tracing. -2. Create datasets with `train`, `eval`, and `test` splits. -3. Eval zero-shot with manual prompts. +.. Will use end to end trec classifier as an example to demonstrate: +.. 1. Look at the data and task, create `data class`, `prompt`, and `task`, and set up `log` and tracing. +.. 2. Create datasets with `train`, `eval`, and `test` splits. +.. 3. Eval zero-shot with manual prompts. -The content will be from `/use_cases/classification/readme.md`. \ No newline at end of file + +.. The content will be from `/use_cases/classification/readme.md`. diff --git a/_sources/index.rst.txt b/_sources/index.rst.txt index 64cb9026..58f21f96 100644 --- a/_sources/index.rst.txt +++ b/_sources/index.rst.txt @@ -26,7 +26,7 @@

    - LightRAG helps developers with both building and optimizing Retriever-Agent-Generator (RAG) pipelines.
    + LightRAG helps developers with both building and optimizing Retriever-Agent-Generator pipelines.
    It is light, modular, and robust, with a 100% readable codebase.

    diff --git a/apis/components/components.model_client.anthropic_client.html b/apis/components/components.model_client.anthropic_client.html index 6939fe64..8526c637 100644 --- a/apis/components/components.model_client.anthropic_client.html +++ b/apis/components/components.model_client.anthropic_client.html @@ -252,6 +252,10 @@ + +
    @@ -514,8 +518,78 @@
    -
    -

    anthropic_client#

    +
    +

    anthropic_client#

    +

    Anthropic ModelClient integration.

    +

    Functions

    +
    + + + + + +

    get_first_message_content(completion)

    When we only need the content of the first message.

    +
    +

    Classes

    +
    + + + + + +

    AnthropicAPIClient([api_key, ...])

    A component wrapper for the Anthropic API client.

    +
    +
    +
    +class AnthropicAPIClient(api_key: str | None = None, chat_completion_parser: Callable[[Message], Any] = None)[source]#
    +

    Bases: ModelClient

    +

    A component wrapper for the Anthropic API client.

    +

    Visit https://docs.anthropic.com/en/docs/intro-to-claude for more api details.

    +
    +
    +init_sync_client()[source]#
    +
    + +
    +
    +init_async_client()[source]#
    +
    + +
    +
    +parse_chat_completion(completion: Message) str[source]#
    +

    Parse the chat completion to str.

    +
    + +
    +
    +convert_inputs_to_api_kwargs(input: Any | None = None, model_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED) dict[source]#
    +

    Anthropic API messages separates the system and the user messages.

    +

    As we focus on one prompt, we have to use the user message as the input.

    +

    api: https://docs.anthropic.com/en/api/messages

    +
    + +
    +
    +call(api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED)[source]#
    +

    kwargs is the combined input and model_kwargs

    +
    + +
    +
    +async acall(api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED)[source]#
    +

    kwargs is the combined input and model_kwargs

    +
    + +
    + +
    +
    +get_first_message_content(completion: Message) str[source]#
    +

    When we only need the content of the first message. +It is the default parser for chat completion.

    +
    +
    @@ -553,6 +627,32 @@ + +
    diff --git a/apis/components/components.model_client.cohere_client.html b/apis/components/components.model_client.cohere_client.html index 6dcd7510..fe034a77 100644 --- a/apis/components/components.model_client.cohere_client.html +++ b/apis/components/components.model_client.cohere_client.html @@ -252,6 +252,10 @@ + +
    @@ -514,8 +518,74 @@
    -
    -

    cohere_client#

    +
    +

    cohere_client#

    +

    Cohere ModelClient integration.

    +

    Classes

    +
    + + + + + +

    CohereAPIClient([api_key])

    A component wrapper for the Cohere API.

    +
    +
    +
    +class CohereAPIClient(api_key: str | None = None)[source]#
    +

    Bases: ModelClient

    +

    A component wrapper for the Cohere API.

    +

    Visit https://docs.cohere.com/ for more api details.

    +

    References: +- Cohere reranker: https://docs.cohere.com/reference/rerank

    +

    Tested Cohere models: 6/16/2024 +- rerank-english-v3.0, rerank-multilingual-v3.0, rerank-english-v2.0, rerank-multilingual-v2.0

    +
    +

    Note

    +

    For all ModelClient integration, such as CohereAPIClient, if you want to subclass CohereAPIClient, you need to import it from the module directly.

    +

    from lightrag.components.model_client.cohere_client import CohereAPIClient

    +

    instead of using the lazy import with:

    +

    from lightrag.components.model_client import CohereAPIClient

    +
    +
    +
    +init_sync_client()[source]#
    +
    + +
    +
    +init_async_client()[source]#
    +
    + +
    +
    +convert_inputs_to_api_kwargs(input: Any | None = None, model_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED) Dict[source]#
    +
    +
    For rerank model, expect model_kwargs to have the following keys:

    model: str, +query: str, +documents: List[str], +top_n: int,

    +
    +
    +
    + +
    +
    +call(api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED)[source]#
    +

    Subclass use this to call the API with the sync client. +model_type: this decides which API, such as chat.completions or embeddings for OpenAI. +api_kwargs: all the arguments that the API call needs, subclass should implement this method.

    +

    Additionally in subclass you can implement the error handling and retry logic here. See OpenAIClient for example.

    +
    + +
    +
    +async acall(api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED)[source]#
    +

    Subclass use this to call the API with the async client.

    +
    + +
    +
    @@ -553,6 +623,30 @@ + +
    diff --git a/apis/components/components.model_client.groq_client.html b/apis/components/components.model_client.groq_client.html index 38cecfb0..954673ec 100644 --- a/apis/components/components.model_client.groq_client.html +++ b/apis/components/components.model_client.groq_client.html @@ -252,6 +252,10 @@ + +
    @@ -514,8 +518,78 @@
    -
    -

    groq_client#

    +
    +

    groq_client#

    +

    Groq ModelClient integration.

    +

    Classes

    +
    + + + + + +

    GroqAPIClient([api_key])

    A component wrapper for the Groq API client.

    +
    +
    +
    +class GroqAPIClient(api_key: str | None = None)[source]#
    +

    Bases: ModelClient

    +

    A component wrapper for the Groq API client.

    +

    Visit https://console.groq.com/docs/ for more api details. +Check https://console.groq.com/docs/models for the available models.

    +

    Tested Groq models: 4/22/2024 +- llama3-8b-8192 +- llama3-70b-8192 +- mixtral-8x7b-32768 +- gemma-7b-it

    +
    +
    +init_sync_client()[source]#
    +
    + +
    +
    +init_async_client()[source]#
    +
    + +
    +
    +parse_chat_completion(completion: Any) str[source]#
    +

    Parse the completion to a string output.

    +
    + +
    +
    +convert_inputs_to_api_kwargs(input: Any | None = None, model_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED) Dict[source]#
    +

    Bridge the Component’s standard input and model_kwargs into API-specific format, the api_kwargs that will be used in _call and _acall methods.

    +
    +
    Parameters:
    +
      +
    • input (Optional[Any], optional) – input to the model. Defaults to None.

    • +
    • model_kwargs (Dict) – model kwargs

    • +
    • model_type (ModelType) – model type

    • +
    +
    +
    +
    + +
    +
    +call(api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED)[source]#
    +

    Subclass use this to call the API with the sync client. +model_type: this decides which API, such as chat.completions or embeddings for OpenAI. +api_kwargs: all the arguments that the API call needs, subclass should implement this method.

    +

    Additionally in subclass you can implement the error handling and retry logic here. See OpenAIClient for example.

    +
    + +
    +
    +async acall(api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED)[source]#
    +

    Subclass use this to call the API with the async client.

    +
    + +
    +
    @@ -553,6 +627,31 @@ + +