Merge branch 'main' into pkaushal/vectordb-chroma
Browse files- README.md +1 -1
- lightrag/lightrag.py +8 -7
- lightrag/llm.py +7 -0
- lightrag/operate.py +20 -8
- lightrag/prompt.py +11 -6
- lightrag/storage.py +10 -4
- lightrag/utils.py +20 -1
README.md
CHANGED
@@ -594,7 +594,7 @@ if __name__ == "__main__":
|
|
594 |
| **llm\_model\_kwargs** | `dict` | Additional parameters for LLM generation | |
|
595 |
| **vector\_db\_storage\_cls\_kwargs** | `dict` | Additional parameters for vector database (currently not used) | |
|
596 |
| **enable\_llm\_cache** | `bool` | If `TRUE`, stores LLM results in cache; repeated prompts return cached responses | `TRUE` |
|
597 |
-
| **addon\_params** | `dict` | Additional parameters, e.g., `{"example_number": 1, "language": "Simplified Chinese"}`: sets example limit and output language | `example_number: all examples, language: English` |
|
598 |
| **convert\_response\_to\_json\_func** | `callable` | Not used | `convert_response_to_json` |
|
599 |
| **embedding\_cache\_config** | `dict` | Configuration for question-answer caching. Contains three parameters:<br>- `enabled`: Boolean value to enable/disable cache lookup functionality. When enabled, the system will check cached responses before generating new answers.<br>- `similarity_threshold`: Float value (0-1), similarity threshold. When a new question's similarity with a cached question exceeds this threshold, the cached answer will be returned directly without calling the LLM.<br>- `use_llm_check`: Boolean value to enable/disable LLM similarity verification. When enabled, LLM will be used as a secondary check to verify the similarity between questions before returning cached answers. | Default: `{"enabled": False, "similarity_threshold": 0.95, "use_llm_check": False}` |
|
600 |
|
|
|
594 |
| **llm\_model\_kwargs** | `dict` | Additional parameters for LLM generation | |
|
595 |
| **vector\_db\_storage\_cls\_kwargs** | `dict` | Additional parameters for vector database (currently not used) | |
|
596 |
| **enable\_llm\_cache** | `bool` | If `TRUE`, stores LLM results in cache; repeated prompts return cached responses | `TRUE` |
|
597 |
+
| **addon\_params** | `dict` | Additional parameters, e.g., `{"example_number": 1, "language": "Simplified Chinese", "entity_types": ["organization", "person", "geo", "event"]}`: sets example limit and output language | `example_number: all examples, language: English` |
|
598 |
| **convert\_response\_to\_json\_func** | `callable` | Not used | `convert_response_to_json` |
|
599 |
| **embedding\_cache\_config** | `dict` | Configuration for question-answer caching. Contains three parameters:<br>- `enabled`: Boolean value to enable/disable cache lookup functionality. When enabled, the system will check cached responses before generating new answers.<br>- `similarity_threshold`: Float value (0-1), similarity threshold. When a new question's similarity with a cached question exceeds this threshold, the cached answer will be returned directly without calling the LLM.<br>- `use_llm_check`: Boolean value to enable/disable LLM similarity verification. When enabled, LLM will be used as a secondary check to verify the similarity between questions before returning cached answers. | Default: `{"enabled": False, "similarity_threshold": 0.95, "use_llm_check": False}` |
|
600 |
|
lightrag/lightrag.py
CHANGED
@@ -50,16 +50,17 @@ from .storage import (
|
|
50 |
def lazy_external_import(module_name: str, class_name: str):
|
51 |
"""Lazily import a class from an external module based on the package of the caller."""
|
52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
def import_class(*args, **kwargs):
|
54 |
-
import inspect
|
55 |
import importlib
|
56 |
|
57 |
-
#
|
58 |
-
caller_frame = inspect.currentframe().f_back
|
59 |
-
module = inspect.getmodule(caller_frame)
|
60 |
-
package = module.__package__ if module else None
|
61 |
-
|
62 |
-
# Import the module using importlib with package context
|
63 |
module = importlib.import_module(module_name, package=package)
|
64 |
|
65 |
# Get the class from the module and instantiate it
|
|
|
50 |
def lazy_external_import(module_name: str, class_name: str):
|
51 |
"""Lazily import a class from an external module based on the package of the caller."""
|
52 |
|
53 |
+
# Get the caller's module and package
|
54 |
+
import inspect
|
55 |
+
|
56 |
+
caller_frame = inspect.currentframe().f_back
|
57 |
+
module = inspect.getmodule(caller_frame)
|
58 |
+
package = module.__package__ if module else None
|
59 |
+
|
60 |
def import_class(*args, **kwargs):
|
|
|
61 |
import importlib
|
62 |
|
63 |
+
# Import the module using importlib
|
|
|
|
|
|
|
|
|
|
|
64 |
module = importlib.import_module(module_name, package=package)
|
65 |
|
66 |
# Get the class from the module and instantiate it
|
lightrag/llm.py
CHANGED
@@ -30,6 +30,7 @@ from .utils import (
|
|
30 |
wrap_embedding_func_with_attrs,
|
31 |
locate_json_string_body_from_string,
|
32 |
safe_unicode_decode,
|
|
|
33 |
)
|
34 |
|
35 |
import sys
|
@@ -63,12 +64,18 @@ async def openai_complete_if_cache(
|
|
63 |
AsyncOpenAI() if base_url is None else AsyncOpenAI(base_url=base_url)
|
64 |
)
|
65 |
kwargs.pop("hashing_kv", None)
|
|
|
66 |
messages = []
|
67 |
if system_prompt:
|
68 |
messages.append({"role": "system", "content": system_prompt})
|
69 |
messages.extend(history_messages)
|
70 |
messages.append({"role": "user", "content": prompt})
|
71 |
|
|
|
|
|
|
|
|
|
|
|
72 |
if "response_format" in kwargs:
|
73 |
response = await openai_async_client.beta.chat.completions.parse(
|
74 |
model=model, messages=messages, **kwargs
|
|
|
30 |
wrap_embedding_func_with_attrs,
|
31 |
locate_json_string_body_from_string,
|
32 |
safe_unicode_decode,
|
33 |
+
logger,
|
34 |
)
|
35 |
|
36 |
import sys
|
|
|
64 |
AsyncOpenAI() if base_url is None else AsyncOpenAI(base_url=base_url)
|
65 |
)
|
66 |
kwargs.pop("hashing_kv", None)
|
67 |
+
kwargs.pop("keyword_extraction", None)
|
68 |
messages = []
|
69 |
if system_prompt:
|
70 |
messages.append({"role": "system", "content": system_prompt})
|
71 |
messages.extend(history_messages)
|
72 |
messages.append({"role": "user", "content": prompt})
|
73 |
|
74 |
+
# 添加日志输出
|
75 |
+
logger.debug("===== Query Input to LLM =====")
|
76 |
+
logger.debug(f"Query: {prompt}")
|
77 |
+
logger.debug(f"System prompt: {system_prompt}")
|
78 |
+
logger.debug("Full context:")
|
79 |
if "response_format" in kwargs:
|
80 |
response = await openai_async_client.beta.chat.completions.parse(
|
81 |
model=model, messages=messages, **kwargs
|
lightrag/operate.py
CHANGED
@@ -260,6 +260,9 @@ async def extract_entities(
|
|
260 |
language = global_config["addon_params"].get(
|
261 |
"language", PROMPTS["DEFAULT_LANGUAGE"]
|
262 |
)
|
|
|
|
|
|
|
263 |
example_number = global_config["addon_params"].get("example_number", None)
|
264 |
if example_number and example_number < len(PROMPTS["entity_extraction_examples"]):
|
265 |
examples = "\n".join(
|
@@ -272,7 +275,7 @@ async def extract_entities(
|
|
272 |
tuple_delimiter=PROMPTS["DEFAULT_TUPLE_DELIMITER"],
|
273 |
record_delimiter=PROMPTS["DEFAULT_RECORD_DELIMITER"],
|
274 |
completion_delimiter=PROMPTS["DEFAULT_COMPLETION_DELIMITER"],
|
275 |
-
entity_types=",".join(
|
276 |
language=language,
|
277 |
)
|
278 |
# add example's format
|
@@ -283,7 +286,7 @@ async def extract_entities(
|
|
283 |
tuple_delimiter=PROMPTS["DEFAULT_TUPLE_DELIMITER"],
|
284 |
record_delimiter=PROMPTS["DEFAULT_RECORD_DELIMITER"],
|
285 |
completion_delimiter=PROMPTS["DEFAULT_COMPLETION_DELIMITER"],
|
286 |
-
entity_types=",".join(
|
287 |
examples=examples,
|
288 |
language=language,
|
289 |
)
|
@@ -412,15 +415,17 @@ async def extract_entities(
|
|
412 |
):
|
413 |
all_relationships_data.append(await result)
|
414 |
|
415 |
-
if not len(all_entities_data):
|
416 |
-
logger.warning("Didn't extract any entities, maybe your LLM is not working")
|
417 |
-
return None
|
418 |
-
if not len(all_relationships_data):
|
419 |
logger.warning(
|
420 |
-
"Didn't extract any relationships, maybe your LLM is not working"
|
421 |
)
|
422 |
return None
|
423 |
|
|
|
|
|
|
|
|
|
|
|
424 |
if entity_vdb is not None:
|
425 |
data_for_vdb = {
|
426 |
compute_mdhash_id(dp["entity_name"], prefix="ent-"): {
|
@@ -630,6 +635,13 @@ async def _build_query_context(
|
|
630 |
text_chunks_db,
|
631 |
query_param,
|
632 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
633 |
if query_param.mode == "hybrid":
|
634 |
entities_context, relations_context, text_units_context = combine_contexts(
|
635 |
[hl_entities_context, ll_entities_context],
|
@@ -865,7 +877,7 @@ async def _get_edge_data(
|
|
865 |
results = await relationships_vdb.query(keywords, top_k=query_param.top_k)
|
866 |
|
867 |
if not len(results):
|
868 |
-
return
|
869 |
|
870 |
edge_datas = await asyncio.gather(
|
871 |
*[knowledge_graph_inst.get_edge(r["src_id"], r["tgt_id"]) for r in results]
|
|
|
260 |
language = global_config["addon_params"].get(
|
261 |
"language", PROMPTS["DEFAULT_LANGUAGE"]
|
262 |
)
|
263 |
+
entity_types = global_config["addon_params"].get(
|
264 |
+
"entity_types", PROMPTS["DEFAULT_ENTITY_TYPES"]
|
265 |
+
)
|
266 |
example_number = global_config["addon_params"].get("example_number", None)
|
267 |
if example_number and example_number < len(PROMPTS["entity_extraction_examples"]):
|
268 |
examples = "\n".join(
|
|
|
275 |
tuple_delimiter=PROMPTS["DEFAULT_TUPLE_DELIMITER"],
|
276 |
record_delimiter=PROMPTS["DEFAULT_RECORD_DELIMITER"],
|
277 |
completion_delimiter=PROMPTS["DEFAULT_COMPLETION_DELIMITER"],
|
278 |
+
entity_types=",".join(entity_types),
|
279 |
language=language,
|
280 |
)
|
281 |
# add example's format
|
|
|
286 |
tuple_delimiter=PROMPTS["DEFAULT_TUPLE_DELIMITER"],
|
287 |
record_delimiter=PROMPTS["DEFAULT_RECORD_DELIMITER"],
|
288 |
completion_delimiter=PROMPTS["DEFAULT_COMPLETION_DELIMITER"],
|
289 |
+
entity_types=",".join(entity_types),
|
290 |
examples=examples,
|
291 |
language=language,
|
292 |
)
|
|
|
415 |
):
|
416 |
all_relationships_data.append(await result)
|
417 |
|
418 |
+
if not len(all_entities_data) and not len(all_relationships_data):
|
|
|
|
|
|
|
419 |
logger.warning(
|
420 |
+
"Didn't extract any entities and relationships, maybe your LLM is not working"
|
421 |
)
|
422 |
return None
|
423 |
|
424 |
+
if not len(all_entities_data):
|
425 |
+
logger.warning("Didn't extract any entities")
|
426 |
+
if not len(all_relationships_data):
|
427 |
+
logger.warning("Didn't extract any relationships")
|
428 |
+
|
429 |
if entity_vdb is not None:
|
430 |
data_for_vdb = {
|
431 |
compute_mdhash_id(dp["entity_name"], prefix="ent-"): {
|
|
|
635 |
text_chunks_db,
|
636 |
query_param,
|
637 |
)
|
638 |
+
if (
|
639 |
+
hl_entities_context == ""
|
640 |
+
and hl_relations_context == ""
|
641 |
+
and hl_text_units_context == ""
|
642 |
+
):
|
643 |
+
logger.warn("No high level context found. Switching to local mode.")
|
644 |
+
query_param.mode = "local"
|
645 |
if query_param.mode == "hybrid":
|
646 |
entities_context, relations_context, text_units_context = combine_contexts(
|
647 |
[hl_entities_context, ll_entities_context],
|
|
|
877 |
results = await relationships_vdb.query(keywords, top_k=query_param.top_k)
|
878 |
|
879 |
if not len(results):
|
880 |
+
return "", "", ""
|
881 |
|
882 |
edge_datas = await asyncio.gather(
|
883 |
*[knowledge_graph_inst.get_edge(r["src_id"], r["tgt_id"]) for r in results]
|
lightrag/prompt.py
CHANGED
@@ -8,7 +8,7 @@ PROMPTS["DEFAULT_RECORD_DELIMITER"] = "##"
|
|
8 |
PROMPTS["DEFAULT_COMPLETION_DELIMITER"] = "<|COMPLETE|>"
|
9 |
PROMPTS["process_tickers"] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
10 |
|
11 |
-
PROMPTS["DEFAULT_ENTITY_TYPES"] = ["organization", "person", "geo", "event"]
|
12 |
|
13 |
PROMPTS["entity_extraction"] = """-Goal-
|
14 |
Given a text document that is potentially relevant to this activity and a list of entity types, identify all entities of those types from the text and all relationships among the identified entities.
|
@@ -268,14 +268,19 @@ PROMPTS[
|
|
268 |
Question 1: {original_prompt}
|
269 |
Question 2: {cached_prompt}
|
270 |
|
271 |
-
Please evaluate:
|
272 |
1. Whether these two questions are semantically similar
|
273 |
2. Whether the answer to Question 2 can be used to answer Question 1
|
274 |
-
|
275 |
-
|
276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
277 |
1: Identical and answer can be directly reused
|
278 |
0.5: Partially related and answer needs modification to be used
|
279 |
-
|
280 |
Return only a number between 0-1, without any additional content.
|
281 |
"""
|
|
|
8 |
PROMPTS["DEFAULT_COMPLETION_DELIMITER"] = "<|COMPLETE|>"
|
9 |
PROMPTS["process_tickers"] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
10 |
|
11 |
+
PROMPTS["DEFAULT_ENTITY_TYPES"] = ["organization", "person", "geo", "event", "category"]
|
12 |
|
13 |
PROMPTS["entity_extraction"] = """-Goal-
|
14 |
Given a text document that is potentially relevant to this activity and a list of entity types, identify all entities of those types from the text and all relationships among the identified entities.
|
|
|
268 |
Question 1: {original_prompt}
|
269 |
Question 2: {cached_prompt}
|
270 |
|
271 |
+
Please evaluate the following two points and provide a similarity score between 0 and 1 directly:
|
272 |
1. Whether these two questions are semantically similar
|
273 |
2. Whether the answer to Question 2 can be used to answer Question 1
|
274 |
+
Similarity score criteria:
|
275 |
+
0: Completely unrelated or answer cannot be reused, including but not limited to:
|
276 |
+
- The questions have different topics
|
277 |
+
- The locations mentioned in the questions are different
|
278 |
+
- The times mentioned in the questions are different
|
279 |
+
- The specific individuals mentioned in the questions are different
|
280 |
+
- The specific events mentioned in the questions are different
|
281 |
+
- The background information in the questions is different
|
282 |
+
- The key conditions in the questions are different
|
283 |
1: Identical and answer can be directly reused
|
284 |
0.5: Partially related and answer needs modification to be used
|
|
|
285 |
Return only a number between 0-1, without any additional content.
|
286 |
"""
|
lightrag/storage.py
CHANGED
@@ -107,10 +107,16 @@ class NanoVectorDBStorage(BaseVectorStorage):
|
|
107 |
embeddings = await f
|
108 |
embeddings_list.append(embeddings)
|
109 |
embeddings = np.concatenate(embeddings_list)
|
110 |
-
|
111 |
-
d
|
112 |
-
|
113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
114 |
|
115 |
async def query(self, query: str, top_k=5):
|
116 |
embedding = await self.embedding_func([query])
|
|
|
107 |
embeddings = await f
|
108 |
embeddings_list.append(embeddings)
|
109 |
embeddings = np.concatenate(embeddings_list)
|
110 |
+
if len(embeddings) == len(list_data):
|
111 |
+
for i, d in enumerate(list_data):
|
112 |
+
d["__vector__"] = embeddings[i]
|
113 |
+
results = self._client.upsert(datas=list_data)
|
114 |
+
return results
|
115 |
+
else:
|
116 |
+
# sometimes the embedding is not returned correctly. just log it.
|
117 |
+
logger.error(
|
118 |
+
f"embedding is not 1-1 with data, {len(embeddings)} != {len(list_data)}"
|
119 |
+
)
|
120 |
|
121 |
async def query(self, query: str, top_k=5):
|
122 |
embedding = await self.embedding_func([query])
|
lightrag/utils.py
CHANGED
@@ -17,6 +17,17 @@ import tiktoken
|
|
17 |
|
18 |
from lightrag.prompt import PROMPTS
|
19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
ENCODER = None
|
21 |
|
22 |
logger = logging.getLogger("lightrag")
|
@@ -42,9 +53,17 @@ class EmbeddingFunc:
|
|
42 |
embedding_dim: int
|
43 |
max_token_size: int
|
44 |
func: callable
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
|
46 |
async def __call__(self, *args, **kwargs) -> np.ndarray:
|
47 |
-
|
|
|
48 |
|
49 |
|
50 |
def locate_json_string_body_from_string(content: str) -> Union[str, None]:
|
|
|
17 |
|
18 |
from lightrag.prompt import PROMPTS
|
19 |
|
20 |
+
|
21 |
+
class UnlimitedSemaphore:
|
22 |
+
"""A context manager that allows unlimited access."""
|
23 |
+
|
24 |
+
async def __aenter__(self):
|
25 |
+
pass
|
26 |
+
|
27 |
+
async def __aexit__(self, exc_type, exc, tb):
|
28 |
+
pass
|
29 |
+
|
30 |
+
|
31 |
ENCODER = None
|
32 |
|
33 |
logger = logging.getLogger("lightrag")
|
|
|
53 |
embedding_dim: int
|
54 |
max_token_size: int
|
55 |
func: callable
|
56 |
+
concurrent_limit: int = 16
|
57 |
+
|
58 |
+
def __post_init__(self):
|
59 |
+
if self.concurrent_limit != 0:
|
60 |
+
self._semaphore = asyncio.Semaphore(self.concurrent_limit)
|
61 |
+
else:
|
62 |
+
self._semaphore = UnlimitedSemaphore()
|
63 |
|
64 |
async def __call__(self, *args, **kwargs) -> np.ndarray:
|
65 |
+
async with self._semaphore:
|
66 |
+
return await self.func(*args, **kwargs)
|
67 |
|
68 |
|
69 |
def locate_json_string_body_from_string(content: str) -> Union[str, None]:
|