yangdx commited on
Commit
7ff0d05
·
2 Parent(s): 8fc3533 c903e60

Merge branch 'main' into clear-doc

Browse files
README-zh.md CHANGED
@@ -409,6 +409,54 @@ if __name__ == "__main__":
409
 
410
  </details>
411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  ### 对话历史
413
 
414
  LightRAG现在通过对话历史功能支持多轮对话。以下是使用方法:
 
409
 
410
  </details>
411
 
412
+ ### Token统计功能
413
+ <details>
414
+ <summary> <b>概述和使用</b> </summary>
415
+
416
+ LightRAG提供了TokenTracker工具来跟踪和管理大模型的token消耗。这个功能对于控制API成本和优化性能特别有用。
417
+
418
+ #### 使用方法
419
+
420
+ ```python
421
+ from lightrag.utils import TokenTracker
422
+
423
+ # 创建TokenTracker实例
424
+ token_tracker = TokenTracker()
425
+
426
+ # 方法1:使用上下文管理器(推荐)
427
+ # 适用于需要自动跟踪token使用的场景
428
+ with token_tracker:
429
+ result1 = await llm_model_func("你的问题1")
430
+ result2 = await llm_model_func("你的问题2")
431
+
432
+ # 方法2:手动添加token使用记录
433
+ # 适用于需要更精细控制token统计的场景
434
+ token_tracker.reset()
435
+
436
+ rag.insert()
437
+
438
+ rag.query("你的问题1", param=QueryParam(mode="naive"))
439
+ rag.query("你的问题2", param=QueryParam(mode="mix"))
440
+
441
+ # 显示总token使用量(包含插入和查询操作)
442
+ print("Token usage:", token_tracker.get_usage())
443
+ ```
444
+
445
+ #### 使用建议
446
+ - 在长会话或批量操作中使用上下文管理器,可以自动跟踪所有token消耗
447
+ - 对于需要分段统计的场景,使用手动模式并适时调用reset()
448
+ - 定期检查token使用情况,有助于及时发现异常消耗
449
+ - 在开发测试阶段积极使用此功能,以便优化生产环境的成本
450
+
451
+ #### 实际应用示例
452
+ 您可以参考以下示例来实现token统计:
453
+ - `examples/lightrag_gemini_track_token_demo.py`:使用Google Gemini模型的token统计示例
454
+ - `examples/lightrag_siliconcloud_track_token_demo.py`:使用SiliconCloud模型的token统计示例
455
+
456
+ 这些示例展示了如何在不同模型和场景下有效地使用TokenTracker功能。
457
+
458
+ </details>
459
+
460
  ### 对话历史
461
 
462
  LightRAG现在通过对话历史功能支持多轮对话。以下是使用方法:
README.md CHANGED
@@ -440,11 +440,65 @@ if __name__ == "__main__":
440
  - [Direct OpenAI Example](examples/lightrag_llamaindex_direct_demo.py)
441
  - [LiteLLM Proxy Example](examples/lightrag_llamaindex_litellm_demo.py)
442
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  ### Conversation History Support
444
 
445
 
446
  LightRAG now supports multi-turn dialogue through the conversation history feature. Here's how to use it:
447
 
 
 
 
448
  ```python
449
  # Create conversation history
450
  conversation_history = [
@@ -467,10 +521,15 @@ response = rag.query(
467
  )
468
  ```
469
 
 
 
470
  ### Custom Prompt Support
471
 
472
  LightRAG now supports custom prompts for fine-tuned control over the system's behavior. Here's how to use it:
473
 
 
 
 
474
  ```python
475
  # Create query parameters
476
  query_param = QueryParam(
@@ -505,6 +564,8 @@ response_custom = rag.query(
505
  print(response_custom)
506
  ```
507
 
 
 
508
  ### Separate Keyword Extraction
509
 
510
  We've introduced a new function `query_with_separate_keyword_extraction` to enhance the keyword extraction capabilities. This function separates the keyword extraction process from the user's prompt, focusing solely on the query to improve the relevance of extracted keywords.
@@ -518,7 +579,8 @@ The function operates by dividing the input into two parts:
518
 
519
  It then performs keyword extraction exclusively on the `user query`. This separation ensures that the extraction process is focused and relevant, unaffected by any additional language in the `prompt`. It also allows the `prompt` to serve purely for response formatting, maintaining the intent and clarity of the user's original question.
520
 
521
- **Usage Example**
 
522
 
523
  This `example` shows how to tailor the function for educational content, focusing on detailed explanations for older students.
524
 
@@ -530,67 +592,6 @@ rag.query_with_separate_keyword_extraction(
530
  )
531
  ```
532
 
533
- ### Insert Custom KG
534
-
535
- ```python
536
- custom_kg = {
537
- "chunks": [
538
- {
539
- "content": "Alice and Bob are collaborating on quantum computing research.",
540
- "source_id": "doc-1"
541
- }
542
- ],
543
- "entities": [
544
- {
545
- "entity_name": "Alice",
546
- "entity_type": "person",
547
- "description": "Alice is a researcher specializing in quantum physics.",
548
- "source_id": "doc-1"
549
- },
550
- {
551
- "entity_name": "Bob",
552
- "entity_type": "person",
553
- "description": "Bob is a mathematician.",
554
- "source_id": "doc-1"
555
- },
556
- {
557
- "entity_name": "Quantum Computing",
558
- "entity_type": "technology",
559
- "description": "Quantum computing utilizes quantum mechanical phenomena for computation.",
560
- "source_id": "doc-1"
561
- }
562
- ],
563
- "relationships": [
564
- {
565
- "src_id": "Alice",
566
- "tgt_id": "Bob",
567
- "description": "Alice and Bob are research partners.",
568
- "keywords": "collaboration research",
569
- "weight": 1.0,
570
- "source_id": "doc-1"
571
- },
572
- {
573
- "src_id": "Alice",
574
- "tgt_id": "Quantum Computing",
575
- "description": "Alice conducts research on quantum computing.",
576
- "keywords": "research expertise",
577
- "weight": 1.0,
578
- "source_id": "doc-1"
579
- },
580
- {
581
- "src_id": "Bob",
582
- "tgt_id": "Quantum Computing",
583
- "description": "Bob researches quantum computing.",
584
- "keywords": "research application",
585
- "weight": 1.0,
586
- "source_id": "doc-1"
587
- }
588
- ]
589
- }
590
-
591
- rag.insert_custom_kg(custom_kg)
592
- ```
593
-
594
  </details>
595
 
596
  ## Insert
@@ -682,6 +683,70 @@ rag.insert(text_content.decode('utf-8'))
682
 
683
  </details>
684
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685
  <details>
686
  <summary><b>Citation Functionality</b></summary>
687
 
@@ -841,7 +906,8 @@ rag.delete_by_doc_id("doc_id")
841
 
842
  LightRAG now supports comprehensive knowledge graph management capabilities, allowing you to create, edit, and delete entities and relationships within your knowledge graph.
843
 
844
- ### Create Entities and Relations
 
845
 
846
  ```python
847
  # Create new entity
@@ -864,7 +930,10 @@ relation = rag.create_relation("Google", "Gmail", {
864
  })
865
  ```
866
 
867
- ### Edit Entities and Relations
 
 
 
868
 
869
  ```python
870
  # Edit an existing entity
@@ -901,6 +970,8 @@ All operations are available in both synchronous and asynchronous versions. The
901
 
902
  These operations maintain data consistency across both the graph database and vector database components, ensuring your knowledge graph remains coherent.
903
 
 
 
904
  ## Data Export Functions
905
 
906
  ### Overview
@@ -909,7 +980,8 @@ LightRAG allows you to export your knowledge graph data in various formats for a
909
 
910
  ### Export Functions
911
 
912
- #### Basic Usage
 
913
 
914
  ```python
915
  # Basic CSV export (default format)
@@ -919,7 +991,10 @@ rag.export_data("knowledge_graph.csv")
919
  rag.export_data("output.xlsx", file_format="excel")
920
  ```
921
 
922
- #### Different File Formats supported
 
 
 
923
 
924
  ```python
925
  #Export data in CSV format
@@ -934,13 +1009,18 @@ rag.export_data("graph_data.md", file_format="md")
934
  # Export data in Text
935
  rag.export_data("graph_data.txt", file_format="txt")
936
  ```
937
- #### Additional Options
 
 
 
938
 
939
  Include vector embeddings in the export (optional):
940
 
941
  ```python
942
  rag.export_data("complete_data.csv", include_vector_data=True)
943
  ```
 
 
944
  ### Data Included in Export
945
 
946
  All exports include:
 
440
  - [Direct OpenAI Example](examples/lightrag_llamaindex_direct_demo.py)
441
  - [LiteLLM Proxy Example](examples/lightrag_llamaindex_litellm_demo.py)
442
 
443
+ </details>
444
+
445
+ ### Token Usage Tracking
446
+
447
+ <details>
448
+ <summary> <b>Overview and Usage</b> </summary>
449
+
450
+ LightRAG provides a TokenTracker tool to monitor and manage token consumption by large language models. This feature is particularly useful for controlling API costs and optimizing performance.
451
+
452
+ #### Usage
453
+
454
+ ```python
455
+ from lightrag.utils import TokenTracker
456
+
457
+ # Create TokenTracker instance
458
+ token_tracker = TokenTracker()
459
+
460
+ # Method 1: Using context manager (Recommended)
461
+ # Suitable for scenarios requiring automatic token usage tracking
462
+ with token_tracker:
463
+ result1 = await llm_model_func("your question 1")
464
+ result2 = await llm_model_func("your question 2")
465
+
466
+ # Method 2: Manually adding token usage records
467
+ # Suitable for scenarios requiring more granular control over token statistics
468
+ token_tracker.reset()
469
+
470
+ rag.insert()
471
+
472
+ rag.query("your question 1", param=QueryParam(mode="naive"))
473
+ rag.query("your question 2", param=QueryParam(mode="mix"))
474
+
475
+ # Display total token usage (including insert and query operations)
476
+ print("Token usage:", token_tracker.get_usage())
477
+ ```
478
+
479
+ #### Usage Tips
480
+ - Use context managers for long sessions or batch operations to automatically track all token consumption
481
+ - For scenarios requiring segmented statistics, use manual mode and call reset() when appropriate
482
+ - Regular checking of token usage helps detect abnormal consumption early
483
+ - Actively use this feature during development and testing to optimize production costs
484
+
485
+ #### Practical Examples
486
+ You can refer to these examples for implementing token tracking:
487
+ - `examples/lightrag_gemini_track_token_demo.py`: Token tracking example using Google Gemini model
488
+ - `examples/lightrag_siliconcloud_track_token_demo.py`: Token tracking example using SiliconCloud model
489
+
490
+ These examples demonstrate how to effectively use the TokenTracker feature with different models and scenarios.
491
+
492
+ </details>
493
+
494
  ### Conversation History Support
495
 
496
 
497
  LightRAG now supports multi-turn dialogue through the conversation history feature. Here's how to use it:
498
 
499
+ <details>
500
+ <summary> <b> Usage Example </b></summary>
501
+
502
  ```python
503
  # Create conversation history
504
  conversation_history = [
 
521
  )
522
  ```
523
 
524
+ </details>
525
+
526
  ### Custom Prompt Support
527
 
528
  LightRAG now supports custom prompts for fine-tuned control over the system's behavior. Here's how to use it:
529
 
530
+ <details>
531
+ <summary> <b> Usage Example </b></summary>
532
+
533
  ```python
534
  # Create query parameters
535
  query_param = QueryParam(
 
564
  print(response_custom)
565
  ```
566
 
567
+ </details>
568
+
569
  ### Separate Keyword Extraction
570
 
571
  We've introduced a new function `query_with_separate_keyword_extraction` to enhance the keyword extraction capabilities. This function separates the keyword extraction process from the user's prompt, focusing solely on the query to improve the relevance of extracted keywords.
 
579
 
580
  It then performs keyword extraction exclusively on the `user query`. This separation ensures that the extraction process is focused and relevant, unaffected by any additional language in the `prompt`. It also allows the `prompt` to serve purely for response formatting, maintaining the intent and clarity of the user's original question.
581
 
582
+ <details>
583
+ <summary> <b> Usage Example </b></summary>
584
 
585
  This `example` shows how to tailor the function for educational content, focusing on detailed explanations for older students.
586
 
 
592
  )
593
  ```
594
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  </details>
596
 
597
  ## Insert
 
683
 
684
  </details>
685
 
686
+ <details>
687
+ <summary> <b> Insert Custom KG </b></summary>
688
+
689
+ ```python
690
+ custom_kg = {
691
+ "chunks": [
692
+ {
693
+ "content": "Alice and Bob are collaborating on quantum computing research.",
694
+ "source_id": "doc-1"
695
+ }
696
+ ],
697
+ "entities": [
698
+ {
699
+ "entity_name": "Alice",
700
+ "entity_type": "person",
701
+ "description": "Alice is a researcher specializing in quantum physics.",
702
+ "source_id": "doc-1"
703
+ },
704
+ {
705
+ "entity_name": "Bob",
706
+ "entity_type": "person",
707
+ "description": "Bob is a mathematician.",
708
+ "source_id": "doc-1"
709
+ },
710
+ {
711
+ "entity_name": "Quantum Computing",
712
+ "entity_type": "technology",
713
+ "description": "Quantum computing utilizes quantum mechanical phenomena for computation.",
714
+ "source_id": "doc-1"
715
+ }
716
+ ],
717
+ "relationships": [
718
+ {
719
+ "src_id": "Alice",
720
+ "tgt_id": "Bob",
721
+ "description": "Alice and Bob are research partners.",
722
+ "keywords": "collaboration research",
723
+ "weight": 1.0,
724
+ "source_id": "doc-1"
725
+ },
726
+ {
727
+ "src_id": "Alice",
728
+ "tgt_id": "Quantum Computing",
729
+ "description": "Alice conducts research on quantum computing.",
730
+ "keywords": "research expertise",
731
+ "weight": 1.0,
732
+ "source_id": "doc-1"
733
+ },
734
+ {
735
+ "src_id": "Bob",
736
+ "tgt_id": "Quantum Computing",
737
+ "description": "Bob researches quantum computing.",
738
+ "keywords": "research application",
739
+ "weight": 1.0,
740
+ "source_id": "doc-1"
741
+ }
742
+ ]
743
+ }
744
+
745
+ rag.insert_custom_kg(custom_kg)
746
+ ```
747
+
748
+ </details>
749
+
750
  <details>
751
  <summary><b>Citation Functionality</b></summary>
752
 
 
906
 
907
  LightRAG now supports comprehensive knowledge graph management capabilities, allowing you to create, edit, and delete entities and relationships within your knowledge graph.
908
 
909
+ <details>
910
+ <summary> <b> Create Entities and Relations </b></summary>
911
 
912
  ```python
913
  # Create new entity
 
930
  })
931
  ```
932
 
933
+ </details>
934
+
935
+ <details>
936
+ <summary> <b> Edit Entities and Relations </b></summary>
937
 
938
  ```python
939
  # Edit an existing entity
 
970
 
971
  These operations maintain data consistency across both the graph database and vector database components, ensuring your knowledge graph remains coherent.
972
 
973
+ </details>
974
+
975
  ## Data Export Functions
976
 
977
  ### Overview
 
980
 
981
  ### Export Functions
982
 
983
+ <details>
984
+ <summary> <b> Basic Usage </b></summary>
985
 
986
  ```python
987
  # Basic CSV export (default format)
 
991
  rag.export_data("output.xlsx", file_format="excel")
992
  ```
993
 
994
+ </details>
995
+
996
+ <details>
997
+ <summary> <b> Different File Formats supported </b></summary>
998
 
999
  ```python
1000
  #Export data in CSV format
 
1009
  # Export data in Text
1010
  rag.export_data("graph_data.txt", file_format="txt")
1011
  ```
1012
+ </details>
1013
+
1014
+ <details>
1015
+ <summary> <b> Additional Options </b></summary>
1016
 
1017
  Include vector embeddings in the export (optional):
1018
 
1019
  ```python
1020
  rag.export_data("complete_data.csv", include_vector_data=True)
1021
  ```
1022
+ </details>
1023
+
1024
  ### Data Included in Export
1025
 
1026
  All exports include:
examples/lightrag_gemini_track_token_demo.py CHANGED
@@ -115,38 +115,36 @@ def main():
115
  # Initialize RAG instance
116
  rag = asyncio.run(initialize_rag())
117
 
118
- # Reset tracker before processing queries
119
- token_tracker.reset()
120
-
121
  with open("./book.txt", "r", encoding="utf-8") as f:
122
  rag.insert(f.read())
123
 
124
- print(
125
- rag.query(
126
- "What are the top themes in this story?", param=QueryParam(mode="naive")
 
 
 
127
  )
128
- )
129
 
130
- print(
131
- rag.query(
132
- "What are the top themes in this story?", param=QueryParam(mode="local")
 
133
  )
134
- )
135
 
136
- print(
137
- rag.query(
138
- "What are the top themes in this story?", param=QueryParam(mode="global")
 
 
139
  )
140
- )
141
 
142
- print(
143
- rag.query(
144
- "What are the top themes in this story?", param=QueryParam(mode="hybrid")
 
 
145
  )
146
- )
147
-
148
- # Display final token usage after main query
149
- print("Token usage:", token_tracker.get_usage())
150
 
151
 
152
  if __name__ == "__main__":
 
115
  # Initialize RAG instance
116
  rag = asyncio.run(initialize_rag())
117
 
 
 
 
118
  with open("./book.txt", "r", encoding="utf-8") as f:
119
  rag.insert(f.read())
120
 
121
+ # Context Manager Method
122
+ with token_tracker:
123
+ print(
124
+ rag.query(
125
+ "What are the top themes in this story?", param=QueryParam(mode="naive")
126
+ )
127
  )
 
128
 
129
+ print(
130
+ rag.query(
131
+ "What are the top themes in this story?", param=QueryParam(mode="local")
132
+ )
133
  )
 
134
 
135
+ print(
136
+ rag.query(
137
+ "What are the top themes in this story?",
138
+ param=QueryParam(mode="global"),
139
+ )
140
  )
 
141
 
142
+ print(
143
+ rag.query(
144
+ "What are the top themes in this story?",
145
+ param=QueryParam(mode="hybrid"),
146
+ )
147
  )
 
 
 
 
148
 
149
 
150
  if __name__ == "__main__":
examples/lightrag_siliconcloud_track_token_demo.py CHANGED
@@ -44,14 +44,10 @@ async def embedding_func(texts: list[str]) -> np.ndarray:
44
 
45
  # function test
46
  async def test_funcs():
47
- # Reset tracker before processing queries
48
- token_tracker.reset()
49
-
50
- result = await llm_model_func("How are you?")
51
- print("llm_model_func: ", result)
52
-
53
- # Display final token usage after main query
54
- print("Token usage:", token_tracker.get_usage())
55
 
56
 
57
  asyncio.run(test_funcs())
 
44
 
45
  # function test
46
  async def test_funcs():
47
+ # Context Manager Method
48
+ with token_tracker:
49
+ result = await llm_model_func("How are you?")
50
+ print("llm_model_func: ", result)
 
 
 
 
51
 
52
 
53
  asyncio.run(test_funcs())
lightrag/llm/openai.py CHANGED
@@ -44,6 +44,47 @@ class InvalidResponseError(Exception):
44
  pass
45
 
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  @retry(
48
  stop=stop_after_attempt(3),
49
  wait=wait_exponential(multiplier=1, min=4, max=10),
@@ -61,29 +102,52 @@ async def openai_complete_if_cache(
61
  token_tracker: Any | None = None,
62
  **kwargs: Any,
63
  ) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  if history_messages is None:
65
  history_messages = []
66
- if not api_key:
67
- api_key = os.environ["OPENAI_API_KEY"]
68
-
69
- default_headers = {
70
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_8) LightRAG/{__api_version__}",
71
- "Content-Type": "application/json",
72
- }
73
 
74
  # Set openai logger level to INFO when VERBOSE_DEBUG is off
75
  if not VERBOSE_DEBUG and logger.level == logging.DEBUG:
76
  logging.getLogger("openai").setLevel(logging.INFO)
77
 
78
- openai_async_client = (
79
- AsyncOpenAI(default_headers=default_headers, api_key=api_key)
80
- if base_url is None
81
- else AsyncOpenAI(
82
- base_url=base_url, default_headers=default_headers, api_key=api_key
83
- )
84
  )
 
 
85
  kwargs.pop("hashing_kv", None)
86
  kwargs.pop("keyword_extraction", None)
 
 
87
  messages: list[dict[str, Any]] = []
88
  if system_prompt:
89
  messages.append({"role": "system", "content": system_prompt})
@@ -272,21 +336,32 @@ async def openai_embed(
272
  model: str = "text-embedding-3-small",
273
  base_url: str = None,
274
  api_key: str = None,
 
275
  ) -> np.ndarray:
276
- if not api_key:
277
- api_key = os.environ["OPENAI_API_KEY"]
278
-
279
- default_headers = {
280
- "User-Agent": f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_8) LightRAG/{__api_version__}",
281
- "Content-Type": "application/json",
282
- }
283
- openai_async_client = (
284
- AsyncOpenAI(default_headers=default_headers, api_key=api_key)
285
- if base_url is None
286
- else AsyncOpenAI(
287
- base_url=base_url, default_headers=default_headers, api_key=api_key
288
- )
 
 
 
 
 
 
 
 
 
289
  )
 
290
  response = await openai_async_client.embeddings.create(
291
  model=model, input=texts, encoding_format="float"
292
  )
 
44
  pass
45
 
46
 
47
+ def create_openai_async_client(
48
+ api_key: str | None = None,
49
+ base_url: str | None = None,
50
+ client_configs: dict[str, Any] = None,
51
+ ) -> AsyncOpenAI:
52
+ """Create an AsyncOpenAI client with the given configuration.
53
+
54
+ Args:
55
+ api_key: OpenAI API key. If None, uses the OPENAI_API_KEY environment variable.
56
+ base_url: Base URL for the OpenAI API. If None, uses the default OpenAI API URL.
57
+ client_configs: Additional configuration options for the AsyncOpenAI client.
58
+ These will override any default configurations but will be overridden by
59
+ explicit parameters (api_key, base_url).
60
+
61
+ Returns:
62
+ An AsyncOpenAI client instance.
63
+ """
64
+ if not api_key:
65
+ api_key = os.environ["OPENAI_API_KEY"]
66
+
67
+ default_headers = {
68
+ "User-Agent": f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_8) LightRAG/{__api_version__}",
69
+ "Content-Type": "application/json",
70
+ }
71
+
72
+ if client_configs is None:
73
+ client_configs = {}
74
+
75
+ # Create a merged config dict with precedence: explicit params > client_configs > defaults
76
+ merged_configs = {
77
+ **client_configs,
78
+ "default_headers": default_headers,
79
+ "api_key": api_key,
80
+ }
81
+
82
+ if base_url is not None:
83
+ merged_configs["base_url"] = base_url
84
+
85
+ return AsyncOpenAI(**merged_configs)
86
+
87
+
88
  @retry(
89
  stop=stop_after_attempt(3),
90
  wait=wait_exponential(multiplier=1, min=4, max=10),
 
102
  token_tracker: Any | None = None,
103
  **kwargs: Any,
104
  ) -> str:
105
+ """Complete a prompt using OpenAI's API with caching support.
106
+
107
+ Args:
108
+ model: The OpenAI model to use.
109
+ prompt: The prompt to complete.
110
+ system_prompt: Optional system prompt to include.
111
+ history_messages: Optional list of previous messages in the conversation.
112
+ base_url: Optional base URL for the OpenAI API.
113
+ api_key: Optional OpenAI API key. If None, uses the OPENAI_API_KEY environment variable.
114
+ **kwargs: Additional keyword arguments to pass to the OpenAI API.
115
+ Special kwargs:
116
+ - openai_client_configs: Dict of configuration options for the AsyncOpenAI client.
117
+ These will be passed to the client constructor but will be overridden by
118
+ explicit parameters (api_key, base_url).
119
+ - hashing_kv: Will be removed from kwargs before passing to OpenAI.
120
+ - keyword_extraction: Will be removed from kwargs before passing to OpenAI.
121
+
122
+ Returns:
123
+ The completed text or an async iterator of text chunks if streaming.
124
+
125
+ Raises:
126
+ InvalidResponseError: If the response from OpenAI is invalid or empty.
127
+ APIConnectionError: If there is a connection error with the OpenAI API.
128
+ RateLimitError: If the OpenAI API rate limit is exceeded.
129
+ APITimeoutError: If the OpenAI API request times out.
130
+ """
131
  if history_messages is None:
132
  history_messages = []
 
 
 
 
 
 
 
133
 
134
  # Set openai logger level to INFO when VERBOSE_DEBUG is off
135
  if not VERBOSE_DEBUG and logger.level == logging.DEBUG:
136
  logging.getLogger("openai").setLevel(logging.INFO)
137
 
138
+ # Extract client configuration options
139
+ client_configs = kwargs.pop("openai_client_configs", {})
140
+
141
+ # Create the OpenAI client
142
+ openai_async_client = create_openai_async_client(
143
+ api_key=api_key, base_url=base_url, client_configs=client_configs
144
  )
145
+
146
+ # Remove special kwargs that shouldn't be passed to OpenAI
147
  kwargs.pop("hashing_kv", None)
148
  kwargs.pop("keyword_extraction", None)
149
+
150
+ # Prepare messages
151
  messages: list[dict[str, Any]] = []
152
  if system_prompt:
153
  messages.append({"role": "system", "content": system_prompt})
 
336
  model: str = "text-embedding-3-small",
337
  base_url: str = None,
338
  api_key: str = None,
339
+ client_configs: dict[str, Any] = None,
340
  ) -> np.ndarray:
341
+ """Generate embeddings for a list of texts using OpenAI's API.
342
+
343
+ Args:
344
+ texts: List of texts to embed.
345
+ model: The OpenAI embedding model to use.
346
+ base_url: Optional base URL for the OpenAI API.
347
+ api_key: Optional OpenAI API key. If None, uses the OPENAI_API_KEY environment variable.
348
+ client_configs: Additional configuration options for the AsyncOpenAI client.
349
+ These will override any default configurations but will be overridden by
350
+ explicit parameters (api_key, base_url).
351
+
352
+ Returns:
353
+ A numpy array of embeddings, one per input text.
354
+
355
+ Raises:
356
+ APIConnectionError: If there is a connection error with the OpenAI API.
357
+ RateLimitError: If the OpenAI API rate limit is exceeded.
358
+ APITimeoutError: If the OpenAI API request times out.
359
+ """
360
+ # Create the OpenAI client
361
+ openai_async_client = create_openai_async_client(
362
+ api_key=api_key, base_url=base_url, client_configs=client_configs
363
  )
364
+
365
  response = await openai_async_client.embeddings.create(
366
  model=model, input=texts, encoding_format="float"
367
  )
lightrag/operate.py CHANGED
@@ -697,8 +697,7 @@ async def kg_query(
697
  if cached_response is not None:
698
  return cached_response
699
 
700
- # Extract keywords using extract_keywords_only function which already supports conversation history
701
- hl_keywords, ll_keywords = await extract_keywords_only(
702
  query, query_param, global_config, hashing_kv
703
  )
704
 
@@ -794,6 +793,38 @@ async def kg_query(
794
  return response
795
 
796
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
797
  async def extract_keywords_only(
798
  text: str,
799
  param: QueryParam,
@@ -934,8 +965,7 @@ async def mix_kg_vector_query(
934
  # 2. Execute knowledge graph and vector searches in parallel
935
  async def get_kg_context():
936
  try:
937
- # Extract keywords using extract_keywords_only function which already supports conversation history
938
- hl_keywords, ll_keywords = await extract_keywords_only(
939
  query, query_param, global_config, hashing_kv
940
  )
941
 
@@ -983,7 +1013,6 @@ async def mix_kg_vector_query(
983
  try:
984
  # Reduce top_k for vector search in hybrid mode since we have structured information from KG
985
  mix_topk = min(10, query_param.top_k)
986
- # TODO: add ids to the query
987
  results = await chunks_vdb.query(
988
  augmented_query, top_k=mix_topk, ids=query_param.ids
989
  )
@@ -1581,9 +1610,7 @@ async def _get_edge_data(
1581
 
1582
  text_units_section_list = [["id", "content", "file_path"]]
1583
  for i, t in enumerate(use_text_units):
1584
- text_units_section_list.append(
1585
- [i, t["content"], t.get("file_path", "unknown_source")]
1586
- )
1587
  text_units_context = list_of_list_to_csv(text_units_section_list)
1588
  return entities_context, relations_context, text_units_context
1589
 
@@ -2017,16 +2044,13 @@ async def query_with_keywords(
2017
  Query response or async iterator
2018
  """
2019
  # Extract keywords
2020
- hl_keywords, ll_keywords = await extract_keywords_only(
2021
- text=query,
2022
- param=param,
2023
  global_config=global_config,
2024
  hashing_kv=hashing_kv,
2025
  )
2026
 
2027
- param.hl_keywords = hl_keywords
2028
- param.ll_keywords = ll_keywords
2029
-
2030
  # Create a new string with the prompt and the keywords
2031
  ll_keywords_str = ", ".join(ll_keywords)
2032
  hl_keywords_str = ", ".join(hl_keywords)
 
697
  if cached_response is not None:
698
  return cached_response
699
 
700
+ hl_keywords, ll_keywords = await get_keywords_from_query(
 
701
  query, query_param, global_config, hashing_kv
702
  )
703
 
 
793
  return response
794
 
795
 
796
+ async def get_keywords_from_query(
797
+ query: str,
798
+ query_param: QueryParam,
799
+ global_config: dict[str, str],
800
+ hashing_kv: BaseKVStorage | None = None,
801
+ ) -> tuple[list[str], list[str]]:
802
+ """
803
+ Retrieves high-level and low-level keywords for RAG operations.
804
+
805
+ This function checks if keywords are already provided in query parameters,
806
+ and if not, extracts them from the query text using LLM.
807
+
808
+ Args:
809
+ query: The user's query text
810
+ query_param: Query parameters that may contain pre-defined keywords
811
+ global_config: Global configuration dictionary
812
+ hashing_kv: Optional key-value storage for caching results
813
+
814
+ Returns:
815
+ A tuple containing (high_level_keywords, low_level_keywords)
816
+ """
817
+ # Check if pre-defined keywords are already provided
818
+ if query_param.hl_keywords or query_param.ll_keywords:
819
+ return query_param.hl_keywords, query_param.ll_keywords
820
+
821
+ # Extract keywords using extract_keywords_only function which already supports conversation history
822
+ hl_keywords, ll_keywords = await extract_keywords_only(
823
+ query, query_param, global_config, hashing_kv
824
+ )
825
+ return hl_keywords, ll_keywords
826
+
827
+
828
  async def extract_keywords_only(
829
  text: str,
830
  param: QueryParam,
 
965
  # 2. Execute knowledge graph and vector searches in parallel
966
  async def get_kg_context():
967
  try:
968
+ hl_keywords, ll_keywords = await get_keywords_from_query(
 
969
  query, query_param, global_config, hashing_kv
970
  )
971
 
 
1013
  try:
1014
  # Reduce top_k for vector search in hybrid mode since we have structured information from KG
1015
  mix_topk = min(10, query_param.top_k)
 
1016
  results = await chunks_vdb.query(
1017
  augmented_query, top_k=mix_topk, ids=query_param.ids
1018
  )
 
1610
 
1611
  text_units_section_list = [["id", "content", "file_path"]]
1612
  for i, t in enumerate(use_text_units):
1613
+ text_units_section_list.append([i, t["content"], t.get("file_path", "unknown")])
 
 
1614
  text_units_context = list_of_list_to_csv(text_units_section_list)
1615
  return entities_context, relations_context, text_units_context
1616
 
 
2044
  Query response or async iterator
2045
  """
2046
  # Extract keywords
2047
+ hl_keywords, ll_keywords = await get_keywords_from_query(
2048
+ query=query,
2049
+ query_param=param,
2050
  global_config=global_config,
2051
  hashing_kv=hashing_kv,
2052
  )
2053
 
 
 
 
2054
  # Create a new string with the prompt and the keywords
2055
  ll_keywords_str = ", ".join(ll_keywords)
2056
  hl_keywords_str = ", ".join(hl_keywords)
lightrag/utils.py CHANGED
@@ -962,6 +962,13 @@ class TokenTracker:
962
  def __init__(self):
963
  self.reset()
964
 
 
 
 
 
 
 
 
965
  def reset(self):
966
  self.prompt_tokens = 0
967
  self.completion_tokens = 0
 
962
  def __init__(self):
963
  self.reset()
964
 
965
+ def __enter__(self):
966
+ self.reset()
967
+ return self
968
+
969
+ def __exit__(self, exc_type, exc_val, exc_tb):
970
+ print(self)
971
+
972
  def reset(self):
973
  self.prompt_tokens = 0
974
  self.completion_tokens = 0
lightrag_webui/src/hooks/useLightragGraph.tsx CHANGED
@@ -205,7 +205,7 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
205
  // Add edges from raw graph data
206
  for (const rawEdge of rawGraph?.edges ?? []) {
207
  rawEdge.dynamicId = graph.addDirectedEdge(rawEdge.source, rawEdge.target, {
208
- label: rawEdge.type || undefined
209
  })
210
  }
211
 
@@ -666,7 +666,7 @@ const useLightrangeGraph = () => {
666
 
667
  // Add the edge to the sigma graph
668
  newEdge.dynamicId = sigmaGraph.addDirectedEdge(newEdge.source, newEdge.target, {
669
- label: newEdge.type || undefined
670
  });
671
 
672
  // Add the edge to the raw graph
 
205
  // Add edges from raw graph data
206
  for (const rawEdge of rawGraph?.edges ?? []) {
207
  rawEdge.dynamicId = graph.addDirectedEdge(rawEdge.source, rawEdge.target, {
208
+ label: rawEdge.properties?.keywords || undefined
209
  })
210
  }
211
 
 
666
 
667
  // Add the edge to the sigma graph
668
  newEdge.dynamicId = sigmaGraph.addDirectedEdge(newEdge.source, newEdge.target, {
669
+ label: newEdge.properties?.keywords || undefined
670
  });
671
 
672
  // Add the edge to the raw graph