mirror of https://github.com/HKUDS/LightRAG.git
Compare commits
170 Commits
ae582f63a3
...
2f0aa7ed12
Author | SHA1 | Date |
---|---|---|
![]() |
2f0aa7ed12 | |
![]() |
a417f7c168 | |
![]() |
e00690b41b | |
![]() |
f6b90fe482 | |
![]() |
d98fe6f340 | |
![]() |
32af45ff46 | |
![]() |
3ae9c63b26 | |
![]() |
fdf051c234 | |
![]() |
790abf148b | |
![]() |
0f624b594a | |
![]() |
317d233486 | |
![]() |
b7ec76e548 | |
![]() |
043e1376b3 | |
![]() |
8271e1f6f1 | |
![]() |
364ae2340d | |
![]() |
9a8f58826d | |
![]() |
9d5603d35e | |
![]() |
3c530b21b6 | |
![]() |
9b63a89054 | |
![]() |
d3f1ea96de | |
![]() |
a59f8e7ceb | |
![]() |
c6bd9f0329 | |
![]() |
755a08edec | |
![]() |
5b8989e4d9 | |
![]() |
47002e645b | |
![]() |
5e2b262094 | |
![]() |
41de51a4db | |
![]() |
2af8a93dc7 | |
![]() |
d0bc5e7c4a | |
![]() |
5282312c64 | |
![]() |
78b4ff2d0b | |
![]() |
3e5efd0b27 | |
![]() |
a8f7e125c4 | |
![]() |
45f27fccc3 | |
![]() |
93dede163d | |
![]() |
08da46ac0f | |
![]() |
83dc672f21 | |
![]() |
0eac1a883a | |
![]() |
e60c26ea77 | |
![]() |
74eecc46e5 | |
![]() |
c24c2ff2f6 | |
![]() |
b456bb0942 | |
![]() |
a788899285 | |
![]() |
797dcc1ff1 | |
![]() |
50621d5a94 | |
![]() |
aba46213a7 | |
![]() |
c7bc4fc42c | |
![]() |
444593bda8 | |
![]() |
6d0a644844 | |
![]() |
1c35322088 | |
![]() |
6fa5a6f634 | |
![]() |
29e829113b | |
![]() |
30f71c8acf | |
![]() |
cfb7117dd6 | |
![]() |
5ec7eedf37 | |
![]() |
faa59cac72 | |
![]() |
cbaede8455 | |
![]() |
7207598fc4 | |
![]() |
75de799353 | |
![]() |
3ef3b8e155 | |
![]() |
6f958d5aee | |
![]() |
93afa7d8a7 | |
![]() |
7206c07468 | |
![]() |
1e1adcb64a | |
![]() |
6014b9bf73 | |
![]() |
dafdf92715 | |
![]() |
40a4cacee0 | |
![]() |
92bbb7a1b3 | |
![]() |
24c36d876c | |
![]() |
9c3e1505b5 | |
![]() |
8274ed52d1 | |
![]() |
c4544c23a7 | |
![]() |
c26dfa33de | |
![]() |
7a5df185a5 | |
![]() |
9923821d75 | |
![]() |
d26d413d97 | |
![]() |
f4c2dc327d | |
![]() |
75d1b1e9f8 | |
![]() |
645f81f7c8 | |
![]() |
9bdbdae120 | |
![]() |
bd94714b15 | |
![]() |
ee53e43568 | |
![]() |
84b09aa5da | |
![]() |
1613f4410e | |
![]() |
769f77ef8f | |
![]() |
958ed80b66 | |
![]() |
5aceca0052 | |
![]() |
98ac6fb3f0 | |
![]() |
f2ffff063b | |
![]() |
598eecd06d | |
![]() |
d0d57a45b6 | |
![]() |
9c4e98ec3b | |
![]() |
4eef9f3778 | |
![]() |
3951a44666 | |
![]() |
d70c584d80 | |
![]() |
7da485dd40 | |
![]() |
35734baa5c | |
![]() |
f2d051eea5 | |
![]() |
519e81aaeb | |
![]() |
3f5ade47cd | |
![]() |
2054bba7e6 | |
![]() |
e09929b42e | |
![]() |
f4bca7bfb2 | |
![]() |
a9565d7379 | |
![]() |
ebaff228aa | |
![]() |
358fbd689f | |
![]() |
99e3812c38 | |
![]() |
c6cfbee3e8 | |
![]() |
cf1ca39b3f | |
![]() |
0dfbce0bb4 | |
![]() |
055629d30d | |
![]() |
a67f93acc9 | |
![]() |
7b915b34f6 | |
![]() |
c8c3545454 | |
![]() |
8e7014d366 | |
![]() |
a943265257 | |
![]() |
6efa8ab263 | |
![]() |
2ed046171e | |
![]() |
e7baf54ec2 | |
![]() |
2c4f621ded | |
![]() |
56c3cb2dbe | |
![]() |
10b55dbdc7 | |
![]() |
912fc0fc31 | |
![]() |
b3c2987006 | |
![]() |
a49b7758e1 | |
![]() |
b4da3de7d9 | |
![]() |
55e2678a1e | |
![]() |
6a99d7ac28 | |
![]() |
93ce53394f | |
![]() |
4ae44bb24b | |
![]() |
ccfe2e73d1 | |
![]() |
983bacd87e | |
![]() |
bf58c73c3f | |
![]() |
33d986aa2a | |
![]() |
44b7ce222e | |
![]() |
51231c7647 | |
![]() |
733adb420d | |
![]() |
55ddc0ee86 | |
![]() |
29d1220f26 | |
![]() |
b1dd015e3e | |
![]() |
5437509824 | |
![]() |
f57ed21593 | |
![]() |
ac7d8560bd | |
![]() |
5574a30856 | |
![]() |
2e0a9dc73b | |
![]() |
d8e7b77099 | |
![]() |
2767212ba0 | |
![]() |
d979e9078f | |
![]() |
4f8de4ecdb | |
![]() |
edeb89abc6 | |
![]() |
795ce4cbe7 | |
![]() |
d78fda1d89 | |
![]() |
baad473b67 | |
![]() |
d97913873b | |
![]() |
3075691f72 | |
![]() |
2c940f0728 | |
![]() |
5a5d32dc32 | |
![]() |
42710221f5 | |
![]() |
02f79508e0 | |
![]() |
0009a37efb | |
![]() |
958151e610 | |
![]() |
7d96ca98f7 | |
![]() |
00d7bc80bf | |
![]() |
75fd6c73ea | |
![]() |
6cc9411c86 | |
![]() |
2d41e5313a | |
![]() |
75beaf249e | |
![]() |
f115661e16 | |
![]() |
681d43bb32 | |
![]() |
ce9dac9bcf |
60
README-zh.md
60
README-zh.md
|
@ -265,14 +265,13 @@ if __name__ == "__main__":
|
|||
| **embedding_func_max_async** | `int` | 最大并发异步嵌入进程数 | `16` |
|
||||
| **llm_model_func** | `callable` | LLM生成的函数 | `gpt_4o_mini_complete` |
|
||||
| **llm_model_name** | `str` | 用于生成的LLM模型名称 | `meta-llama/Llama-3.2-1B-Instruct` |
|
||||
| **llm_model_max_token_size** | `int` | 生成实体关系摘要时送给LLM的最大令牌数 | `32000`(默认值由环境变量MAX_TOKENS更改) |
|
||||
| **summary_max_tokens** | `int` | 生成实体关系摘要时送给LLM的最大令牌数 | `32000`(默认值由环境变量MAX_TOKENS更改) |
|
||||
| **llm_model_max_async** | `int` | 最大并发异步LLM进程数 | `4`(默认值由环境变量MAX_ASYNC更改) |
|
||||
| **llm_model_kwargs** | `dict` | LLM生成的附加参数 | |
|
||||
| **vector_db_storage_cls_kwargs** | `dict` | 向量数据库的附加参数,如设置节点和关系检索的阈值 | cosine_better_than_threshold: 0.2(默认值由环境变量COSINE_THRESHOLD更改) |
|
||||
| **enable_llm_cache** | `bool` | 如果为`TRUE`,将LLM结果存储在缓存中;重复的提示返回缓存的响应 | `TRUE` |
|
||||
| **enable_llm_cache_for_entity_extract** | `bool` | 如果为`TRUE`,将实体提取的LLM结果存储在缓存中;适合初学者调试应用程序 | `TRUE` |
|
||||
| **addon_params** | `dict` | 附加参数,例如`{"example_number": 1, "language": "Simplified Chinese", "entity_types": ["organization", "person", "geo", "event"]}`:设置示例限制、输出语言和文档处理的批量大小 | `example_number: 所有示例, language: English` |
|
||||
| **convert_response_to_json_func** | `callable` | 未使用 | `convert_response_to_json` |
|
||||
| **embedding_cache_config** | `dict` | 问答缓存的配置。包含三个参数:`enabled`:布尔值,启用/禁用缓存查找功能。启用时,系统将在生成新答案之前检查缓存的响应。`similarity_threshold`:浮点值(0-1),相似度阈值。当新问题与缓存问题的相似度超过此阈值时,将直接返回缓存的答案而不调用LLM。`use_llm_check`:布尔值,启用/禁用LLM相似度验证。启用时,在返回缓存答案之前,将使用LLM作为二次检查来验证问题之间的相似度。 | 默认:`{"enabled": False, "similarity_threshold": 0.95, "use_llm_check": False}` |
|
||||
|
||||
</details>
|
||||
|
@ -320,7 +319,7 @@ class QueryParam:
|
|||
max_relation_tokens: int = int(os.getenv("MAX_RELATION_TOKENS", "10000"))
|
||||
"""Maximum number of tokens allocated for relationship context in unified token control system."""
|
||||
|
||||
max_total_tokens: int = int(os.getenv("MAX_TOTAL_TOKENS", "32000"))
|
||||
max_total_tokens: int = int(os.getenv("MAX_TOTAL_TOKENS", "30000"))
|
||||
"""Maximum total tokens budget for the entire query context (entities + relations + chunks + system prompt)."""
|
||||
|
||||
hl_keywords: list[str] = field(default_factory=list)
|
||||
|
@ -334,7 +333,8 @@ class QueryParam:
|
|||
Format: [{"role": "user/assistant", "content": "message"}].
|
||||
"""
|
||||
|
||||
history_turns: int = 3
|
||||
# Deprated: history message have negtive effect on query performance
|
||||
history_turns: int = 0
|
||||
"""Number of complete conversation turns (user-assistant pairs) to consider in the response context."""
|
||||
|
||||
ids: list[str] | None = None
|
||||
|
@ -396,7 +396,6 @@ async def initialize_rag():
|
|||
llm_model_func=llm_model_func,
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=4096,
|
||||
max_token_size=8192,
|
||||
func=embedding_func
|
||||
)
|
||||
)
|
||||
|
@ -425,7 +424,6 @@ rag = LightRAG(
|
|||
# 使用Hugging Face嵌入函数
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=384,
|
||||
max_token_size=5000,
|
||||
func=lambda texts: hf_embed(
|
||||
texts,
|
||||
tokenizer=AutoTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2"),
|
||||
|
@ -452,7 +450,6 @@ rag = LightRAG(
|
|||
# 使用Ollama嵌入函数
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=768,
|
||||
max_token_size=8192,
|
||||
func=lambda texts: ollama_embed(
|
||||
texts,
|
||||
embed_model="nomic-embed-text"
|
||||
|
@ -504,7 +501,6 @@ rag = LightRAG(
|
|||
# 使用Ollama嵌入函数
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=768,
|
||||
max_token_size=8192,
|
||||
func=lambda texts: ollama_embed(
|
||||
texts,
|
||||
embed_model="nomic-embed-text"
|
||||
|
@ -547,7 +543,6 @@ async def initialize_rag():
|
|||
llm_model_func=llama_index_complete_if_cache, # LlamaIndex兼容的完成函数
|
||||
embedding_func=EmbeddingFunc( # LlamaIndex兼容的嵌入函数
|
||||
embedding_dim=1536,
|
||||
max_token_size=8192,
|
||||
func=lambda texts: llama_index_embed(texts, embed_model=embed_model)
|
||||
),
|
||||
)
|
||||
|
@ -690,7 +685,7 @@ rag.insert(["文本1", "文本2",...], ids=["文本1的ID", "文本2的ID"])
|
|||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>使用管道插入</b></summary>
|
||||
<summary><b>使用流水线插入</b></summary>
|
||||
|
||||
`apipeline_enqueue_documents`和`apipeline_process_enqueue_documents`函数允许您对文档进行增量插入到图中。
|
||||
|
||||
|
@ -786,7 +781,6 @@ async def initialize_rag():
|
|||
<summary> <b>使用Faiss进行存储</b> </summary>
|
||||
在使用Faiss向量数据库之前必须手工安装`faiss-cpu`或`faiss-gpu`。
|
||||
|
||||
|
||||
- 安装所需依赖:
|
||||
|
||||
```
|
||||
|
@ -809,7 +803,6 @@ rag = LightRAG(
|
|||
llm_model_func=llm_model_func,
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=384,
|
||||
max_token_size=8192,
|
||||
func=embedding_func,
|
||||
),
|
||||
vector_storage="FaissVectorDBStorage",
|
||||
|
@ -824,51 +817,13 @@ rag = LightRAG(
|
|||
<details>
|
||||
<summary> <b>使用PostgreSQL进行存储</b> </summary>
|
||||
|
||||
对于生产级场景,您很可能想要利用企业级解决方案。PostgreSQL可以为您提供一站式解决方案,作为KV存储、向量数据库(pgvector)和图数据库(apache AGE)。
|
||||
对于生产级场景,您很可能想要利用企业级解决方案。PostgreSQL可以为您提供一站式解决方案,作为KV存储、向量数据库(pgvector)和图数据库(apache AGE)。支持 PostgreSQL 版本为16.6或以上。
|
||||
|
||||
* PostgreSQL很轻量,整个二进制发行版包括所有必要的插件可以压缩到40MB:参考[Windows发布版](https://github.com/ShanGor/apache-age-windows/releases/tag/PG17%2Fv1.5.0-rc0),它在Linux/Mac上也很容易安装。
|
||||
* 如果您是初学者并想避免麻烦,推荐使用docker,请从这个镜像开始(请务必阅读概述):https://hub.docker.com/r/shangor/postgres-for-rag
|
||||
* 如何开始?参考:[examples/lightrag_zhipu_postgres_demo.py](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_zhipu_postgres_demo.py)
|
||||
* 为AGE创建索引示例:(如有必要,将下面的`dickens`改为您的图名)
|
||||
```sql
|
||||
load 'age';
|
||||
SET search_path = ag_catalog, "$user", public;
|
||||
CREATE INDEX CONCURRENTLY entity_p_idx ON dickens."Entity" (id);
|
||||
CREATE INDEX CONCURRENTLY vertex_p_idx ON dickens."_ag_label_vertex" (id);
|
||||
CREATE INDEX CONCURRENTLY directed_p_idx ON dickens."DIRECTED" (id);
|
||||
CREATE INDEX CONCURRENTLY directed_eid_idx ON dickens."DIRECTED" (end_id);
|
||||
CREATE INDEX CONCURRENTLY directed_sid_idx ON dickens."DIRECTED" (start_id);
|
||||
CREATE INDEX CONCURRENTLY directed_seid_idx ON dickens."DIRECTED" (start_id,end_id);
|
||||
CREATE INDEX CONCURRENTLY edge_p_idx ON dickens."_ag_label_edge" (id);
|
||||
CREATE INDEX CONCURRENTLY edge_sid_idx ON dickens."_ag_label_edge" (start_id);
|
||||
CREATE INDEX CONCURRENTLY edge_eid_idx ON dickens."_ag_label_edge" (end_id);
|
||||
CREATE INDEX CONCURRENTLY edge_seid_idx ON dickens."_ag_label_edge" (start_id,end_id);
|
||||
create INDEX CONCURRENTLY vertex_idx_node_id ON dickens."_ag_label_vertex" (ag_catalog.agtype_access_operator(properties, '"node_id"'::agtype));
|
||||
create INDEX CONCURRENTLY entity_idx_node_id ON dickens."Entity" (ag_catalog.agtype_access_operator(properties, '"node_id"'::agtype));
|
||||
CREATE INDEX CONCURRENTLY entity_node_id_gin_idx ON dickens."Entity" using gin(properties);
|
||||
ALTER TABLE dickens."DIRECTED" CLUSTER ON directed_sid_idx;
|
||||
|
||||
-- 如有必要可以删除
|
||||
drop INDEX entity_p_idx;
|
||||
drop INDEX vertex_p_idx;
|
||||
drop INDEX directed_p_idx;
|
||||
drop INDEX directed_eid_idx;
|
||||
drop INDEX directed_sid_idx;
|
||||
drop INDEX directed_seid_idx;
|
||||
drop INDEX edge_p_idx;
|
||||
drop INDEX edge_sid_idx;
|
||||
drop INDEX edge_eid_idx;
|
||||
drop INDEX edge_seid_idx;
|
||||
drop INDEX vertex_idx_node_id;
|
||||
drop INDEX entity_idx_node_id;
|
||||
drop INDEX entity_node_id_gin_idx;
|
||||
```
|
||||
* Apache AGE的已知问题:发布版本存在以下问题:
|
||||
> 您可能会发现节点/边的属性是空的。
|
||||
> 这是发布版本的已知问题:https://github.com/apache/age/pull/1721
|
||||
>
|
||||
> 您可以从源代码编译AGE来修复它。
|
||||
>
|
||||
* Apache AGE的性能不如Neo4j。最求高性能的图数据库请使用Noe4j。
|
||||
|
||||
</details>
|
||||
|
||||
|
@ -1229,7 +1184,6 @@ LightRAG 现已与 [RAG-Anything](https://github.com/HKUDS/RAG-Anything) 实现
|
|||
),
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=3072,
|
||||
max_token_size=8192,
|
||||
func=lambda texts: openai_embed(
|
||||
texts,
|
||||
model="text-embedding-3-large",
|
||||
|
|
70
README.md
70
README.md
|
@ -218,12 +218,12 @@ async def main():
|
|||
try:
|
||||
# Initialize RAG instance
|
||||
rag = await initialize_rag()
|
||||
rag.insert("Your text")
|
||||
await rag.ainsert("Your text")
|
||||
|
||||
# Perform hybrid search
|
||||
mode = "hybrid"
|
||||
print(
|
||||
await rag.query(
|
||||
await rag.aquery(
|
||||
"What are the top themes in this story?",
|
||||
param=QueryParam(mode=mode)
|
||||
)
|
||||
|
@ -272,14 +272,13 @@ A full list of LightRAG init parameters:
|
|||
| **embedding_func_max_async** | `int` | Maximum number of concurrent asynchronous embedding processes | `16` |
|
||||
| **llm_model_func** | `callable` | Function for LLM generation | `gpt_4o_mini_complete` |
|
||||
| **llm_model_name** | `str` | LLM model name for generation | `meta-llama/Llama-3.2-1B-Instruct` |
|
||||
| **llm_model_max_token_size** | `int` | Maximum tokens send to LLM to generate entity relation summaries | `32000`(default value changed by env var MAX_TOKENS) |
|
||||
| **summary_max_tokens** | `int` | Maximum tokens send to LLM to generate entity relation summaries | `32000`(default value changed by env var MAX_TOKENS) |
|
||||
| **llm_model_max_async** | `int` | Maximum number of concurrent asynchronous LLM processes | `4`(default value changed by env var MAX_ASYNC) |
|
||||
| **llm_model_kwargs** | `dict` | Additional parameters for LLM generation | |
|
||||
| **vector_db_storage_cls_kwargs** | `dict` | Additional parameters for vector database, like setting the threshold for nodes and relations retrieval | cosine_better_than_threshold: 0.2(default value changed by env var COSINE_THRESHOLD) |
|
||||
| **enable_llm_cache** | `bool` | If `TRUE`, stores LLM results in cache; repeated prompts return cached responses | `TRUE` |
|
||||
| **enable_llm_cache_for_entity_extract** | `bool` | If `TRUE`, stores LLM results in cache for entity extraction; Good for beginners to debug your application | `TRUE` |
|
||||
| **addon_params** | `dict` | Additional parameters, e.g., `{"example_number": 1, "language": "Simplified Chinese", "entity_types": ["organization", "person", "geo", "event"]}`: sets example limit, entiy/relation extraction output language | `example_number: all examples, language: English` |
|
||||
| **convert_response_to_json_func** | `callable` | Not used | `convert_response_to_json` |
|
||||
| **embedding_cache_config** | `dict` | Configuration for question-answer caching. Contains three parameters: `enabled`: Boolean value to enable/disable cache lookup functionality. When enabled, the system will check cached responses before generating new answers. `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. `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}` |
|
||||
|
||||
</details>
|
||||
|
@ -327,7 +326,7 @@ class QueryParam:
|
|||
max_relation_tokens: int = int(os.getenv("MAX_RELATION_TOKENS", "10000"))
|
||||
"""Maximum number of tokens allocated for relationship context in unified token control system."""
|
||||
|
||||
max_total_tokens: int = int(os.getenv("MAX_TOTAL_TOKENS", "32000"))
|
||||
max_total_tokens: int = int(os.getenv("MAX_TOTAL_TOKENS", "30000"))
|
||||
"""Maximum total tokens budget for the entire query context (entities + relations + chunks + system prompt)."""
|
||||
|
||||
conversation_history: list[dict[str, str]] = field(default_factory=list)
|
||||
|
@ -335,7 +334,8 @@ class QueryParam:
|
|||
Format: [{"role": "user/assistant", "content": "message"}].
|
||||
"""
|
||||
|
||||
history_turns: int = 3
|
||||
# Deprated: history message have negtive effect on query performance
|
||||
history_turns: int = 0
|
||||
"""Number of complete conversation turns (user-assistant pairs) to consider in the response context."""
|
||||
|
||||
ids: list[str] | None = None
|
||||
|
@ -397,7 +397,6 @@ async def initialize_rag():
|
|||
llm_model_func=llm_model_func,
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=4096,
|
||||
max_token_size=8192,
|
||||
func=embedding_func
|
||||
)
|
||||
)
|
||||
|
@ -426,7 +425,6 @@ rag = LightRAG(
|
|||
# Use Hugging Face embedding function
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=384,
|
||||
max_token_size=5000,
|
||||
func=lambda texts: hf_embed(
|
||||
texts,
|
||||
tokenizer=AutoTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2"),
|
||||
|
@ -455,7 +453,6 @@ rag = LightRAG(
|
|||
# Use Ollama embedding function
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=768,
|
||||
max_token_size=8192,
|
||||
func=lambda texts: ollama_embed(
|
||||
texts,
|
||||
embed_model="nomic-embed-text"
|
||||
|
@ -507,7 +504,6 @@ rag = LightRAG(
|
|||
# Use Ollama embedding function
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=768,
|
||||
max_token_size=8192,
|
||||
func=lambda texts: ollama_embed(
|
||||
texts,
|
||||
embed_model="nomic-embed-text"
|
||||
|
@ -550,7 +546,6 @@ async def initialize_rag():
|
|||
llm_model_func=llama_index_complete_if_cache, # LlamaIndex-compatible completion function
|
||||
embedding_func=EmbeddingFunc( # LlamaIndex-compatible embedding function
|
||||
embedding_dim=1536,
|
||||
max_token_size=8192,
|
||||
func=lambda texts: llama_index_embed(texts, embed_model=embed_model)
|
||||
),
|
||||
)
|
||||
|
@ -619,7 +614,6 @@ conversation_history = [
|
|||
query_param = QueryParam(
|
||||
mode="mix", # or any other mode: "local", "global", "hybrid"
|
||||
conversation_history=conversation_history, # Add the conversation history
|
||||
history_turns=3 # Number of recent conversation turns to consider
|
||||
)
|
||||
|
||||
# Make a query that takes into account the conversation history
|
||||
|
@ -797,58 +791,18 @@ see test_neo4j.py for a working example.
|
|||
<details>
|
||||
<summary> <b>Using PostgreSQL for Storage</b> </summary>
|
||||
|
||||
For production level scenarios you will most likely want to leverage an enterprise solution. PostgreSQL can provide a one-stop solution for you as KV store, VectorDB (pgvector) and GraphDB (apache AGE).
|
||||
For production level scenarios you will most likely want to leverage an enterprise solution. PostgreSQL can provide a one-stop solution for you as KV store, VectorDB (pgvector) and GraphDB (apache AGE). PostgreSQL version 16.6 or higher is supported.
|
||||
|
||||
* PostgreSQL is lightweight,the whole binary distribution including all necessary plugins can be zipped to 40MB: Ref to [Windows Release](https://github.com/ShanGor/apache-age-windows/releases/tag/PG17%2Fv1.5.0-rc0) as it is easy to install for Linux/Mac.
|
||||
* If you prefer docker, please start with this image if you are a beginner to avoid hiccups (DO read the overview): https://hub.docker.com/r/shangor/postgres-for-rag
|
||||
* How to start? Ref to: [examples/lightrag_zhipu_postgres_demo.py](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_zhipu_postgres_demo.py)
|
||||
* Create index for AGE example: (Change below `dickens` to your graph name if necessary)
|
||||
```sql
|
||||
load 'age';
|
||||
SET search_path = ag_catalog, "$user", public;
|
||||
CREATE INDEX CONCURRENTLY entity_p_idx ON dickens."Entity" (id);
|
||||
CREATE INDEX CONCURRENTLY vertex_p_idx ON dickens."_ag_label_vertex" (id);
|
||||
CREATE INDEX CONCURRENTLY directed_p_idx ON dickens."DIRECTED" (id);
|
||||
CREATE INDEX CONCURRENTLY directed_eid_idx ON dickens."DIRECTED" (end_id);
|
||||
CREATE INDEX CONCURRENTLY directed_sid_idx ON dickens."DIRECTED" (start_id);
|
||||
CREATE INDEX CONCURRENTLY directed_seid_idx ON dickens."DIRECTED" (start_id,end_id);
|
||||
CREATE INDEX CONCURRENTLY edge_p_idx ON dickens."_ag_label_edge" (id);
|
||||
CREATE INDEX CONCURRENTLY edge_sid_idx ON dickens."_ag_label_edge" (start_id);
|
||||
CREATE INDEX CONCURRENTLY edge_eid_idx ON dickens."_ag_label_edge" (end_id);
|
||||
CREATE INDEX CONCURRENTLY edge_seid_idx ON dickens."_ag_label_edge" (start_id,end_id);
|
||||
create INDEX CONCURRENTLY vertex_idx_node_id ON dickens."_ag_label_vertex" (ag_catalog.agtype_access_operator(properties, '"node_id"'::agtype));
|
||||
create INDEX CONCURRENTLY entity_idx_node_id ON dickens."Entity" (ag_catalog.agtype_access_operator(properties, '"node_id"'::agtype));
|
||||
CREATE INDEX CONCURRENTLY entity_node_id_gin_idx ON dickens."Entity" using gin(properties);
|
||||
ALTER TABLE dickens."DIRECTED" CLUSTER ON directed_sid_idx;
|
||||
|
||||
-- drop if necessary
|
||||
drop INDEX entity_p_idx;
|
||||
drop INDEX vertex_p_idx;
|
||||
drop INDEX directed_p_idx;
|
||||
drop INDEX directed_eid_idx;
|
||||
drop INDEX directed_sid_idx;
|
||||
drop INDEX directed_seid_idx;
|
||||
drop INDEX edge_p_idx;
|
||||
drop INDEX edge_sid_idx;
|
||||
drop INDEX edge_eid_idx;
|
||||
drop INDEX edge_seid_idx;
|
||||
drop INDEX vertex_idx_node_id;
|
||||
drop INDEX entity_idx_node_id;
|
||||
drop INDEX entity_node_id_gin_idx;
|
||||
```
|
||||
* Known issue of the Apache AGE: The released versions got below issue:
|
||||
> You might find that the properties of the nodes/edges are empty.
|
||||
> It is a known issue of the release version: https://github.com/apache/age/pull/1721
|
||||
>
|
||||
> You can Compile the AGE from source code and fix it.
|
||||
>
|
||||
* For high-performance graph database requirements, Neo4j is recommended as Apache AGE's performance is not as competitive.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> <b>Using Faiss for Storage</b> </summary>
|
||||
You must manually install faiss-cpu or faiss-gpu before using FAISS vector db.
|
||||
Manually install `faiss-cpu` or `faiss-gpu` before using FAISS vector db.
|
||||
Before using Faiss vector database, you must manually install `faiss-cpu` or `faiss-gpu`.
|
||||
|
||||
- Install the required dependencies:
|
||||
|
||||
|
@ -872,7 +826,6 @@ rag = LightRAG(
|
|||
llm_model_func=llm_model_func,
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=384,
|
||||
max_token_size=8192,
|
||||
func=embedding_func,
|
||||
),
|
||||
vector_storage="FaissVectorDBStorage",
|
||||
|
@ -1278,7 +1231,6 @@ LightRAG now seamlessly integrates with [RAG-Anything](https://github.com/HKUDS/
|
|||
),
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=3072,
|
||||
max_token_size=8192,
|
||||
func=lambda texts: openai_embed(
|
||||
texts,
|
||||
model="text-embedding-3-large",
|
||||
|
@ -1287,10 +1239,8 @@ LightRAG now seamlessly integrates with [RAG-Anything](https://github.com/HKUDS/
|
|||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Initialize storage (this will load existing data if available)
|
||||
await lightrag_instance.initialize_storages()
|
||||
|
||||
# Now initialize RAGAnything with the existing LightRAG instance
|
||||
rag = RAGAnything(
|
||||
lightrag=lightrag_instance, # Pass the existing LightRAG instance
|
||||
|
@ -1319,14 +1269,12 @@ LightRAG now seamlessly integrates with [RAG-Anything](https://github.com/HKUDS/
|
|||
)
|
||||
# Note: working_dir, llm_model_func, embedding_func, etc. are inherited from lightrag_instance
|
||||
)
|
||||
|
||||
# Query the existing knowledge base
|
||||
result = await rag.query_with_multimodal(
|
||||
"What data has been processed in this LightRAG instance?",
|
||||
mode="hybrid"
|
||||
)
|
||||
print("Query result:", result)
|
||||
|
||||
# Add new multimodal documents to the existing LightRAG instance
|
||||
await rag.process_document_complete(
|
||||
file_path="path/to/new/multimodal_document.pdf",
|
||||
|
|
|
@ -84,7 +84,6 @@ LightRAG can be configured using environment variables in the `.env` file:
|
|||
- `MAX_ASYNC`: Maximum async operations
|
||||
- `MAX_TOKENS`: Maximum token size
|
||||
- `EMBEDDING_DIM`: Embedding dimensions
|
||||
- `MAX_EMBED_TOKENS`: Maximum embedding token size
|
||||
|
||||
#### Security
|
||||
- `LIGHTRAG_API_KEY`: API key for authentication
|
||||
|
|
|
@ -6,7 +6,7 @@ LightRAG employs a multi-layered concurrent control strategy when processing mul
|
|||
|
||||
**Control Parameter**: `max_parallel_insert`
|
||||
|
||||
This parameter controls the number of documents processed simultaneously. The purpose is to prevent excessive parallelism from overwhelming system resources, which could lead to extended processing times for individual files. Document-level concurrency is governed by the `max_parallel_insert` attribute within LightRAG, which defaults to 2 and is configurable via the `MAX_PARALLEL_INSERT` environment variable.
|
||||
This parameter controls the number of documents processed simultaneously. The purpose is to prevent excessive parallelism from overwhelming system resources, which could lead to extended processing times for individual files. Document-level concurrency is governed by the `max_parallel_insert` attribute within LightRAG, which defaults to 2 and is configurable via the `MAX_PARALLEL_INSERT` environment variable. `max_parallel_insert` is recommended to be set between 2 and 10, typically `llm_model_max_async/3`. Setting this value too high can increase the likelihood of naming conflicts among entities and relationships across different documents during the merge phase, thereby reducing its overall efficiency.
|
||||
|
||||
### 2. Chunk-Level Concurrent Control
|
||||
|
||||
|
|
89
env.example
89
env.example
|
@ -7,7 +7,6 @@ HOST=0.0.0.0
|
|||
PORT=9621
|
||||
WEBUI_TITLE='My Graph KB'
|
||||
WEBUI_DESCRIPTION="Simple and Fast Graph Based RAG System"
|
||||
OLLAMA_EMULATING_MODEL_TAG=latest
|
||||
# WORKERS=2
|
||||
# CORS_ORIGINS=http://localhost:3000,http://localhost:8080
|
||||
|
||||
|
@ -21,6 +20,10 @@ OLLAMA_EMULATING_MODEL_TAG=latest
|
|||
# INPUT_DIR=<absolute_path_for_doc_input_dir>
|
||||
# WORKING_DIR=<absolute_path_for_working_dir>
|
||||
|
||||
### Ollama Emulating Model and Tag
|
||||
# OLLAMA_EMULATING_MODEL_NAME=lightrag
|
||||
OLLAMA_EMULATING_MODEL_TAG=latest
|
||||
|
||||
### Max nodes return from grap retrieval in webui
|
||||
# MAX_GRAPH_NODES=1000
|
||||
|
||||
|
@ -45,30 +48,36 @@ OLLAMA_EMULATING_MODEL_TAG=latest
|
|||
# LIGHTRAG_API_KEY=your-secure-api-key-here
|
||||
# WHITELIST_PATHS=/health,/api/*
|
||||
|
||||
########################
|
||||
######################################################################################
|
||||
### Query Configuration
|
||||
########################
|
||||
# LLM responde cache for query (Not valid for streaming response
|
||||
###
|
||||
### How to control the context lenght sent to LLM:
|
||||
### MAX_ENTITY_TOKENS + MAX_RELATION_TOKENS < MAX_TOTAL_TOKENS
|
||||
### Chunk_Tokens = MAX_TOTAL_TOKENS - Actual_Entity_Tokens - Actual_Reation_Tokens
|
||||
######################################################################################
|
||||
# LLM responde cache for query (Not valid for streaming response)
|
||||
ENABLE_LLM_CACHE=true
|
||||
# HISTORY_TURNS=0
|
||||
# COSINE_THRESHOLD=0.2
|
||||
### Number of entities or relations retrieved from KG
|
||||
# TOP_K=40
|
||||
### Maxmium number or chunks plan to send to LLM
|
||||
### Maxmium number or chunks for naive vactor search
|
||||
# CHUNK_TOP_K=10
|
||||
### control the actual enties send to LLM
|
||||
# MAX_ENTITY_TOKENS=10000
|
||||
### control the actual relations send to LLM
|
||||
# MAX_RELATION_TOKENS=10000
|
||||
### control the maximum tokens send to LLM (include entities, raltions and chunks)
|
||||
# MAX_TOTAL_TOKENS=32000
|
||||
### maxumium related chunks grab from single entity or relations
|
||||
# RELATED_CHUNK_NUMBER=10
|
||||
# MAX_TOTAL_TOKENS=30000
|
||||
### maximum number of related chunks per source entity or relation (higher values increase re-ranking time)
|
||||
# RELATED_CHUNK_NUMBER=5
|
||||
|
||||
### Reranker configuration (Set ENABLE_RERANK to true in reranking model is configed)
|
||||
ENABLE_RERANK=False
|
||||
# RERANK_MODEL=BAAI/bge-reranker-v2-m3
|
||||
# RERANK_BINDING_HOST=https://api.your-rerank-provider.com/v1/rerank
|
||||
# ENABLE_RERANK=True
|
||||
### Minimum rerank score for document chunk exclusion (set to 0.0 to keep all chunks, 0.6 or above if LLM is not strong enought)
|
||||
# MIN_RERANK_SCORE=0.0
|
||||
### Rerank model configuration (required when ENABLE_RERANK=True)
|
||||
# RERANK_MODEL=jina-reranker-v2-base-multilingual
|
||||
# RERANK_BINDING_HOST=https://api.jina.ai/v1/rerank
|
||||
# RERANK_BINDING_API_KEY=your_rerank_api_key_here
|
||||
|
||||
########################################
|
||||
|
@ -77,14 +86,13 @@ ENABLE_RERANK=False
|
|||
### Language: English, Chinese, French, German ...
|
||||
SUMMARY_LANGUAGE=English
|
||||
ENABLE_LLM_CACHE_FOR_EXTRACT=true
|
||||
### MAX_TOKENS: max tokens send to LLM for entity relation summaries (less than context size of the model)
|
||||
MAX_TOKENS=32000
|
||||
### Chunk size for document splitting, 500~1500 is recommended
|
||||
# CHUNK_SIZE=1200
|
||||
# CHUNK_OVERLAP_SIZE=100
|
||||
### Entity and relation summarization configuration
|
||||
### Number of duplicated entities/edges to trigger LLM re-summary on merge ( at least 3 is recommented)
|
||||
### Number of duplicated entities/edges to trigger LLM re-summary on merge (at least 3 is recommented), and max tokens send to LLM
|
||||
# FORCE_LLM_SUMMARY_ON_MERGE=4
|
||||
# MAX_TOKENS=10000
|
||||
### Maximum number of entity extraction attempts for ambiguous content
|
||||
# MAX_GLEANING=1
|
||||
|
||||
|
@ -93,7 +101,7 @@ MAX_TOKENS=32000
|
|||
###############################
|
||||
### Max concurrency requests of LLM (for both query and document processing)
|
||||
MAX_ASYNC=4
|
||||
### Number of parallel processing documents(between 2~10, MAX_ASYNC/4 is recommended)
|
||||
### Number of parallel processing documents(between 2~10, MAX_ASYNC/3 is recommended)
|
||||
MAX_PARALLEL_INSERT=2
|
||||
### Max concurrency requests for Embedding
|
||||
# EMBEDDING_FUNC_MAX_ASYNC=8
|
||||
|
@ -103,18 +111,21 @@ MAX_PARALLEL_INSERT=2
|
|||
#######################
|
||||
### LLM Configuration
|
||||
#######################
|
||||
### Time out in seconds for LLM, None for infinite timeout
|
||||
TIMEOUT=240
|
||||
### Some models like o1-mini require temperature to be set to 1
|
||||
TEMPERATURE=0
|
||||
### Some models like o1-mini require temperature to be set to 1, some LLM can fall into output loops with low temperature
|
||||
# TEMPERATURE=1.0
|
||||
|
||||
### LLM Binding type: openai, ollama, lollms, azure_openai
|
||||
LLM_BINDING=openai
|
||||
LLM_MODEL=gpt-4o
|
||||
LLM_BINDING_HOST=https://api.openai.com/v1
|
||||
LLM_BINDING_API_KEY=your_api_key
|
||||
|
||||
### Set as num_ctx option for Ollama LLM
|
||||
# OLLAMA_NUM_CTX=32768
|
||||
### Most Commont Parameters for Ollama Server
|
||||
### see also env.ollama-binding-options.example for fine tuning ollama
|
||||
### OLLAMA_LLM_NUM_CTX must be larger than MAX_TOTAL_TOKENS + 2000
|
||||
# OLLAMA_LLM_NUM_CTX=32768
|
||||
### Time out in seconds, None for infinite timeout
|
||||
TIMEOUT=240
|
||||
|
||||
### Optional for Azure
|
||||
# AZURE_OPENAI_API_VERSION=2024-08-01-preview
|
||||
|
@ -123,15 +134,22 @@ LLM_BINDING_API_KEY=your_api_key
|
|||
####################################################################################
|
||||
### Embedding Configuration (Should not be changed after the first file processed)
|
||||
####################################################################################
|
||||
### Embedding Binding type: openai, ollama, lollms, azure_openai
|
||||
### Embedding Binding type: openai, ollama, lollms, azure_openai, jina
|
||||
|
||||
### see also env.ollama-binding-options.example for fine tuning ollama
|
||||
EMBEDDING_BINDING=ollama
|
||||
EMBEDDING_MODEL=bge-m3:latest
|
||||
EMBEDDING_DIM=1024
|
||||
EMBEDDING_BINDING_API_KEY=your_api_key
|
||||
# If the embedding service is deployed within the same Docker stack, use host.docker.internal instead of localhost
|
||||
EMBEDDING_BINDING_HOST=http://localhost:11434
|
||||
### Maximum tokens sent to Embedding for each chunk (no longer in use?)
|
||||
# MAX_EMBED_TOKENS=8192
|
||||
|
||||
### OpenAI compatible
|
||||
# EMBEDDING_BINDING=openai
|
||||
# EMBEDDING_MODEL=text-embedding-3-large
|
||||
# EMBEDDING_DIM=3072
|
||||
# EMBEDDING_BINDING_HOST=https://api.openai.com
|
||||
# EMBEDDING_BINDING_API_KEY=your_api_key
|
||||
|
||||
### Optional for Azure
|
||||
# AZURE_EMBEDDING_DEPLOYMENT=text-embedding-3-large
|
||||
|
@ -139,6 +157,20 @@ EMBEDDING_BINDING_HOST=http://localhost:11434
|
|||
# AZURE_EMBEDDING_ENDPOINT=your_endpoint
|
||||
# AZURE_EMBEDDING_API_KEY=your_api_key
|
||||
|
||||
### Jina AI Embedding
|
||||
EMBEDDING_BINDING=jina
|
||||
EMBEDDING_BINDING_HOST=https://api.jina.ai/v1/embeddings
|
||||
EMBEDDING_MODEL=jina-embeddings-v4
|
||||
EMBEDDING_DIM=2048
|
||||
EMBEDDING_BINDING_API_KEY=your_api_key
|
||||
|
||||
####################################################################
|
||||
### WORKSPACE setting workspace name for all storage types
|
||||
### in the purpose of isolating data from LightRAG instances.
|
||||
### Valid workspace name constraints: a-z, A-Z, 0-9, and _
|
||||
####################################################################
|
||||
# WORKSPACE=space1
|
||||
|
||||
############################
|
||||
### Data storage selection
|
||||
############################
|
||||
|
@ -173,13 +205,6 @@ EMBEDDING_BINDING_HOST=http://localhost:11434
|
|||
# LIGHTRAG_GRAPH_STORAGE=MongoGraphStorage
|
||||
# LIGHTRAG_VECTOR_STORAGE=MongoVectorDBStorage
|
||||
|
||||
####################################################################
|
||||
### WORKSPACE setting workspace name for all storage types
|
||||
### in the purpose of isolating data from LightRAG instances.
|
||||
### Valid workspace name constraints: a-z, A-Z, 0-9, and _
|
||||
####################################################################
|
||||
# WORKSPACE=space1
|
||||
|
||||
### PostgreSQL Configuration
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_PORT=5432
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
################################################################################
|
||||
# Autogenerated .env entries list for LightRAG binding options
|
||||
#
|
||||
# To generate run:
|
||||
# $ python -m lightrag.llm.binding_options
|
||||
################################################################################
|
||||
# ollama_embedding -- Context window size (number of tokens)
|
||||
# OLLAMA_EMBEDDING_NUM_CTX=4096
|
||||
|
||||
# ollama_embedding -- Maximum number of tokens to predict
|
||||
# OLLAMA_EMBEDDING_NUM_PREDICT=128
|
||||
|
||||
# ollama_embedding -- Number of tokens to keep from the initial prompt
|
||||
# OLLAMA_EMBEDDING_NUM_KEEP=0
|
||||
|
||||
# ollama_embedding -- Random seed for generation (-1 for random)
|
||||
# OLLAMA_EMBEDDING_SEED=-1
|
||||
|
||||
# ollama_embedding -- Controls randomness (0.0-2.0, higher = more creative)
|
||||
# OLLAMA_EMBEDDING_TEMPERATURE=0.8
|
||||
|
||||
# ollama_embedding -- Top-k sampling parameter (0 = disabled)
|
||||
# OLLAMA_EMBEDDING_TOP_K=40
|
||||
|
||||
# ollama_embedding -- Top-p (nucleus) sampling parameter (0.0-1.0)
|
||||
# OLLAMA_EMBEDDING_TOP_P=0.9
|
||||
|
||||
# ollama_embedding -- Tail free sampling parameter (1.0 = disabled)
|
||||
# OLLAMA_EMBEDDING_TFS_Z=1.0
|
||||
|
||||
# ollama_embedding -- Typical probability mass (1.0 = disabled)
|
||||
# OLLAMA_EMBEDDING_TYPICAL_P=1.0
|
||||
|
||||
# ollama_embedding -- Minimum probability threshold (0.0 = disabled)
|
||||
# OLLAMA_EMBEDDING_MIN_P=0.0
|
||||
|
||||
# ollama_embedding -- Number of tokens to consider for repetition penalty
|
||||
# OLLAMA_EMBEDDING_REPEAT_LAST_N=64
|
||||
|
||||
# ollama_embedding -- Penalty for repetition (1.0 = no penalty)
|
||||
# OLLAMA_EMBEDDING_REPEAT_PENALTY=1.1
|
||||
|
||||
# ollama_embedding -- Penalty for token presence (-2.0 to 2.0)
|
||||
# OLLAMA_EMBEDDING_PRESENCE_PENALTY=0.0
|
||||
|
||||
# ollama_embedding -- Penalty for token frequency (-2.0 to 2.0)
|
||||
# OLLAMA_EMBEDDING_FREQUENCY_PENALTY=0.0
|
||||
|
||||
# ollama_embedding -- Mirostat sampling algorithm (0=disabled, 1=Mirostat 1.0, 2=Mirostat 2.0)
|
||||
# OLLAMA_EMBEDDING_MIROSTAT=0
|
||||
|
||||
# ollama_embedding -- Mirostat target entropy
|
||||
# OLLAMA_EMBEDDING_MIROSTAT_TAU=5.0
|
||||
|
||||
# ollama_embedding -- Mirostat learning rate
|
||||
# OLLAMA_EMBEDDING_MIROSTAT_ETA=0.1
|
||||
|
||||
# ollama_embedding -- Enable NUMA optimization
|
||||
# OLLAMA_EMBEDDING_NUMA=False
|
||||
|
||||
# ollama_embedding -- Batch size for processing
|
||||
# OLLAMA_EMBEDDING_NUM_BATCH=512
|
||||
|
||||
# ollama_embedding -- Number of GPUs to use (-1 for auto)
|
||||
# OLLAMA_EMBEDDING_NUM_GPU=-1
|
||||
|
||||
# ollama_embedding -- Main GPU index
|
||||
# OLLAMA_EMBEDDING_MAIN_GPU=0
|
||||
|
||||
# ollama_embedding -- Optimize for low VRAM
|
||||
# OLLAMA_EMBEDDING_LOW_VRAM=False
|
||||
|
||||
# ollama_embedding -- Number of CPU threads (0 for auto)
|
||||
# OLLAMA_EMBEDDING_NUM_THREAD=0
|
||||
|
||||
# ollama_embedding -- Use half-precision for key/value cache
|
||||
# OLLAMA_EMBEDDING_F16_KV=True
|
||||
|
||||
# ollama_embedding -- Return logits for all tokens
|
||||
# OLLAMA_EMBEDDING_LOGITS_ALL=False
|
||||
|
||||
# ollama_embedding -- Only load vocabulary
|
||||
# OLLAMA_EMBEDDING_VOCAB_ONLY=False
|
||||
|
||||
# ollama_embedding -- Use memory mapping for model files
|
||||
# OLLAMA_EMBEDDING_USE_MMAP=True
|
||||
|
||||
# ollama_embedding -- Lock model in memory
|
||||
# OLLAMA_EMBEDDING_USE_MLOCK=False
|
||||
|
||||
# ollama_embedding -- Only use for embeddings
|
||||
# OLLAMA_EMBEDDING_EMBEDDING_ONLY=False
|
||||
|
||||
# ollama_embedding -- Penalize newline tokens
|
||||
# OLLAMA_EMBEDDING_PENALIZE_NEWLINE=True
|
||||
|
||||
# ollama_embedding -- Stop sequences (comma-separated string)
|
||||
# OLLAMA_EMBEDDING_STOP=
|
||||
|
||||
# ollama_llm -- Context window size (number of tokens)
|
||||
# OLLAMA_LLM_NUM_CTX=4096
|
||||
|
||||
# ollama_llm -- Maximum number of tokens to predict
|
||||
# OLLAMA_LLM_NUM_PREDICT=128
|
||||
|
||||
# ollama_llm -- Number of tokens to keep from the initial prompt
|
||||
# OLLAMA_LLM_NUM_KEEP=0
|
||||
|
||||
# ollama_llm -- Random seed for generation (-1 for random)
|
||||
# OLLAMA_LLM_SEED=-1
|
||||
|
||||
# ollama_llm -- Controls randomness (0.0-2.0, higher = more creative)
|
||||
# OLLAMA_LLM_TEMPERATURE=0.8
|
||||
|
||||
# ollama_llm -- Top-k sampling parameter (0 = disabled)
|
||||
# OLLAMA_LLM_TOP_K=40
|
||||
|
||||
# ollama_llm -- Top-p (nucleus) sampling parameter (0.0-1.0)
|
||||
# OLLAMA_LLM_TOP_P=0.9
|
||||
|
||||
# ollama_llm -- Tail free sampling parameter (1.0 = disabled)
|
||||
# OLLAMA_LLM_TFS_Z=1.0
|
||||
|
||||
# ollama_llm -- Typical probability mass (1.0 = disabled)
|
||||
# OLLAMA_LLM_TYPICAL_P=1.0
|
||||
|
||||
# ollama_llm -- Minimum probability threshold (0.0 = disabled)
|
||||
# OLLAMA_LLM_MIN_P=0.0
|
||||
|
||||
# ollama_llm -- Number of tokens to consider for repetition penalty
|
||||
# OLLAMA_LLM_REPEAT_LAST_N=64
|
||||
|
||||
# ollama_llm -- Penalty for repetition (1.0 = no penalty)
|
||||
# OLLAMA_LLM_REPEAT_PENALTY=1.1
|
||||
|
||||
# ollama_llm -- Penalty for token presence (-2.0 to 2.0)
|
||||
# OLLAMA_LLM_PRESENCE_PENALTY=0.0
|
||||
|
||||
# ollama_llm -- Penalty for token frequency (-2.0 to 2.0)
|
||||
# OLLAMA_LLM_FREQUENCY_PENALTY=0.0
|
||||
|
||||
# ollama_llm -- Mirostat sampling algorithm (0=disabled, 1=Mirostat 1.0, 2=Mirostat 2.0)
|
||||
# OLLAMA_LLM_MIROSTAT=0
|
||||
|
||||
# ollama_llm -- Mirostat target entropy
|
||||
# OLLAMA_LLM_MIROSTAT_TAU=5.0
|
||||
|
||||
# ollama_llm -- Mirostat learning rate
|
||||
# OLLAMA_LLM_MIROSTAT_ETA=0.1
|
||||
|
||||
# ollama_llm -- Enable NUMA optimization
|
||||
# OLLAMA_LLM_NUMA=False
|
||||
|
||||
# ollama_llm -- Batch size for processing
|
||||
# OLLAMA_LLM_NUM_BATCH=512
|
||||
|
||||
# ollama_llm -- Number of GPUs to use (-1 for auto)
|
||||
# OLLAMA_LLM_NUM_GPU=-1
|
||||
|
||||
# ollama_llm -- Main GPU index
|
||||
# OLLAMA_LLM_MAIN_GPU=0
|
||||
|
||||
# ollama_llm -- Optimize for low VRAM
|
||||
# OLLAMA_LLM_LOW_VRAM=False
|
||||
|
||||
# ollama_llm -- Number of CPU threads (0 for auto)
|
||||
# OLLAMA_LLM_NUM_THREAD=0
|
||||
|
||||
# ollama_llm -- Use half-precision for key/value cache
|
||||
# OLLAMA_LLM_F16_KV=True
|
||||
|
||||
# ollama_llm -- Return logits for all tokens
|
||||
# OLLAMA_LLM_LOGITS_ALL=False
|
||||
|
||||
# ollama_llm -- Only load vocabulary
|
||||
# OLLAMA_LLM_VOCAB_ONLY=False
|
||||
|
||||
# ollama_llm -- Use memory mapping for model files
|
||||
# OLLAMA_LLM_USE_MMAP=True
|
||||
|
||||
# ollama_llm -- Lock model in memory
|
||||
# OLLAMA_LLM_USE_MLOCK=False
|
||||
|
||||
# ollama_llm -- Only use for embeddings
|
||||
# OLLAMA_LLM_EMBEDDING_ONLY=False
|
||||
|
||||
# ollama_llm -- Penalize newline tokens
|
||||
# OLLAMA_LLM_PENALIZE_NEWLINE=True
|
||||
|
||||
# ollama_llm -- Stop sequences (comma-separated string)
|
||||
# OLLAMA_LLM_STOP=
|
||||
|
||||
#
|
||||
# End of .env entries for LightRAG binding options
|
||||
################################################################################
|
|
@ -87,7 +87,7 @@ async def initialize_rag():
|
|||
working_dir=WORKING_DIR,
|
||||
llm_model_func=ollama_model_complete,
|
||||
llm_model_name=os.getenv("LLM_MODEL", "qwen2.5-coder:7b"),
|
||||
llm_model_max_token_size=8192,
|
||||
summary_max_tokens=8192,
|
||||
llm_model_kwargs={
|
||||
"host": os.getenv("LLM_BINDING_HOST", "http://localhost:11434"),
|
||||
"options": {"num_ctx": 8192},
|
||||
|
|
|
@ -211,7 +211,7 @@ async def initialize_rag():
|
|||
max_parallel_insert=2,
|
||||
llm_model_func=cloudflare_worker.query,
|
||||
llm_model_name=os.getenv("LLM_MODEL", LLM_MODEL),
|
||||
llm_model_max_token_size=4080,
|
||||
summary_max_tokens=4080,
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=int(os.getenv("EMBEDDING_DIM", "1024")),
|
||||
max_token_size=int(os.getenv("MAX_EMBED_TOKENS", "2048")),
|
||||
|
|
|
@ -56,7 +56,7 @@ async def initialize_rag():
|
|||
rag = LightRAG(
|
||||
working_dir=WORKING_DIR,
|
||||
llm_model_func=llm_model_func,
|
||||
llm_model_max_token_size=32768,
|
||||
summary_max_tokens=10000,
|
||||
embedding_func=embedding_func,
|
||||
chunk_token_size=512,
|
||||
chunk_overlap_token_size=256,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from .lightrag import LightRAG as LightRAG, QueryParam as QueryParam
|
||||
|
||||
__version__ = "1.4.4"
|
||||
__version__ = "1.4.6"
|
||||
__author__ = "Zirui Guo"
|
||||
__url__ = "https://github.com/HKUDS/LightRAG"
|
||||
|
|
|
@ -54,8 +54,6 @@ LLM_BINDING=openai
|
|||
LLM_MODEL=gpt-4o
|
||||
LLM_BINDING_HOST=https://api.openai.com/v1
|
||||
LLM_BINDING_API_KEY=your_api_key
|
||||
### 发送给 LLM 进行实体关系摘要的最大 token 数(小于模型上下文大小)
|
||||
MAX_TOKENS=32000
|
||||
|
||||
EMBEDDING_BINDING=ollama
|
||||
EMBEDDING_BINDING_HOST=http://localhost:11434
|
||||
|
@ -71,10 +69,8 @@ LLM_BINDING=ollama
|
|||
LLM_MODEL=mistral-nemo:latest
|
||||
LLM_BINDING_HOST=http://localhost:11434
|
||||
# LLM_BINDING_API_KEY=your_api_key
|
||||
### 发送给 LLM 进行实体关系摘要的最大 token 数(小于模型上下文大小)
|
||||
MAX_TOKENS=7500
|
||||
### Ollama 服务器上下文 token 数(基于您的 Ollama 服务器容量)
|
||||
OLLAMA_NUM_CTX=8192
|
||||
### Ollama 服务器上下文 token 数(必须大于 MAX_TOTAL_TOKENS+2000)
|
||||
OLLAMA_LLM_NUM_CTX=8192
|
||||
|
||||
EMBEDDING_BINDING=ollama
|
||||
EMBEDDING_BINDING_HOST=http://localhost:11434
|
||||
|
@ -419,6 +415,8 @@ PGDocStatusStorage Postgres
|
|||
MongoDocStatusStorage MongoDB
|
||||
```
|
||||
|
||||
每一种存储类型的链接配置范例可以在 `env.example` 文件中找到。链接字符串中的数据库实例是需要你预先在数据库服务器上创建好的,LightRAG 仅负责在数据库实例中创建数据表,不负责创建数据库实例。如果使用 Redis 作为存储,记得给 Redis 配置自动持久化数据规则,否则 Redis 服务重启后数据会丢失。如果使用PostgreSQL数据库,推荐使用16.6版本或以上。
|
||||
|
||||
### 如何选择存储实现
|
||||
|
||||
您可以通过环境变量选择存储实现。在首次启动 API 服务器之前,您可以将以下环境变量设置为特定的存储实现名称:
|
||||
|
@ -470,9 +468,7 @@ MAX_PARALLEL_INSERT=2
|
|||
|
||||
### LLM Configuration (Use valid host. For local services installed with docker, you can use host.docker.internal)
|
||||
TIMEOUT=200
|
||||
TEMPERATURE=0.0
|
||||
MAX_ASYNC=4
|
||||
MAX_TOKENS=32768
|
||||
|
||||
LLM_BINDING=openai
|
||||
LLM_MODEL=gpt-4o-mini
|
||||
|
@ -568,12 +564,12 @@ pip install lightrag-hku
|
|||
|
||||
LightRAG 中的文档处理流程有些复杂,分为两个主要阶段:提取阶段(实体和关系提取)和合并阶段(实体和关系合并)。有两个关键参数控制流程并发性:并行处理的最大文件数(`MAX_PARALLEL_INSERT`)和最大并发 LLM 请求数(`MAX_ASYNC`)。工作流程描述如下:
|
||||
|
||||
1. `MAX_PARALLEL_INSERT` 控制提取阶段并行处理的文件数量。
|
||||
2. `MAX_ASYNC` 限制系统中并发 LLM 请求的总数,包括查询、提取和合并的请求。LLM 请求具有不同的优先级:查询操作优先级最高,其次是合并,然后是提取。
|
||||
1. `MAX_ASYNC` 限制系统中并发 LLM 请求的总数,包括查询、提取和合并的请求。LLM 请求具有不同的优先级:查询操作优先级最高,其次是合并,然后是提取。
|
||||
2. `MAX_PARALLEL_INSERT` 控制提取阶段并行处理的文件数量。`MAX_PARALLEL_INSERT`建议设置为2~10之间,通常设置为 `MAX_ASYNC/3`,设置太大会导致合并阶段不同文档之间实体和关系重名的机会增大,降低合并阶段的效率。
|
||||
3. 在单个文件中,来自不同文本块的实体和关系提取是并发处理的,并发度由 `MAX_ASYNC` 设置。只有在处理完 `MAX_ASYNC` 个文本块后,系统才会继续处理同一文件中的下一批文本块。
|
||||
4. 合并阶段仅在文件中所有文本块完成实体和关系提取后开始。当一个文件进入合并阶段时,流程允许下一个文件开始提取。
|
||||
5. 由于提取阶段通常比合并阶段快,因此实际并发处理的文件数可能会超过 `MAX_PARALLEL_INSERT`,因为此参数仅控制提取阶段的并行度。
|
||||
6. 为防止竞争条件,合并阶段不支持多个文件的并发处理;一次只能合并一个文件,其他文件必须在队列中等待。
|
||||
4. 当一个文件完成实体和关系提后,将进入实体和关系合并阶段。这一阶段也会并发处理多个实体和关系,其并发度同样是由 `MAX_ASYNC` 控制。
|
||||
5. 合并阶段的 LLM 请求的优先级别高于提取阶段,目的是让进入合并阶段的文件尽快完成处理,并让处理结果尽快更新到向量数据库中。
|
||||
6. 为防止竞争条件,合并阶段会避免并发处理同一个实体或关系,当多个文件中都涉及同一个实体或关系需要合并的时候他们会串行执行。
|
||||
7. 每个文件在流程中被视为一个原子处理单元。只有当其所有文本块都完成提取和合并后,文件才会被标记为成功处理。如果在处理过程中发生任何错误,整个文件将被标记为失败,并且必须重新处理。
|
||||
8. 当由于错误而重新处理文件时,由于 LLM 缓存,先前处理的文本块可以快速跳过。尽管 LLM 缓存在合并阶段也会被利用,但合并顺序的不一致可能会限制其在此阶段的有效性。
|
||||
9. 如果在提取过程中发生错误,系统不会保留任何中间结果。如果在合并过程中发生错误,已合并的实体和关系可能会被保留;当重新处理同一文件时,重新提取的实体和关系将与现有实体和关系合并,而不会影响查询结果。
|
||||
|
@ -596,112 +592,20 @@ LightRAG 中的文档处理流程有些复杂,分为两个主要阶段:提
|
|||
4. 使用查询端点查询系统
|
||||
5. 如果在输入目录中放入新文件,触发文档扫描
|
||||
|
||||
### 查询端点
|
||||
## 异步文档索引与进度跟踪
|
||||
|
||||
#### POST /query
|
||||
使用不同搜索模式查询 RAG 系统。
|
||||
LightRAG采用异步文档索引机制,便于前端监控和查询文档处理进度。用户通过指定端点上传文件或插入文本时,系统将返回唯一的跟踪ID,以便实时监控处理进度。
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:9621/query" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "您的问题", "mode": "hybrid", ""}'
|
||||
```
|
||||
**支持生成跟踪ID的API端点:**
|
||||
* `/documents/upload`
|
||||
* `/documents/text`
|
||||
* `/documents/texts`
|
||||
|
||||
#### POST /query/stream
|
||||
从 RAG 系统流式获取响应。
|
||||
**文档处理状态查询端点:**
|
||||
* `/track_status/{track_id}`
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:9621/query/stream" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "您的问题", "mode": "hybrid"}'
|
||||
```
|
||||
|
||||
### 文档管理端点
|
||||
|
||||
#### POST /documents/text
|
||||
直接将文本插入 RAG 系统。
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:9621/documents/text" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "您的文本内容", "description": "可选描述"}'
|
||||
```
|
||||
|
||||
#### POST /documents/file
|
||||
向 RAG 系统上传单个文件。
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:9621/documents/file" \
|
||||
-F "file=@/path/to/your/document.txt" \
|
||||
-F "description=可选描述"
|
||||
```
|
||||
|
||||
#### POST /documents/batch
|
||||
一次上传多个文件。
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:9621/documents/batch" \
|
||||
-F "files=@/path/to/doc1.txt" \
|
||||
-F "files=@/path/to/doc2.txt"
|
||||
```
|
||||
|
||||
#### POST /documents/scan
|
||||
|
||||
触发输入目录中新文件的文档扫描。
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:9621/documents/scan" --max-time 1800
|
||||
```
|
||||
|
||||
> 根据所有新文件的预计索引时间调整 max-time。
|
||||
|
||||
#### DELETE /documents
|
||||
|
||||
从 RAG 系统中清除所有文档。
|
||||
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:9621/documents"
|
||||
```
|
||||
|
||||
### Ollama 模拟端点
|
||||
|
||||
#### GET /api/version
|
||||
|
||||
获取 Ollama 版本信息。
|
||||
|
||||
```bash
|
||||
curl http://localhost:9621/api/version
|
||||
```
|
||||
|
||||
#### GET /api/tags
|
||||
|
||||
获取 Ollama 可用模型。
|
||||
|
||||
```bash
|
||||
curl http://localhost:9621/api/tags
|
||||
```
|
||||
|
||||
#### POST /api/chat
|
||||
|
||||
处理聊天补全请求。通过根据查询前缀选择查询模式将用户查询路由到 LightRAG。检测并将 OpenWebUI 会话相关请求(用于元数据生成任务)直接转发给底层 LLM。
|
||||
|
||||
```shell
|
||||
curl -N -X POST http://localhost:9621/api/chat -H "Content-Type: application/json" -d \
|
||||
'{"model":"lightrag:latest","messages":[{"role":"user","content":"猪八戒是谁"}],"stream":true}'
|
||||
```
|
||||
|
||||
> 有关 Ollama API 的更多信息,请访问:[Ollama API 文档](https://github.com/ollama/ollama/blob/main/docs/api.md)
|
||||
|
||||
#### POST /api/generate
|
||||
|
||||
处理生成补全请求。为了兼容性目的,该请求不由 LightRAG 处理,而是由底层 LLM 模型处理。
|
||||
|
||||
### 实用工具端点
|
||||
|
||||
#### GET /health
|
||||
检查服务器健康状况和配置。
|
||||
|
||||
```bash
|
||||
curl "http://localhost:9621/health"
|
||||
|
||||
```
|
||||
该端点提供全面的状态信息,包括:
|
||||
* 文档处理状态(待处理/处理中/已处理/失败)
|
||||
* 内容摘要和元数据
|
||||
* 处理失败时的错误信息
|
||||
* 创建和更新时间戳
|
||||
|
|
|
@ -54,8 +54,6 @@ LLM_BINDING=openai
|
|||
LLM_MODEL=gpt-4o
|
||||
LLM_BINDING_HOST=https://api.openai.com/v1
|
||||
LLM_BINDING_API_KEY=your_api_key
|
||||
### Max tokens sent to LLM (less than model context size)
|
||||
MAX_TOKENS=32768
|
||||
|
||||
EMBEDDING_BINDING=ollama
|
||||
EMBEDDING_BINDING_HOST=http://localhost:11434
|
||||
|
@ -71,10 +69,8 @@ LLM_BINDING=ollama
|
|||
LLM_MODEL=mistral-nemo:latest
|
||||
LLM_BINDING_HOST=http://localhost:11434
|
||||
# LLM_BINDING_API_KEY=your_api_key
|
||||
### Max tokens sent to LLM for entity relation description summarization (Less than LLM context length)
|
||||
MAX_TOKENS=7500
|
||||
### Ollama Server context length
|
||||
OLLAMA_NUM_CTX=8192
|
||||
### Ollama Server context length (Must be larger than MAX_TOTAL_TOKENS+2000)
|
||||
OLLAMA_LLM_NUM_CTX=16384
|
||||
|
||||
EMBEDDING_BINDING=ollama
|
||||
EMBEDDING_BINDING_HOST=http://localhost:11434
|
||||
|
@ -422,6 +418,8 @@ JsonDocStatusStorage JsonFile (default)
|
|||
PGDocStatusStorage Postgres
|
||||
MongoDocStatusStorage MongoDB
|
||||
```
|
||||
Example connection configurations for each storage type can be found in the `env.example` file. The database instance in the connection string needs to be created by you on the database server beforehand. LightRAG is only responsible for creating tables within the database instance, not for creating the database instance itself. If using Redis as storage, remember to configure automatic data persistence rules for Redis, otherwise data will be lost after the Redis service restarts. If using PostgreSQL, it is recommended to use version 16.6 or above.
|
||||
|
||||
|
||||
### How to Select Storage Implementation
|
||||
|
||||
|
@ -459,6 +457,10 @@ You cannot change storage implementation selection after adding documents to Lig
|
|||
| --embedding-binding | ollama | Embedding binding type (lollms, ollama, openai, azure_openai) |
|
||||
| --auto-scan-at-startup| - | Scan input directory for new files and start indexing |
|
||||
|
||||
### Additional Ollama Binding Options
|
||||
|
||||
When using `--llm-binding ollama` or `--embedding-binding ollama`, additional Ollama-specific configuration options are available. To see all available Ollama binding options, add `--help` to the command line when starting the server. These additional options allow for fine-tuning of Ollama model parameters and connection settings.
|
||||
|
||||
### .env Examples
|
||||
|
||||
```bash
|
||||
|
@ -474,9 +476,7 @@ MAX_PARALLEL_INSERT=2
|
|||
|
||||
### LLM Configuration (Use valid host. For local services installed with docker, you can use host.docker.internal)
|
||||
TIMEOUT=200
|
||||
TEMPERATURE=0.0
|
||||
MAX_ASYNC=4
|
||||
MAX_TOKENS=32768
|
||||
|
||||
LLM_BINDING=openai
|
||||
LLM_MODEL=gpt-4o-mini
|
||||
|
@ -484,6 +484,7 @@ LLM_BINDING_HOST=https://api.openai.com/v1
|
|||
LLM_BINDING_API_KEY=your-api-key
|
||||
|
||||
### Embedding Configuration (Use valid host. For local services installed with docker, you can use host.docker.internal)
|
||||
# see also env.ollama-binding-options.example for fine tuning ollama
|
||||
EMBEDDING_MODEL=bge-m3:latest
|
||||
EMBEDDING_DIM=1024
|
||||
EMBEDDING_BINDING=ollama
|
||||
|
@ -504,12 +505,12 @@ EMBEDDING_BINDING_HOST=http://localhost:11434
|
|||
|
||||
The document processing pipeline in LightRAG is somewhat complex and is divided into two primary stages: the Extraction stage (entity and relationship extraction) and the Merging stage (entity and relationship merging). There are two key parameters that control pipeline concurrency: the maximum number of files processed in parallel (MAX_PARALLEL_INSERT) and the maximum number of concurrent LLM requests (MAX_ASYNC). The workflow is described as follows:
|
||||
|
||||
1. MAX_PARALLEL_INSERT controls the number of files processed in parallel during the extraction stage.
|
||||
2. MAX_ASYNC limits the total number of concurrent LLM requests in the system, including those for querying, extraction, and merging. LLM requests have different priorities: query operations have the highest priority, followed by merging, and then extraction.
|
||||
1. MAX_ASYNC limits the total number of concurrent LLM requests in the system, including those for querying, extraction, and merging. LLM requests have different priorities: query operations have the highest priority, followed by merging, and then extraction.
|
||||
2. MAX_PARALLEL_INSERT controls the number of files processed in parallel during the extraction stage. For optimal performance, MAX_PARALLEL_INSERT is recommended to be set between 2 and 10, typically MAX_ASYNC/3. Setting this value too high can increase the likelihood of naming conflicts among entities and relationships across different documents during the merge phase, thereby reducing its overall efficiency.
|
||||
3. Within a single file, entity and relationship extractions from different text blocks are processed concurrently, with the degree of concurrency set by MAX_ASYNC. Only after MAX_ASYNC text blocks are processed will the system proceed to the next batch within the same file.
|
||||
4. The merging stage begins only after all text blocks in a file have completed entity and relationship extraction. When a file enters the merging stage, the pipeline allows the next file to begin extraction.
|
||||
5. Since the extraction stage is generally faster than merging, the actual number of files being processed concurrently may exceed MAX_PARALLEL_INSERT, as this parameter only controls parallelism during the extraction stage.
|
||||
6. To prevent race conditions, the merging stage does not support concurrent processing of multiple files; only one file can be merged at a time, while other files must wait in queue.
|
||||
4. When a file completes entity and relationship extraction, it enters the entity and relationship merging stage. This stage also processes multiple entities and relationships concurrently, with the concurrency level also controlled by `MAX_ASYNC`.
|
||||
5. LLM requests for the merging stage are prioritized over the extraction stage to ensure that files in the merging phase are processed quickly and their results are promptly updated in the vector database.
|
||||
6. To prevent race conditions, the merging stage avoids concurrent processing of the same entity or relationship. When multiple files involve the same entity or relationship that needs to be merged, they are processed serially.
|
||||
7. Each file is treated as an atomic processing unit in the pipeline. A file is marked as successfully processed only after all its text blocks have completed extraction and merging. If any error occurs during processing, the entire file is marked as failed and must be reprocessed.
|
||||
8. When a file is reprocessed due to errors, previously processed text blocks can be quickly skipped thanks to LLM caching. Although LLM cache is also utilized during the merging stage, inconsistencies in merging order may limit its effectiveness in this stage.
|
||||
9. If an error occurs during extraction, the system does not retain any intermediate results. If an error occurs during merging, already merged entities and relationships might be preserved; when the same file is reprocessed, re-extracted entities and relationships will be merged with the existing ones, without impacting the query results.
|
||||
|
@ -532,111 +533,21 @@ You can test the API endpoints using the provided curl commands or through the S
|
|||
4. Query the system using the query endpoints
|
||||
5. Trigger document scan if new files are put into the inputs directory
|
||||
|
||||
### Query Endpoints:
|
||||
## Asynchronous Document Indexing with Progress Tracking
|
||||
|
||||
#### POST /query
|
||||
Query the RAG system with options for different search modes.
|
||||
LightRAG implements asynchronous document indexing to enable frontend monitoring and querying of document processing progress. Upon uploading files or inserting text through designated endpoints, a unique Track ID is returned to facilitate real-time progress monitoring.
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:9621/query" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "Your question here", "mode": "hybrid"}'
|
||||
```
|
||||
**API Endpoints Supporting Track ID Generation:**
|
||||
|
||||
#### POST /query/stream
|
||||
Stream responses from the RAG system.
|
||||
* `/documents/upload`
|
||||
* `/documents/text`
|
||||
* `/documents/texts`
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:9621/query/stream" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "Your question here", "mode": "hybrid"}'
|
||||
```
|
||||
**Document Processing Status Query Endpoint:**
|
||||
* `/track_status/{track_id}`
|
||||
|
||||
### Document Management Endpoints:
|
||||
|
||||
#### POST /documents/text
|
||||
Insert text directly into the RAG system.
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:9621/documents/text" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "Your text content here", "description": "Optional description"}'
|
||||
```
|
||||
|
||||
#### POST /documents/file
|
||||
Upload a single file to the RAG system.
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:9621/documents/file" \
|
||||
-F "file=@/path/to/your/document.txt" \
|
||||
-F "description=Optional description"
|
||||
```
|
||||
|
||||
#### POST /documents/batch
|
||||
Upload multiple files at once.
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:9621/documents/batch" \
|
||||
-F "files=@/path/to/doc1.txt" \
|
||||
-F "files=@/path/to/doc2.txt"
|
||||
```
|
||||
|
||||
#### POST /documents/scan
|
||||
|
||||
Trigger document scan for new files in the input directory.
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:9621/documents/scan" --max-time 1800
|
||||
```
|
||||
|
||||
> Adjust max-time according to the estimated indexing time for all new files.
|
||||
|
||||
#### DELETE /documents
|
||||
|
||||
Clear all documents from the RAG system.
|
||||
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:9621/documents"
|
||||
```
|
||||
|
||||
### Ollama Emulation Endpoints:
|
||||
|
||||
#### GET /api/version
|
||||
|
||||
Get Ollama version information.
|
||||
|
||||
```bash
|
||||
curl http://localhost:9621/api/version
|
||||
```
|
||||
|
||||
#### GET /api/tags
|
||||
|
||||
Get available Ollama models.
|
||||
|
||||
```bash
|
||||
curl http://localhost:9621/api/tags
|
||||
```
|
||||
|
||||
#### POST /api/chat
|
||||
|
||||
Handle chat completion requests. Routes user queries through LightRAG by selecting query mode based on query prefix. Detects and forwards OpenWebUI session-related requests (for metadata generation task) directly to the underlying LLM.
|
||||
|
||||
```shell
|
||||
curl -N -X POST http://localhost:9621/api/chat -H "Content-Type: application/json" -d \
|
||||
'{"model":"lightrag:latest","messages":[{"role":"user","content":"猪八戒是谁"}],"stream":true}'
|
||||
```
|
||||
|
||||
> For more information about Ollama API, please visit: [Ollama API documentation](https://github.com/ollama/ollama/blob/main/docs/api.md)
|
||||
|
||||
#### POST /api/generate
|
||||
|
||||
Handle generate completion requests. For compatibility purposes, the request is not processed by LightRAG, and will be handled by the underlying LLM model.
|
||||
|
||||
### Utility Endpoints:
|
||||
|
||||
#### GET /health
|
||||
Check server health and configuration.
|
||||
|
||||
```bash
|
||||
curl "http://localhost:9621/health"
|
||||
```
|
||||
This endpoint provides comprehensive status information including:
|
||||
* Document processing status (pending/processing/processed/failed)
|
||||
* Content summary and metadata
|
||||
* Error messages if processing failed
|
||||
* Timestamps for creation and updates
|
||||
|
|
|
@ -1 +1 @@
|
|||
__api_version__ = "0187"
|
||||
__api_version__ = "0196"
|
||||
|
|
|
@ -7,6 +7,9 @@ import argparse
|
|||
import logging
|
||||
from dotenv import load_dotenv
|
||||
from lightrag.utils import get_env_value
|
||||
from lightrag.llm.binding_options import OllamaEmbeddingOptions, OllamaLLMOptions
|
||||
from lightrag.base import OllamaServerInfos
|
||||
import sys
|
||||
|
||||
from lightrag.constants import (
|
||||
DEFAULT_WOKERS,
|
||||
|
@ -19,6 +22,16 @@ from lightrag.constants import (
|
|||
DEFAULT_MAX_TOTAL_TOKENS,
|
||||
DEFAULT_COSINE_THRESHOLD,
|
||||
DEFAULT_RELATED_CHUNK_NUMBER,
|
||||
DEFAULT_MIN_RERANK_SCORE,
|
||||
DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE,
|
||||
DEFAULT_MAX_ASYNC,
|
||||
DEFAULT_SUMMARY_MAX_TOKENS,
|
||||
DEFAULT_SUMMARY_LANGUAGE,
|
||||
DEFAULT_EMBEDDING_FUNC_MAX_ASYNC,
|
||||
DEFAULT_EMBEDDING_BATCH_NUM,
|
||||
DEFAULT_OLLAMA_MODEL_NAME,
|
||||
DEFAULT_OLLAMA_MODEL_TAG,
|
||||
DEFAULT_TEMPERATURE,
|
||||
)
|
||||
|
||||
# use the .env that is inside the current folder
|
||||
|
@ -27,16 +40,6 @@ from lightrag.constants import (
|
|||
load_dotenv(dotenv_path=".env", override=False)
|
||||
|
||||
|
||||
class OllamaServerInfos:
|
||||
# Constants for emulated Ollama model information
|
||||
LIGHTRAG_NAME = "lightrag"
|
||||
LIGHTRAG_TAG = os.getenv("OLLAMA_EMULATING_MODEL_TAG", "latest")
|
||||
LIGHTRAG_MODEL = f"{LIGHTRAG_NAME}:{LIGHTRAG_TAG}"
|
||||
LIGHTRAG_SIZE = 7365960935 # it's a dummy value
|
||||
LIGHTRAG_CREATED_AT = "2024-01-15T00:00:00Z"
|
||||
LIGHTRAG_DIGEST = "sha256:lightrag"
|
||||
|
||||
|
||||
ollama_server_infos = OllamaServerInfos()
|
||||
|
||||
|
||||
|
@ -110,14 +113,14 @@ def parse_args() -> argparse.Namespace:
|
|||
parser.add_argument(
|
||||
"--max-async",
|
||||
type=int,
|
||||
default=get_env_value("MAX_ASYNC", 4, int),
|
||||
help="Maximum async operations (default: from env or 4)",
|
||||
default=get_env_value("MAX_ASYNC", DEFAULT_MAX_ASYNC, int),
|
||||
help=f"Maximum async operations (default: from env or {DEFAULT_MAX_ASYNC})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-tokens",
|
||||
type=int,
|
||||
default=get_env_value("MAX_TOKENS", 32000, int),
|
||||
help="Maximum token size (default: from env or 32768)",
|
||||
default=get_env_value("MAX_TOKENS", DEFAULT_SUMMARY_MAX_TOKENS, int),
|
||||
help=f"Maximum token size (default: from env or {DEFAULT_SUMMARY_MAX_TOKENS})",
|
||||
)
|
||||
|
||||
# Logging configuration
|
||||
|
@ -159,14 +162,19 @@ def parse_args() -> argparse.Namespace:
|
|||
help="Path to SSL private key file (required if --ssl is enabled)",
|
||||
)
|
||||
|
||||
# Ollama model name
|
||||
# Ollama model configuration
|
||||
parser.add_argument(
|
||||
"--simulated-model-name",
|
||||
type=str,
|
||||
default=get_env_value(
|
||||
"SIMULATED_MODEL_NAME", ollama_server_infos.LIGHTRAG_MODEL
|
||||
),
|
||||
help="Number of conversation history turns to include (default: from env or 3)",
|
||||
default=get_env_value("OLLAMA_EMULATING_MODEL_NAME", DEFAULT_OLLAMA_MODEL_NAME),
|
||||
help="Name for the simulated Ollama model (default: from env or lightrag)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--simulated-model-tag",
|
||||
type=str,
|
||||
default=get_env_value("OLLAMA_EMULATING_MODEL_TAG", DEFAULT_OLLAMA_MODEL_TAG),
|
||||
help="Tag for the simulated Ollama model (default: from env or latest)",
|
||||
)
|
||||
|
||||
# Namespace
|
||||
|
@ -208,6 +216,29 @@ def parse_args() -> argparse.Namespace:
|
|||
help="Embedding binding type (default: from env or ollama)",
|
||||
)
|
||||
|
||||
# Conditionally add binding options defined in binding_options module
|
||||
# This will add command line arguments for all binding options (e.g., --ollama-embedding-num_ctx)
|
||||
# and corresponding environment variables (e.g., OLLAMA_EMBEDDING_NUM_CTX)
|
||||
if "--llm-binding" in sys.argv:
|
||||
try:
|
||||
idx = sys.argv.index("--llm-binding")
|
||||
if idx + 1 < len(sys.argv) and sys.argv[idx + 1] == "ollama":
|
||||
OllamaLLMOptions.add_args(parser)
|
||||
except IndexError:
|
||||
pass
|
||||
elif os.environ.get("LLM_BINDING") == "ollama":
|
||||
OllamaLLMOptions.add_args(parser)
|
||||
|
||||
if "--embedding-binding" in sys.argv:
|
||||
try:
|
||||
idx = sys.argv.index("--embedding-binding")
|
||||
if idx + 1 < len(sys.argv) and sys.argv[idx + 1] == "ollama":
|
||||
OllamaEmbeddingOptions.add_args(parser)
|
||||
except IndexError:
|
||||
pass
|
||||
elif os.environ.get("EMBEDDING_BINDING") == "ollama":
|
||||
OllamaEmbeddingOptions.add_args(parser)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# convert relative path to absolute path
|
||||
|
@ -255,7 +286,6 @@ def parse_args() -> argparse.Namespace:
|
|||
args.llm_model = get_env_value("LLM_MODEL", "mistral-nemo:latest")
|
||||
args.embedding_model = get_env_value("EMBEDDING_MODEL", "bge-m3:latest")
|
||||
args.embedding_dim = get_env_value("EMBEDDING_DIM", 1024, int)
|
||||
args.max_embed_tokens = get_env_value("MAX_EMBED_TOKENS", 8192, int)
|
||||
|
||||
# Inject chunk configuration
|
||||
args.chunk_size = get_env_value("CHUNK_SIZE", 1200, int)
|
||||
|
@ -268,14 +298,14 @@ def parse_args() -> argparse.Namespace:
|
|||
args.enable_llm_cache = get_env_value("ENABLE_LLM_CACHE", True, bool)
|
||||
|
||||
# Inject LLM temperature configuration
|
||||
args.temperature = get_env_value("TEMPERATURE", 0.5, float)
|
||||
args.temperature = get_env_value("TEMPERATURE", DEFAULT_TEMPERATURE, float)
|
||||
|
||||
# Select Document loading tool (DOCLING, DEFAULT)
|
||||
args.document_loading_engine = get_env_value("DOCUMENT_LOADING_ENGINE", "DEFAULT")
|
||||
|
||||
# Add environment variables that were previously read directly
|
||||
args.cors_origins = get_env_value("CORS_ORIGINS", "*")
|
||||
args.summary_language = get_env_value("SUMMARY_LANGUAGE", "English")
|
||||
args.summary_language = get_env_value("SUMMARY_LANGUAGE", DEFAULT_SUMMARY_LANGUAGE)
|
||||
args.whitelist_paths = get_env_value("WHITELIST_PATHS", "/health,/api/*")
|
||||
|
||||
# For JWT Auth
|
||||
|
@ -290,6 +320,11 @@ def parse_args() -> argparse.Namespace:
|
|||
args.rerank_binding_host = get_env_value("RERANK_BINDING_HOST", None)
|
||||
args.rerank_binding_api_key = get_env_value("RERANK_BINDING_API_KEY", None)
|
||||
|
||||
# Min rerank score configuration
|
||||
args.min_rerank_score = get_env_value(
|
||||
"MIN_RERANK_SCORE", DEFAULT_MIN_RERANK_SCORE, float
|
||||
)
|
||||
|
||||
# Query configuration
|
||||
args.history_turns = get_env_value("HISTORY_TURNS", DEFAULT_HISTORY_TURNS, int)
|
||||
args.top_k = get_env_value("TOP_K", DEFAULT_TOP_K, int)
|
||||
|
@ -310,7 +345,19 @@ def parse_args() -> argparse.Namespace:
|
|||
"RELATED_CHUNK_NUMBER", DEFAULT_RELATED_CHUNK_NUMBER, int
|
||||
)
|
||||
|
||||
ollama_server_infos.LIGHTRAG_MODEL = args.simulated_model_name
|
||||
# Add missing environment variables for health endpoint
|
||||
args.force_llm_summary_on_merge = get_env_value(
|
||||
"FORCE_LLM_SUMMARY_ON_MERGE", DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE, int
|
||||
)
|
||||
args.embedding_func_max_async = get_env_value(
|
||||
"EMBEDDING_FUNC_MAX_ASYNC", DEFAULT_EMBEDDING_FUNC_MAX_ASYNC, int
|
||||
)
|
||||
args.embedding_batch_num = get_env_value(
|
||||
"EMBEDDING_BATCH_NUM", DEFAULT_EMBEDDING_BATCH_NUM, int
|
||||
)
|
||||
|
||||
ollama_server_infos.LIGHTRAG_NAME = args.simulated_model_name
|
||||
ollama_server_infos.LIGHTRAG_TAG = args.simulated_model_tag
|
||||
|
||||
return args
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ import asyncio
|
|||
import os
|
||||
import logging
|
||||
import logging.config
|
||||
import signal
|
||||
import sys
|
||||
import uvicorn
|
||||
import pipmaster as pm
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
@ -28,7 +30,6 @@ from .config import (
|
|||
get_default_host,
|
||||
)
|
||||
from lightrag.utils import get_env_value
|
||||
import sys
|
||||
from lightrag import LightRAG, __version__ as core_version
|
||||
from lightrag.api import __api_version__
|
||||
from lightrag.types import GPTKeywordExtractionFormat
|
||||
|
@ -53,6 +54,7 @@ from lightrag.kg.shared_storage import (
|
|||
get_pipeline_status_lock,
|
||||
initialize_pipeline_status,
|
||||
cleanup_keyed_lock,
|
||||
finalize_share_data,
|
||||
)
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from lightrag.api.auth import auth_handler
|
||||
|
@ -74,6 +76,24 @@ config.read("config.ini")
|
|||
auth_configured = bool(auth_handler.accounts)
|
||||
|
||||
|
||||
def setup_signal_handlers():
|
||||
"""Setup signal handlers for graceful shutdown"""
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print(f"\n\nReceived signal {sig}, shutting down gracefully...")
|
||||
print(f"Process ID: {os.getpid()}")
|
||||
|
||||
# Release shared resources
|
||||
finalize_share_data()
|
||||
|
||||
# Exit with success status
|
||||
sys.exit(0)
|
||||
|
||||
# Register signal handlers
|
||||
signal.signal(signal.SIGINT, signal_handler) # Ctrl+C
|
||||
signal.signal(signal.SIGTERM, signal_handler) # kill command
|
||||
|
||||
|
||||
def create_app(args):
|
||||
# Setup logging
|
||||
logger.setLevel(args.log_level)
|
||||
|
@ -89,7 +109,13 @@ def create_app(args):
|
|||
]:
|
||||
raise Exception("llm binding not supported")
|
||||
|
||||
if args.embedding_binding not in ["lollms", "ollama", "openai", "azure_openai"]:
|
||||
if args.embedding_binding not in [
|
||||
"lollms",
|
||||
"ollama",
|
||||
"openai",
|
||||
"azure_openai",
|
||||
"jina",
|
||||
]:
|
||||
raise Exception("embedding binding not supported")
|
||||
|
||||
# Set default hosts if not provided
|
||||
|
@ -153,6 +179,9 @@ def create_app(args):
|
|||
# Clean up database connections
|
||||
await rag.finalize_storages()
|
||||
|
||||
# Clean up shared data
|
||||
finalize_share_data()
|
||||
|
||||
# Initialize FastAPI
|
||||
app_kwargs = {
|
||||
"title": "LightRAG Server API",
|
||||
|
@ -203,6 +232,7 @@ def create_app(args):
|
|||
from lightrag.llm.lollms import lollms_model_complete, lollms_embed
|
||||
if args.llm_binding == "ollama" or args.embedding_binding == "ollama":
|
||||
from lightrag.llm.ollama import ollama_model_complete, ollama_embed
|
||||
from lightrag.llm.binding_options import OllamaLLMOptions
|
||||
if args.llm_binding == "openai" or args.embedding_binding == "openai":
|
||||
from lightrag.llm.openai import openai_complete_if_cache, openai_embed
|
||||
if args.llm_binding == "azure_openai" or args.embedding_binding == "azure_openai":
|
||||
|
@ -213,6 +243,9 @@ def create_app(args):
|
|||
if args.llm_binding_host == "openai-ollama" or args.embedding_binding == "ollama":
|
||||
from lightrag.llm.openai import openai_complete_if_cache
|
||||
from lightrag.llm.ollama import ollama_embed
|
||||
from lightrag.llm.binding_options import OllamaEmbeddingOptions
|
||||
if args.embedding_binding == "jina":
|
||||
from lightrag.llm.jina import jina_embed
|
||||
|
||||
async def openai_alike_model_complete(
|
||||
prompt,
|
||||
|
@ -263,7 +296,6 @@ def create_app(args):
|
|||
|
||||
embedding_func = EmbeddingFunc(
|
||||
embedding_dim=args.embedding_dim,
|
||||
max_token_size=args.max_embed_tokens,
|
||||
func=lambda texts: lollms_embed(
|
||||
texts,
|
||||
embed_model=args.embedding_model,
|
||||
|
@ -276,6 +308,7 @@ def create_app(args):
|
|||
embed_model=args.embedding_model,
|
||||
host=args.embedding_binding_host,
|
||||
api_key=args.embedding_binding_api_key,
|
||||
options=OllamaEmbeddingOptions.options_dict(args),
|
||||
)
|
||||
if args.embedding_binding == "ollama"
|
||||
else azure_openai_embed(
|
||||
|
@ -284,6 +317,13 @@ def create_app(args):
|
|||
api_key=args.embedding_binding_api_key,
|
||||
)
|
||||
if args.embedding_binding == "azure_openai"
|
||||
else jina_embed(
|
||||
texts,
|
||||
dimensions=args.embedding_dim,
|
||||
base_url=args.embedding_binding_host,
|
||||
api_key=args.embedding_binding_api_key,
|
||||
)
|
||||
if args.embedding_binding == "jina"
|
||||
else openai_embed(
|
||||
texts,
|
||||
model=args.embedding_model,
|
||||
|
@ -320,6 +360,13 @@ def create_app(args):
|
|||
"Rerank model not configured. Set RERANK_BINDING_API_KEY and RERANK_BINDING_HOST to enable reranking."
|
||||
)
|
||||
|
||||
# Create ollama_server_infos from command line arguments
|
||||
from lightrag.api.config import OllamaServerInfos
|
||||
|
||||
ollama_server_infos = OllamaServerInfos(
|
||||
name=args.simulated_model_name, tag=args.simulated_model_tag
|
||||
)
|
||||
|
||||
# Initialize RAG
|
||||
if args.llm_binding in ["lollms", "ollama", "openai"]:
|
||||
rag = LightRAG(
|
||||
|
@ -332,13 +379,13 @@ def create_app(args):
|
|||
else openai_alike_model_complete,
|
||||
llm_model_name=args.llm_model,
|
||||
llm_model_max_async=args.max_async,
|
||||
llm_model_max_token_size=args.max_tokens,
|
||||
summary_max_tokens=args.max_tokens,
|
||||
chunk_token_size=int(args.chunk_size),
|
||||
chunk_overlap_token_size=int(args.chunk_overlap_size),
|
||||
llm_model_kwargs={
|
||||
"host": args.llm_binding_host,
|
||||
"timeout": args.timeout,
|
||||
"options": {"num_ctx": args.ollama_num_ctx},
|
||||
"options": OllamaLLMOptions.options_dict(args),
|
||||
"api_key": args.llm_binding_api_key,
|
||||
}
|
||||
if args.llm_binding == "lollms" or args.llm_binding == "ollama"
|
||||
|
@ -358,6 +405,7 @@ def create_app(args):
|
|||
max_parallel_insert=args.max_parallel_insert,
|
||||
max_graph_nodes=args.max_graph_nodes,
|
||||
addon_params={"language": args.summary_language},
|
||||
ollama_server_infos=ollama_server_infos,
|
||||
)
|
||||
else: # azure_openai
|
||||
rag = LightRAG(
|
||||
|
@ -371,7 +419,7 @@ def create_app(args):
|
|||
},
|
||||
llm_model_name=args.llm_model,
|
||||
llm_model_max_async=args.max_async,
|
||||
llm_model_max_token_size=args.max_tokens,
|
||||
summary_max_tokens=args.max_tokens,
|
||||
embedding_func=embedding_func,
|
||||
kv_storage=args.kv_storage,
|
||||
graph_storage=args.graph_storage,
|
||||
|
@ -387,6 +435,7 @@ def create_app(args):
|
|||
max_parallel_insert=args.max_parallel_insert,
|
||||
max_graph_nodes=args.max_graph_nodes,
|
||||
addon_params={"language": args.summary_language},
|
||||
ollama_server_infos=ollama_server_infos,
|
||||
)
|
||||
|
||||
# Add routes
|
||||
|
@ -520,6 +569,16 @@ def create_app(args):
|
|||
"rerank_binding_host": args.rerank_binding_host
|
||||
if rerank_model_func is not None
|
||||
else None,
|
||||
# Environment variable status (requested configuration)
|
||||
"summary_language": args.summary_language,
|
||||
"force_llm_summary_on_merge": args.force_llm_summary_on_merge,
|
||||
"max_parallel_insert": args.max_parallel_insert,
|
||||
"cosine_threshold": args.cosine_threshold,
|
||||
"min_rerank_score": args.min_rerank_score,
|
||||
"related_chunk_number": args.related_chunk_number,
|
||||
"max_async": args.max_async,
|
||||
"embedding_func_max_async": args.embedding_func_max_async,
|
||||
"embedding_batch_num": args.embedding_batch_num,
|
||||
},
|
||||
"auth_mode": auth_mode,
|
||||
"pipeline_busy": pipeline_status.get("busy", False),
|
||||
|
@ -701,6 +760,9 @@ def main():
|
|||
update_uvicorn_mode_config()
|
||||
display_splash_screen(global_args)
|
||||
|
||||
# Setup signal handlers for graceful shutdown
|
||||
setup_signal_handlers()
|
||||
|
||||
# Create application instance directly instead of using factory function
|
||||
app = create_app(global_args)
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ from pydantic import BaseModel, Field, field_validator
|
|||
|
||||
from lightrag import LightRAG
|
||||
from lightrag.base import DeletionResult, DocProcessingStatus, DocStatus
|
||||
from lightrag.utils import generate_track_id
|
||||
from lightrag.api.utils_api import get_combined_auth_dependency
|
||||
from ..config import global_args
|
||||
|
||||
|
@ -113,6 +114,7 @@ class ScanResponse(BaseModel):
|
|||
Attributes:
|
||||
status: Status of the scanning operation
|
||||
message: Optional message with additional details
|
||||
track_id: Tracking ID for monitoring scanning progress
|
||||
"""
|
||||
|
||||
status: Literal["scanning_started"] = Field(
|
||||
|
@ -121,12 +123,14 @@ class ScanResponse(BaseModel):
|
|||
message: Optional[str] = Field(
|
||||
default=None, description="Additional details about the scanning operation"
|
||||
)
|
||||
track_id: str = Field(description="Tracking ID for monitoring scanning progress")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"status": "scanning_started",
|
||||
"message": "Scanning process has been initiated in the background",
|
||||
"track_id": "scan_20250729_170612_abc123",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -210,18 +214,21 @@ class InsertResponse(BaseModel):
|
|||
Attributes:
|
||||
status: Status of the operation (success, duplicated, partial_success, failure)
|
||||
message: Detailed message describing the operation result
|
||||
track_id: Tracking ID for monitoring processing status
|
||||
"""
|
||||
|
||||
status: Literal["success", "duplicated", "partial_success", "failure"] = Field(
|
||||
description="Status of the operation"
|
||||
)
|
||||
message: str = Field(description="Message describing the operation result")
|
||||
track_id: str = Field(description="Tracking ID for monitoring processing status")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"status": "success",
|
||||
"message": "File 'document.pdf' uploaded successfully. Processing will continue in background.",
|
||||
"track_id": "upload_20250729_170612_abc123",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -360,10 +367,13 @@ class DocStatusResponse(BaseModel):
|
|||
status: DocStatus = Field(description="Current processing status")
|
||||
created_at: str = Field(description="Creation timestamp (ISO format string)")
|
||||
updated_at: str = Field(description="Last update timestamp (ISO format string)")
|
||||
track_id: Optional[str] = Field(
|
||||
default=None, description="Tracking ID for monitoring progress"
|
||||
)
|
||||
chunks_count: Optional[int] = Field(
|
||||
default=None, description="Number of chunks the document was split into"
|
||||
)
|
||||
error: Optional[str] = Field(
|
||||
error_msg: Optional[str] = Field(
|
||||
default=None, description="Error message if processing failed"
|
||||
)
|
||||
metadata: Optional[dict[str, Any]] = Field(
|
||||
|
@ -380,6 +390,7 @@ class DocStatusResponse(BaseModel):
|
|||
"status": "PROCESSED",
|
||||
"created_at": "2025-03-31T12:34:56",
|
||||
"updated_at": "2025-03-31T12:35:30",
|
||||
"track_id": "upload_20250729_170612_abc123",
|
||||
"chunks_count": 12,
|
||||
"error": None,
|
||||
"metadata": {"author": "John Doe", "year": 2025},
|
||||
|
@ -412,6 +423,10 @@ class DocsStatusesResponse(BaseModel):
|
|||
"status": "PENDING",
|
||||
"created_at": "2025-03-31T10:00:00",
|
||||
"updated_at": "2025-03-31T10:00:00",
|
||||
"track_id": "upload_20250331_100000_abc123",
|
||||
"chunks_count": None,
|
||||
"error": None,
|
||||
"metadata": None,
|
||||
"file_path": "pending_doc.pdf",
|
||||
}
|
||||
],
|
||||
|
@ -423,7 +438,10 @@ class DocsStatusesResponse(BaseModel):
|
|||
"status": "PROCESSED",
|
||||
"created_at": "2025-03-31T09:00:00",
|
||||
"updated_at": "2025-03-31T09:05:00",
|
||||
"track_id": "insert_20250331_090000_def456",
|
||||
"chunks_count": 8,
|
||||
"error": None,
|
||||
"metadata": {"author": "John Doe"},
|
||||
"file_path": "processed_doc.pdf",
|
||||
}
|
||||
],
|
||||
|
@ -432,6 +450,192 @@ class DocsStatusesResponse(BaseModel):
|
|||
}
|
||||
|
||||
|
||||
class TrackStatusResponse(BaseModel):
|
||||
"""Response model for tracking document processing status by track_id
|
||||
|
||||
Attributes:
|
||||
track_id: The tracking ID
|
||||
documents: List of documents associated with this track_id
|
||||
total_count: Total number of documents for this track_id
|
||||
status_summary: Count of documents by status
|
||||
"""
|
||||
|
||||
track_id: str = Field(description="The tracking ID")
|
||||
documents: List[DocStatusResponse] = Field(
|
||||
description="List of documents associated with this track_id"
|
||||
)
|
||||
total_count: int = Field(description="Total number of documents for this track_id")
|
||||
status_summary: Dict[str, int] = Field(description="Count of documents by status")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"track_id": "upload_20250729_170612_abc123",
|
||||
"documents": [
|
||||
{
|
||||
"id": "doc_123456",
|
||||
"content_summary": "Research paper on machine learning",
|
||||
"content_length": 15240,
|
||||
"status": "PROCESSED",
|
||||
"created_at": "2025-03-31T12:34:56",
|
||||
"updated_at": "2025-03-31T12:35:30",
|
||||
"track_id": "upload_20250729_170612_abc123",
|
||||
"chunks_count": 12,
|
||||
"error": None,
|
||||
"metadata": {"author": "John Doe", "year": 2025},
|
||||
"file_path": "research_paper.pdf",
|
||||
}
|
||||
],
|
||||
"total_count": 1,
|
||||
"status_summary": {"PROCESSED": 1},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DocumentsRequest(BaseModel):
|
||||
"""Request model for paginated document queries
|
||||
|
||||
Attributes:
|
||||
status_filter: Filter by document status, None for all statuses
|
||||
page: Page number (1-based)
|
||||
page_size: Number of documents per page (10-200)
|
||||
sort_field: Field to sort by ('created_at', 'updated_at', 'id')
|
||||
sort_direction: Sort direction ('asc' or 'desc')
|
||||
"""
|
||||
|
||||
status_filter: Optional[DocStatus] = Field(
|
||||
default=None, description="Filter by document status, None for all statuses"
|
||||
)
|
||||
page: int = Field(default=1, ge=1, description="Page number (1-based)")
|
||||
page_size: int = Field(
|
||||
default=50, ge=10, le=200, description="Number of documents per page (10-200)"
|
||||
)
|
||||
sort_field: Literal["created_at", "updated_at", "id", "file_path"] = Field(
|
||||
default="updated_at", description="Field to sort by"
|
||||
)
|
||||
sort_direction: Literal["asc", "desc"] = Field(
|
||||
default="desc", description="Sort direction"
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"status_filter": "PROCESSED",
|
||||
"page": 1,
|
||||
"page_size": 50,
|
||||
"sort_field": "updated_at",
|
||||
"sort_direction": "desc",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PaginationInfo(BaseModel):
|
||||
"""Pagination information
|
||||
|
||||
Attributes:
|
||||
page: Current page number
|
||||
page_size: Number of items per page
|
||||
total_count: Total number of items
|
||||
total_pages: Total number of pages
|
||||
has_next: Whether there is a next page
|
||||
has_prev: Whether there is a previous page
|
||||
"""
|
||||
|
||||
page: int = Field(description="Current page number")
|
||||
page_size: int = Field(description="Number of items per page")
|
||||
total_count: int = Field(description="Total number of items")
|
||||
total_pages: int = Field(description="Total number of pages")
|
||||
has_next: bool = Field(description="Whether there is a next page")
|
||||
has_prev: bool = Field(description="Whether there is a previous page")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"page": 1,
|
||||
"page_size": 50,
|
||||
"total_count": 150,
|
||||
"total_pages": 3,
|
||||
"has_next": True,
|
||||
"has_prev": False,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PaginatedDocsResponse(BaseModel):
|
||||
"""Response model for paginated document queries
|
||||
|
||||
Attributes:
|
||||
documents: List of documents for the current page
|
||||
pagination: Pagination information
|
||||
status_counts: Count of documents by status for all documents
|
||||
"""
|
||||
|
||||
documents: List[DocStatusResponse] = Field(
|
||||
description="List of documents for the current page"
|
||||
)
|
||||
pagination: PaginationInfo = Field(description="Pagination information")
|
||||
status_counts: Dict[str, int] = Field(
|
||||
description="Count of documents by status for all documents"
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"documents": [
|
||||
{
|
||||
"id": "doc_123456",
|
||||
"content_summary": "Research paper on machine learning",
|
||||
"content_length": 15240,
|
||||
"status": "PROCESSED",
|
||||
"created_at": "2025-03-31T12:34:56",
|
||||
"updated_at": "2025-03-31T12:35:30",
|
||||
"track_id": "upload_20250729_170612_abc123",
|
||||
"chunks_count": 12,
|
||||
"error_msg": None,
|
||||
"metadata": {"author": "John Doe", "year": 2025},
|
||||
"file_path": "research_paper.pdf",
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"page_size": 50,
|
||||
"total_count": 150,
|
||||
"total_pages": 3,
|
||||
"has_next": True,
|
||||
"has_prev": False,
|
||||
},
|
||||
"status_counts": {
|
||||
"PENDING": 10,
|
||||
"PROCESSING": 5,
|
||||
"PROCESSED": 130,
|
||||
"FAILED": 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class StatusCountsResponse(BaseModel):
|
||||
"""Response model for document status counts
|
||||
|
||||
Attributes:
|
||||
status_counts: Count of documents by status
|
||||
"""
|
||||
|
||||
status_counts: Dict[str, int] = Field(description="Count of documents by status")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"status_counts": {
|
||||
"PENDING": 10,
|
||||
"PROCESSING": 5,
|
||||
"PROCESSED": 130,
|
||||
"FAILED": 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PipelineStatusResponse(BaseModel):
|
||||
"""Response model for pipeline status
|
||||
|
||||
|
@ -549,14 +753,17 @@ class DocumentManager:
|
|||
return any(filename.lower().endswith(ext) for ext in self.supported_extensions)
|
||||
|
||||
|
||||
async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
||||
async def pipeline_enqueue_file(
|
||||
rag: LightRAG, file_path: Path, track_id: str = None
|
||||
) -> tuple[bool, str]:
|
||||
"""Add a file to the queue for processing
|
||||
|
||||
Args:
|
||||
rag: LightRAG instance
|
||||
file_path: Path to the saved file
|
||||
track_id: Optional tracking ID, if not provided will be generated
|
||||
Returns:
|
||||
bool: True if the file was successfully enqueued, False otherwise
|
||||
tuple: (success: bool, track_id: str)
|
||||
"""
|
||||
|
||||
try:
|
||||
|
@ -730,9 +937,16 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
|||
f"File contains only whitespace characters. file_paths={file_path.name}"
|
||||
)
|
||||
|
||||
await rag.apipeline_enqueue_documents(content, file_paths=file_path.name)
|
||||
# Generate track_id if not provided
|
||||
if track_id is None:
|
||||
track_id = generate_track_id("unkown")
|
||||
|
||||
await rag.apipeline_enqueue_documents(
|
||||
content, file_paths=file_path.name, track_id=track_id
|
||||
)
|
||||
|
||||
logger.info(f"Successfully fetched and enqueued file: {file_path.name}")
|
||||
return True
|
||||
return True, track_id
|
||||
else:
|
||||
logger.error(f"No content could be extracted from file: {file_path.name}")
|
||||
|
||||
|
@ -745,18 +959,22 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
|||
file_path.unlink()
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting file {file_path}: {str(e)}")
|
||||
return False
|
||||
return False, ""
|
||||
|
||||
|
||||
async def pipeline_index_file(rag: LightRAG, file_path: Path):
|
||||
"""Index a file
|
||||
async def pipeline_index_file(rag: LightRAG, file_path: Path, track_id: str = None):
|
||||
"""Index a file with track_id
|
||||
|
||||
Args:
|
||||
rag: LightRAG instance
|
||||
file_path: Path to the saved file
|
||||
track_id: Optional tracking ID
|
||||
"""
|
||||
try:
|
||||
if await pipeline_enqueue_file(rag, file_path):
|
||||
success, returned_track_id = await pipeline_enqueue_file(
|
||||
rag, file_path, track_id
|
||||
)
|
||||
if success:
|
||||
await rag.apipeline_process_enqueue_documents()
|
||||
|
||||
except Exception as e:
|
||||
|
@ -764,12 +982,15 @@ async def pipeline_index_file(rag: LightRAG, file_path: Path):
|
|||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
async def pipeline_index_files(rag: LightRAG, file_paths: List[Path]):
|
||||
async def pipeline_index_files(
|
||||
rag: LightRAG, file_paths: List[Path], track_id: str = None
|
||||
):
|
||||
"""Index multiple files sequentially to avoid high CPU load
|
||||
|
||||
Args:
|
||||
rag: LightRAG instance
|
||||
file_paths: Paths to the files to index
|
||||
track_id: Optional tracking ID to pass to all files
|
||||
"""
|
||||
if not file_paths:
|
||||
return
|
||||
|
@ -780,9 +1001,10 @@ async def pipeline_index_files(rag: LightRAG, file_paths: List[Path]):
|
|||
collator = Collator()
|
||||
sorted_file_paths = sorted(file_paths, key=lambda p: collator.sort_key(str(p)))
|
||||
|
||||
# Process files sequentially
|
||||
# Process files sequentially with track_id
|
||||
for file_path in sorted_file_paths:
|
||||
if await pipeline_enqueue_file(rag, file_path):
|
||||
success, _ = await pipeline_enqueue_file(rag, file_path, track_id)
|
||||
if success:
|
||||
enqueued = True
|
||||
|
||||
# Process the queue only if at least one file was successfully enqueued
|
||||
|
@ -794,14 +1016,18 @@ async def pipeline_index_files(rag: LightRAG, file_paths: List[Path]):
|
|||
|
||||
|
||||
async def pipeline_index_texts(
|
||||
rag: LightRAG, texts: List[str], file_sources: List[str] = None
|
||||
rag: LightRAG,
|
||||
texts: List[str],
|
||||
file_sources: List[str] = None,
|
||||
track_id: str = None,
|
||||
):
|
||||
"""Index a list of texts
|
||||
"""Index a list of texts with track_id
|
||||
|
||||
Args:
|
||||
rag: LightRAG instance
|
||||
texts: The texts to index
|
||||
file_sources: Sources of the texts
|
||||
track_id: Optional tracking ID
|
||||
"""
|
||||
if not texts:
|
||||
return
|
||||
|
@ -811,36 +1037,22 @@ async def pipeline_index_texts(
|
|||
file_sources.append("unknown_source")
|
||||
for _ in range(len(file_sources), len(texts))
|
||||
]
|
||||
await rag.apipeline_enqueue_documents(input=texts, file_paths=file_sources)
|
||||
await rag.apipeline_enqueue_documents(
|
||||
input=texts, file_paths=file_sources, track_id=track_id
|
||||
)
|
||||
await rag.apipeline_process_enqueue_documents()
|
||||
|
||||
|
||||
# TODO: deprecate after /insert_file is removed
|
||||
async def save_temp_file(input_dir: Path, file: UploadFile = File(...)) -> Path:
|
||||
"""Save the uploaded file to a temporary location
|
||||
async def run_scanning_process(
|
||||
rag: LightRAG, doc_manager: DocumentManager, track_id: str = None
|
||||
):
|
||||
"""Background task to scan and index documents
|
||||
|
||||
Args:
|
||||
file: The uploaded file
|
||||
|
||||
Returns:
|
||||
Path: The path to the saved file
|
||||
rag: LightRAG instance
|
||||
doc_manager: DocumentManager instance
|
||||
track_id: Optional tracking ID to pass to all scanned files
|
||||
"""
|
||||
# Generate unique filename to avoid conflicts
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
unique_filename = f"{temp_prefix}{timestamp}_{file.filename}"
|
||||
|
||||
# Create a temporary file to save the uploaded content
|
||||
temp_path = input_dir / "temp" / unique_filename
|
||||
temp_path.parent.mkdir(exist_ok=True)
|
||||
|
||||
# Save the file
|
||||
with open(temp_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
return temp_path
|
||||
|
||||
|
||||
async def run_scanning_process(rag: LightRAG, doc_manager: DocumentManager):
|
||||
"""Background task to scan and index documents"""
|
||||
try:
|
||||
new_files = doc_manager.scan_directory_for_new_files()
|
||||
total_files = len(new_files)
|
||||
|
@ -849,8 +1061,8 @@ async def run_scanning_process(rag: LightRAG, doc_manager: DocumentManager):
|
|||
if not new_files:
|
||||
return
|
||||
|
||||
# Process all files at once
|
||||
await pipeline_index_files(rag, new_files)
|
||||
# Process all files at once with track_id
|
||||
await pipeline_index_files(rag, new_files, track_id)
|
||||
logger.info(f"Scanning process completed: {total_files} files Processed.")
|
||||
|
||||
except Exception as e:
|
||||
|
@ -1031,13 +1243,17 @@ def create_document_routes(
|
|||
that fact.
|
||||
|
||||
Returns:
|
||||
ScanResponse: A response object containing the scanning status
|
||||
ScanResponse: A response object containing the scanning status and track_id
|
||||
"""
|
||||
# Start the scanning process in the background
|
||||
background_tasks.add_task(run_scanning_process, rag, doc_manager)
|
||||
# Generate track_id with "scan" prefix for scanning operation
|
||||
track_id = generate_track_id("scan")
|
||||
|
||||
# Start the scanning process in the background with track_id
|
||||
background_tasks.add_task(run_scanning_process, rag, doc_manager, track_id)
|
||||
return ScanResponse(
|
||||
status="scanning_started",
|
||||
message="Scanning process has been initiated in the background",
|
||||
track_id=track_id,
|
||||
)
|
||||
|
||||
@router.post(
|
||||
|
@ -1080,18 +1296,23 @@ def create_document_routes(
|
|||
return InsertResponse(
|
||||
status="duplicated",
|
||||
message=f"File '{safe_filename}' already exists in the input directory.",
|
||||
track_id="",
|
||||
)
|
||||
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# Add to background tasks
|
||||
background_tasks.add_task(pipeline_index_file, rag, file_path)
|
||||
track_id = generate_track_id("upload")
|
||||
|
||||
# Add to background tasks and get track_id
|
||||
background_tasks.add_task(pipeline_index_file, rag, file_path, track_id)
|
||||
|
||||
return InsertResponse(
|
||||
status="success",
|
||||
message=f"File '{safe_filename}' uploaded successfully. Processing will continue in background.",
|
||||
track_id=track_id,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error /documents/upload: {file.filename}: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
@ -1120,15 +1341,21 @@ def create_document_routes(
|
|||
HTTPException: If an error occurs during text processing (500).
|
||||
"""
|
||||
try:
|
||||
# Generate track_id for text insertion
|
||||
track_id = generate_track_id("insert")
|
||||
|
||||
background_tasks.add_task(
|
||||
pipeline_index_texts,
|
||||
rag,
|
||||
[request.text],
|
||||
file_sources=[request.file_source],
|
||||
track_id=track_id,
|
||||
)
|
||||
|
||||
return InsertResponse(
|
||||
status="success",
|
||||
message="Text successfully received. Processing will continue in background.",
|
||||
track_id=track_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error /documents/text: {str(e)}")
|
||||
|
@ -1160,18 +1387,24 @@ def create_document_routes(
|
|||
HTTPException: If an error occurs during text processing (500).
|
||||
"""
|
||||
try:
|
||||
# Generate track_id for texts insertion
|
||||
track_id = generate_track_id("insert")
|
||||
|
||||
background_tasks.add_task(
|
||||
pipeline_index_texts,
|
||||
rag,
|
||||
request.texts,
|
||||
file_sources=request.file_sources,
|
||||
track_id=track_id,
|
||||
)
|
||||
|
||||
return InsertResponse(
|
||||
status="success",
|
||||
message="Text successfully received. Processing will continue in background.",
|
||||
message="Texts successfully received. Processing will continue in background.",
|
||||
track_id=track_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error /documents/text: {str(e)}")
|
||||
logger.error(f"Error /documents/texts: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
@ -1473,8 +1706,9 @@ def create_document_routes(
|
|||
status=doc_status.status,
|
||||
created_at=format_datetime(doc_status.created_at),
|
||||
updated_at=format_datetime(doc_status.updated_at),
|
||||
track_id=doc_status.track_id,
|
||||
chunks_count=doc_status.chunks_count,
|
||||
error=doc_status.error,
|
||||
error_msg=doc_status.error_msg,
|
||||
metadata=doc_status.metadata,
|
||||
file_path=doc_status.file_path,
|
||||
)
|
||||
|
@ -1699,4 +1933,192 @@ def create_document_routes(
|
|||
logger.error(traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=error_msg)
|
||||
|
||||
@router.get(
|
||||
"/track_status/{track_id}",
|
||||
response_model=TrackStatusResponse,
|
||||
dependencies=[Depends(combined_auth)],
|
||||
)
|
||||
async def get_track_status(track_id: str) -> TrackStatusResponse:
|
||||
"""
|
||||
Get the processing status of documents by tracking ID.
|
||||
|
||||
This endpoint retrieves all documents associated with a specific tracking ID,
|
||||
allowing users to monitor the processing progress of their uploaded files or inserted texts.
|
||||
|
||||
Args:
|
||||
track_id (str): The tracking ID returned from upload, text, or texts endpoints
|
||||
|
||||
Returns:
|
||||
TrackStatusResponse: A response object containing:
|
||||
- track_id: The tracking ID
|
||||
- documents: List of documents associated with this track_id
|
||||
- total_count: Total number of documents for this track_id
|
||||
|
||||
Raises:
|
||||
HTTPException: If track_id is invalid (400) or an error occurs (500).
|
||||
"""
|
||||
try:
|
||||
# Validate track_id
|
||||
if not track_id or not track_id.strip():
|
||||
raise HTTPException(status_code=400, detail="Track ID cannot be empty")
|
||||
|
||||
track_id = track_id.strip()
|
||||
|
||||
# Get documents by track_id
|
||||
docs_by_track_id = await rag.aget_docs_by_track_id(track_id)
|
||||
|
||||
# Convert to response format
|
||||
documents = []
|
||||
status_summary = {}
|
||||
|
||||
for doc_id, doc_status in docs_by_track_id.items():
|
||||
documents.append(
|
||||
DocStatusResponse(
|
||||
id=doc_id,
|
||||
content_summary=doc_status.content_summary,
|
||||
content_length=doc_status.content_length,
|
||||
status=doc_status.status,
|
||||
created_at=format_datetime(doc_status.created_at),
|
||||
updated_at=format_datetime(doc_status.updated_at),
|
||||
track_id=doc_status.track_id,
|
||||
chunks_count=doc_status.chunks_count,
|
||||
error_msg=doc_status.error_msg,
|
||||
metadata=doc_status.metadata,
|
||||
file_path=doc_status.file_path,
|
||||
)
|
||||
)
|
||||
|
||||
# Build status summary
|
||||
# Handle both DocStatus enum and string cases for robust deserialization
|
||||
status_key = str(doc_status.status)
|
||||
status_summary[status_key] = status_summary.get(status_key, 0) + 1
|
||||
|
||||
return TrackStatusResponse(
|
||||
track_id=track_id,
|
||||
documents=documents,
|
||||
total_count=len(documents),
|
||||
status_summary=status_summary,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting track status for {track_id}: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post(
|
||||
"/paginated",
|
||||
response_model=PaginatedDocsResponse,
|
||||
dependencies=[Depends(combined_auth)],
|
||||
)
|
||||
async def get_documents_paginated(
|
||||
request: DocumentsRequest,
|
||||
) -> PaginatedDocsResponse:
|
||||
"""
|
||||
Get documents with pagination support.
|
||||
|
||||
This endpoint retrieves documents with pagination, filtering, and sorting capabilities.
|
||||
It provides better performance for large document collections by loading only the
|
||||
requested page of data.
|
||||
|
||||
Args:
|
||||
request (DocumentsRequest): The request body containing pagination parameters
|
||||
|
||||
Returns:
|
||||
PaginatedDocsResponse: A response object containing:
|
||||
- documents: List of documents for the current page
|
||||
- pagination: Pagination information (page, total_count, etc.)
|
||||
- status_counts: Count of documents by status for all documents
|
||||
|
||||
Raises:
|
||||
HTTPException: If an error occurs while retrieving documents (500).
|
||||
"""
|
||||
try:
|
||||
# Get paginated documents and status counts in parallel
|
||||
docs_task = rag.doc_status.get_docs_paginated(
|
||||
status_filter=request.status_filter,
|
||||
page=request.page,
|
||||
page_size=request.page_size,
|
||||
sort_field=request.sort_field,
|
||||
sort_direction=request.sort_direction,
|
||||
)
|
||||
status_counts_task = rag.doc_status.get_all_status_counts()
|
||||
|
||||
# Execute both queries in parallel
|
||||
(documents_with_ids, total_count), status_counts = await asyncio.gather(
|
||||
docs_task, status_counts_task
|
||||
)
|
||||
|
||||
# Convert documents to response format
|
||||
doc_responses = []
|
||||
for doc_id, doc in documents_with_ids:
|
||||
doc_responses.append(
|
||||
DocStatusResponse(
|
||||
id=doc_id,
|
||||
content_summary=doc.content_summary,
|
||||
content_length=doc.content_length,
|
||||
status=doc.status,
|
||||
created_at=format_datetime(doc.created_at),
|
||||
updated_at=format_datetime(doc.updated_at),
|
||||
track_id=doc.track_id,
|
||||
chunks_count=doc.chunks_count,
|
||||
error_msg=doc.error_msg,
|
||||
metadata=doc.metadata,
|
||||
file_path=doc.file_path,
|
||||
)
|
||||
)
|
||||
|
||||
# Calculate pagination info
|
||||
total_pages = (total_count + request.page_size - 1) // request.page_size
|
||||
has_next = request.page < total_pages
|
||||
has_prev = request.page > 1
|
||||
|
||||
pagination = PaginationInfo(
|
||||
page=request.page,
|
||||
page_size=request.page_size,
|
||||
total_count=total_count,
|
||||
total_pages=total_pages,
|
||||
has_next=has_next,
|
||||
has_prev=has_prev,
|
||||
)
|
||||
|
||||
return PaginatedDocsResponse(
|
||||
documents=doc_responses,
|
||||
pagination=pagination,
|
||||
status_counts=status_counts,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting paginated documents: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get(
|
||||
"/status_counts",
|
||||
response_model=StatusCountsResponse,
|
||||
dependencies=[Depends(combined_auth)],
|
||||
)
|
||||
async def get_document_status_counts() -> StatusCountsResponse:
|
||||
"""
|
||||
Get counts of documents by status.
|
||||
|
||||
This endpoint retrieves the count of documents in each processing status
|
||||
(PENDING, PROCESSING, PROCESSED, FAILED) for all documents in the system.
|
||||
|
||||
Returns:
|
||||
StatusCountsResponse: A response object containing status counts
|
||||
|
||||
Raises:
|
||||
HTTPException: If an error occurs while retrieving status counts (500).
|
||||
"""
|
||||
try:
|
||||
status_counts = await rag.doc_status.get_all_status_counts()
|
||||
return StatusCountsResponse(status_counts=status_counts)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting document status counts: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
return router
|
||||
|
|
|
@ -11,7 +11,7 @@ import asyncio
|
|||
from ascii_colors import trace_exception
|
||||
from lightrag import LightRAG, QueryParam
|
||||
from lightrag.utils import TiktokenTokenizer
|
||||
from lightrag.api.utils_api import ollama_server_infos, get_combined_auth_dependency
|
||||
from lightrag.api.utils_api import get_combined_auth_dependency
|
||||
from fastapi import Depends
|
||||
|
||||
|
||||
|
@ -221,7 +221,7 @@ def parse_query_mode(query: str) -> tuple[str, SearchMode, bool, Optional[str]]:
|
|||
class OllamaAPI:
|
||||
def __init__(self, rag: LightRAG, top_k: int = 60, api_key: Optional[str] = None):
|
||||
self.rag = rag
|
||||
self.ollama_server_infos = ollama_server_infos
|
||||
self.ollama_server_infos = rag.ollama_server_infos
|
||||
self.top_k = top_k
|
||||
self.api_key = api_key
|
||||
self.router = APIRouter(tags=["ollama"])
|
||||
|
|
|
@ -268,8 +268,6 @@ def display_splash_screen(args: argparse.Namespace) -> None:
|
|||
ASCIIColors.yellow(f"{args.summary_language}")
|
||||
ASCIIColors.white(" ├─ Max Parallel Insert: ", end="")
|
||||
ASCIIColors.yellow(f"{args.max_parallel_insert}")
|
||||
ASCIIColors.white(" ├─ Max Embed Tokens: ", end="")
|
||||
ASCIIColors.yellow(f"{args.max_embed_tokens}")
|
||||
ASCIIColors.white(" ├─ Chunk Size: ", end="")
|
||||
ASCIIColors.yellow(f"{args.chunk_size}")
|
||||
ASCIIColors.white(" ├─ Chunk Overlap Size: ", end="")
|
||||
|
|
|
@ -1 +1 @@
|
|||
import{e as v,c as b,g as m,k as O,h as P,j as p,l as w,m as c,n as x,t as A,o as N}from"./_baseUniq-BmMh76ze.js";import{aU as g,aq as _,aV as $,aW as E,aX as F,aY as I,aZ as M,a_ as y,a$ as B,b0 as T}from"./mermaid-vendor-CY273lNM.js";var S=/\s/;function q(n){for(var r=n.length;r--&&S.test(n.charAt(r)););return r}var G=/^\s+/;function H(n){return n&&n.slice(0,q(n)+1).replace(G,"")}var o=NaN,L=/^[-+]0x[0-9a-f]+$/i,R=/^0b[01]+$/i,W=/^0o[0-7]+$/i,X=parseInt;function Y(n){if(typeof n=="number")return n;if(v(n))return o;if(g(n)){var r=typeof n.valueOf=="function"?n.valueOf():n;n=g(r)?r+"":r}if(typeof n!="string")return n===0?n:+n;n=H(n);var t=R.test(n);return t||W.test(n)?X(n.slice(2),t?2:8):L.test(n)?o:+n}var z=1/0,C=17976931348623157e292;function K(n){if(!n)return n===0?n:0;if(n=Y(n),n===z||n===-1/0){var r=n<0?-1:1;return r*C}return n===n?n:0}function U(n){var r=K(n),t=r%1;return r===r?t?r-t:r:0}function fn(n){var r=n==null?0:n.length;return r?b(n):[]}var l=Object.prototype,Z=l.hasOwnProperty,dn=_(function(n,r){n=Object(n);var t=-1,e=r.length,a=e>2?r[2]:void 0;for(a&&$(r[0],r[1],a)&&(e=1);++t<e;)for(var f=r[t],i=E(f),s=-1,d=i.length;++s<d;){var u=i[s],h=n[u];(h===void 0||F(h,l[u])&&!Z.call(n,u))&&(n[u]=f[u])}return n});function un(n){var r=n==null?0:n.length;return r?n[r-1]:void 0}function D(n){return function(r,t,e){var a=Object(r);if(!I(r)){var f=m(t);r=O(r),t=function(s){return f(a[s],s,a)}}var i=n(r,t,e);return i>-1?a[f?r[i]:i]:void 0}}var J=Math.max;function Q(n,r,t){var e=n==null?0:n.length;if(!e)return-1;var a=t==null?0:U(t);return a<0&&(a=J(e+a,0)),P(n,m(r),a)}var hn=D(Q);function V(n,r){var t=-1,e=I(n)?Array(n.length):[];return p(n,function(a,f,i){e[++t]=r(a,f,i)}),e}function gn(n,r){var t=M(n)?w:V;return t(n,m(r))}var j=Object.prototype,k=j.hasOwnProperty;function nn(n,r){return n!=null&&k.call(n,r)}function mn(n,r){return n!=null&&c(n,r,nn)}function rn(n,r){return n<r}function tn(n,r,t){for(var e=-1,a=n.length;++e<a;){var f=n[e],i=r(f);if(i!=null&&(s===void 0?i===i&&!v(i):t(i,s)))var s=i,d=f}return d}function on(n){return n&&n.length?tn(n,y,rn):void 0}function an(n,r,t,e){if(!g(n))return n;r=x(r,n);for(var a=-1,f=r.length,i=f-1,s=n;s!=null&&++a<f;){var d=A(r[a]),u=t;if(d==="__proto__"||d==="constructor"||d==="prototype")return n;if(a!=i){var h=s[d];u=void 0,u===void 0&&(u=g(h)?h:B(r[a+1])?[]:{})}T(s,d,u),s=s[d]}return n}function vn(n,r,t){for(var e=-1,a=r.length,f={};++e<a;){var i=r[e],s=N(n,i);t(s,i)&&an(f,x(i,n),s)}return f}export{rn as a,tn as b,V as c,vn as d,on as e,fn as f,hn as g,mn as h,dn as i,U as j,un as l,gn as m,K as t};
|
||||
import{e as v,c as b,g as m,k as O,h as P,j as p,l as w,m as c,n as x,t as A,o as N}from"./_baseUniq-D-427rzS.js";import{aU as g,aq as _,aV as $,aW as E,aX as F,aY as I,aZ as M,a_ as y,a$ as B,b0 as T}from"./mermaid-vendor-DGPC_TDM.js";var S=/\s/;function q(n){for(var r=n.length;r--&&S.test(n.charAt(r)););return r}var G=/^\s+/;function H(n){return n&&n.slice(0,q(n)+1).replace(G,"")}var o=NaN,L=/^[-+]0x[0-9a-f]+$/i,R=/^0b[01]+$/i,W=/^0o[0-7]+$/i,X=parseInt;function Y(n){if(typeof n=="number")return n;if(v(n))return o;if(g(n)){var r=typeof n.valueOf=="function"?n.valueOf():n;n=g(r)?r+"":r}if(typeof n!="string")return n===0?n:+n;n=H(n);var t=R.test(n);return t||W.test(n)?X(n.slice(2),t?2:8):L.test(n)?o:+n}var z=1/0,C=17976931348623157e292;function K(n){if(!n)return n===0?n:0;if(n=Y(n),n===z||n===-1/0){var r=n<0?-1:1;return r*C}return n===n?n:0}function U(n){var r=K(n),t=r%1;return r===r?t?r-t:r:0}function fn(n){var r=n==null?0:n.length;return r?b(n):[]}var l=Object.prototype,Z=l.hasOwnProperty,dn=_(function(n,r){n=Object(n);var t=-1,e=r.length,a=e>2?r[2]:void 0;for(a&&$(r[0],r[1],a)&&(e=1);++t<e;)for(var f=r[t],i=E(f),s=-1,d=i.length;++s<d;){var u=i[s],h=n[u];(h===void 0||F(h,l[u])&&!Z.call(n,u))&&(n[u]=f[u])}return n});function un(n){var r=n==null?0:n.length;return r?n[r-1]:void 0}function D(n){return function(r,t,e){var a=Object(r);if(!I(r)){var f=m(t);r=O(r),t=function(s){return f(a[s],s,a)}}var i=n(r,t,e);return i>-1?a[f?r[i]:i]:void 0}}var J=Math.max;function Q(n,r,t){var e=n==null?0:n.length;if(!e)return-1;var a=t==null?0:U(t);return a<0&&(a=J(e+a,0)),P(n,m(r),a)}var hn=D(Q);function V(n,r){var t=-1,e=I(n)?Array(n.length):[];return p(n,function(a,f,i){e[++t]=r(a,f,i)}),e}function gn(n,r){var t=M(n)?w:V;return t(n,m(r))}var j=Object.prototype,k=j.hasOwnProperty;function nn(n,r){return n!=null&&k.call(n,r)}function mn(n,r){return n!=null&&c(n,r,nn)}function rn(n,r){return n<r}function tn(n,r,t){for(var e=-1,a=n.length;++e<a;){var f=n[e],i=r(f);if(i!=null&&(s===void 0?i===i&&!v(i):t(i,s)))var s=i,d=f}return d}function on(n){return n&&n.length?tn(n,y,rn):void 0}function an(n,r,t,e){if(!g(n))return n;r=x(r,n);for(var a=-1,f=r.length,i=f-1,s=n;s!=null&&++a<f;){var d=A(r[a]),u=t;if(d==="__proto__"||d==="constructor"||d==="prototype")return n;if(a!=i){var h=s[d];u=void 0,u===void 0&&(u=g(h)?h:B(r[a+1])?[]:{})}T(s,d,u),s=s[d]}return n}function vn(n,r,t){for(var e=-1,a=r.length,f={};++e<a;){var i=r[e],s=N(n,i);t(s,i)&&an(f,x(i,n),s)}return f}export{rn as a,tn as b,V as c,vn as d,on as e,fn as f,hn as g,mn as h,dn as i,U as j,un as l,gn as m,K as t};
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
import{_ as l}from"./mermaid-vendor-CY273lNM.js";function m(e,c){var i,t,o;e.accDescr&&((i=c.setAccDescription)==null||i.call(c,e.accDescr)),e.accTitle&&((t=c.setAccTitle)==null||t.call(c,e.accTitle)),e.title&&((o=c.setDiagramTitle)==null||o.call(c,e.title))}l(m,"populateCommonDb");export{m as p};
|
||||
import{_ as l}from"./mermaid-vendor-DGPC_TDM.js";function m(e,c){var i,t,o;e.accDescr&&((i=c.setAccDescription)==null||i.call(c,e.accDescr)),e.accTitle&&((t=c.setAccTitle)==null||t.call(c,e.accTitle)),e.title&&((o=c.setDiagramTitle)==null||o.call(c,e.title))}l(m,"populateCommonDb");export{m as p};
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
import{_ as n,a1 as x,j as l}from"./mermaid-vendor-CY273lNM.js";var c=n((a,t)=>{const e=a.append("rect");if(e.attr("x",t.x),e.attr("y",t.y),e.attr("fill",t.fill),e.attr("stroke",t.stroke),e.attr("width",t.width),e.attr("height",t.height),t.name&&e.attr("name",t.name),t.rx&&e.attr("rx",t.rx),t.ry&&e.attr("ry",t.ry),t.attrs!==void 0)for(const r in t.attrs)e.attr(r,t.attrs[r]);return t.class&&e.attr("class",t.class),e},"drawRect"),d=n((a,t)=>{const e={x:t.startx,y:t.starty,width:t.stopx-t.startx,height:t.stopy-t.starty,fill:t.fill,stroke:t.stroke,class:"rect"};c(a,e).lower()},"drawBackgroundRect"),g=n((a,t)=>{const e=t.text.replace(x," "),r=a.append("text");r.attr("x",t.x),r.attr("y",t.y),r.attr("class","legend"),r.style("text-anchor",t.anchor),t.class&&r.attr("class",t.class);const s=r.append("tspan");return s.attr("x",t.x+t.textMargin*2),s.text(e),r},"drawText"),h=n((a,t,e,r)=>{const s=a.append("image");s.attr("x",t),s.attr("y",e);const i=l.sanitizeUrl(r);s.attr("xlink:href",i)},"drawImage"),m=n((a,t,e,r)=>{const s=a.append("use");s.attr("x",t),s.attr("y",e);const i=l.sanitizeUrl(r);s.attr("xlink:href",`#${i}`)},"drawEmbeddedImage"),y=n(()=>({x:0,y:0,width:100,height:100,fill:"#EDF2AE",stroke:"#666",anchor:"start",rx:0,ry:0}),"getNoteRect"),p=n(()=>({x:0,y:0,width:100,height:100,"text-anchor":"start",style:"#666",textMargin:0,rx:0,ry:0,tspan:!0}),"getTextObj");export{d as a,p as b,m as c,c as d,h as e,g as f,y as g};
|
||||
import{_ as n,a1 as x,j as l}from"./mermaid-vendor-DGPC_TDM.js";var c=n((a,t)=>{const e=a.append("rect");if(e.attr("x",t.x),e.attr("y",t.y),e.attr("fill",t.fill),e.attr("stroke",t.stroke),e.attr("width",t.width),e.attr("height",t.height),t.name&&e.attr("name",t.name),t.rx&&e.attr("rx",t.rx),t.ry&&e.attr("ry",t.ry),t.attrs!==void 0)for(const r in t.attrs)e.attr(r,t.attrs[r]);return t.class&&e.attr("class",t.class),e},"drawRect"),d=n((a,t)=>{const e={x:t.startx,y:t.starty,width:t.stopx-t.startx,height:t.stopy-t.starty,fill:t.fill,stroke:t.stroke,class:"rect"};c(a,e).lower()},"drawBackgroundRect"),g=n((a,t)=>{const e=t.text.replace(x," "),r=a.append("text");r.attr("x",t.x),r.attr("y",t.y),r.attr("class","legend"),r.style("text-anchor",t.anchor),t.class&&r.attr("class",t.class);const s=r.append("tspan");return s.attr("x",t.x+t.textMargin*2),s.text(e),r},"drawText"),h=n((a,t,e,r)=>{const s=a.append("image");s.attr("x",t),s.attr("y",e);const i=l.sanitizeUrl(r);s.attr("xlink:href",i)},"drawImage"),m=n((a,t,e,r)=>{const s=a.append("use");s.attr("x",t),s.attr("y",e);const i=l.sanitizeUrl(r);s.attr("xlink:href",`#${i}`)},"drawEmbeddedImage"),y=n(()=>({x:0,y:0,width:100,height:100,fill:"#EDF2AE",stroke:"#666",anchor:"start",rx:0,ry:0}),"getNoteRect"),p=n(()=>({x:0,y:0,width:100,height:100,"text-anchor":"start",style:"#666",textMargin:0,rx:0,ry:0,tspan:!0}),"getTextObj");export{d as a,p as b,m as c,c as d,h as e,g as f,y as g};
|
|
@ -1 +1 @@
|
|||
import{_ as n,d as r,e as d,l as g}from"./mermaid-vendor-CY273lNM.js";var u=n((e,t)=>{let o;return t==="sandbox"&&(o=r("#i"+e)),(t==="sandbox"?r(o.nodes()[0].contentDocument.body):r("body")).select(`[id="${e}"]`)},"getDiagramElement"),b=n((e,t,o,i)=>{e.attr("class",o);const{width:a,height:s,x:h,y:x}=l(e,t);d(e,s,a,i);const c=w(h,x,a,s,t);e.attr("viewBox",c),g.debug(`viewBox configured: ${c} with padding: ${t}`)},"setupViewPortForSVG"),l=n((e,t)=>{var i;const o=((i=e.node())==null?void 0:i.getBBox())||{width:0,height:0,x:0,y:0};return{width:o.width+t*2,height:o.height+t*2,x:o.x,y:o.y}},"calculateDimensionsWithPadding"),w=n((e,t,o,i,a)=>`${e-a} ${t-a} ${o} ${i}`,"createViewBox");export{u as g,b as s};
|
||||
import{_ as n,d as r,e as d,l as g}from"./mermaid-vendor-DGPC_TDM.js";var u=n((e,t)=>{let o;return t==="sandbox"&&(o=r("#i"+e)),(t==="sandbox"?r(o.nodes()[0].contentDocument.body):r("body")).select(`[id="${e}"]`)},"getDiagramElement"),b=n((e,t,o,i)=>{e.attr("class",o);const{width:a,height:s,x:h,y:x}=l(e,t);d(e,s,a,i);const c=w(h,x,a,s,t);e.attr("viewBox",c),g.debug(`viewBox configured: ${c} with padding: ${t}`)},"setupViewPortForSVG"),l=n((e,t)=>{var i;const o=((i=e.node())==null?void 0:i.getBBox())||{width:0,height:0,x:0,y:0};return{width:o.width+t*2,height:o.height+t*2,x:o.x,y:o.y}},"calculateDimensionsWithPadding"),w=n((e,t,o,i,a)=>`${e-a} ${t-a} ${o} ${i}`,"createViewBox");export{u as g,b as s};
|
|
@ -1 +1 @@
|
|||
import{_ as s}from"./mermaid-vendor-CY273lNM.js";var t,e=(t=class{constructor(i){this.init=i,this.records=this.init()}reset(){this.records=this.init()}},s(t,"ImperativeState"),t);export{e as I};
|
||||
import{_ as s}from"./mermaid-vendor-DGPC_TDM.js";var t,e=(t=class{constructor(i){this.init=i,this.records=this.init()}reset(){this.records=this.init()}},s(t,"ImperativeState"),t);export{e as I};
|
|
@ -1 +1 @@
|
|||
import{s as a,c as s,a as e,C as t}from"./chunk-A2AXSNBT-BECeRkdd.js";import{_ as i}from"./mermaid-vendor-CY273lNM.js";import"./chunk-RZ5BOZE2-BJpHTKlK.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var f={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{f as diagram};
|
||||
import{s as a,c as s,a as e,C as t}from"./chunk-A2AXSNBT-ZI6SIAYy.js";import{_ as i}from"./mermaid-vendor-DGPC_TDM.js";import"./chunk-RZ5BOZE2-BhCqM4LN.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var f={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{f as diagram};
|
|
@ -1 +1 @@
|
|||
import{s as a,c as s,a as e,C as t}from"./chunk-A2AXSNBT-BECeRkdd.js";import{_ as i}from"./mermaid-vendor-CY273lNM.js";import"./chunk-RZ5BOZE2-BJpHTKlK.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var f={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{f as diagram};
|
||||
import{s as a,c as s,a as e,C as t}from"./chunk-A2AXSNBT-ZI6SIAYy.js";import{_ as i}from"./mermaid-vendor-DGPC_TDM.js";import"./chunk-RZ5BOZE2-BhCqM4LN.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var f={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{f as diagram};
|
|
@ -0,0 +1 @@
|
|||
import{b as r}from"./_baseUniq-D-427rzS.js";var e=4;function a(o){return r(o,e)}export{a as c};
|
|
@ -1 +0,0 @@
|
|||
import{b as r}from"./_baseUniq-BmMh76ze.js";var e=4;function a(o){return r(o,e)}export{a as c};
|
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
import{p as k}from"./chunk-4BMEZGHF-DcXQJx5Q.js";import{_ as l,s as R,g as F,t as I,q as _,a as E,b as D,K as G,z,F as y,G as C,H as P,l as H,Q as V}from"./mermaid-vendor-CY273lNM.js";import{p as W}from"./radar-MK3ICKWK-D66UL2n2.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-BmMh76ze.js";import"./_basePickBy-deWrp6cQ.js";import"./clone-DjMnmZIs.js";var h={showLegend:!0,ticks:5,max:null,min:0,graticule:"circle"},w={axes:[],curves:[],options:h},g=structuredClone(w),B=P.radar,j=l(()=>y({...B,...C().radar}),"getConfig"),b=l(()=>g.axes,"getAxes"),q=l(()=>g.curves,"getCurves"),K=l(()=>g.options,"getOptions"),N=l(a=>{g.axes=a.map(t=>({name:t.name,label:t.label??t.name}))},"setAxes"),Q=l(a=>{g.curves=a.map(t=>({name:t.name,label:t.label??t.name,entries:U(t.entries)}))},"setCurves"),U=l(a=>{if(a[0].axis==null)return a.map(e=>e.value);const t=b();if(t.length===0)throw new Error("Axes must be populated before curves for reference entries");return t.map(e=>{const r=a.find(s=>{var o;return((o=s.axis)==null?void 0:o.$refText)===e.name});if(r===void 0)throw new Error("Missing entry for axis "+e.label);return r.value})},"computeCurveEntries"),X=l(a=>{var e,r,s,o,i;const t=a.reduce((n,c)=>(n[c.name]=c,n),{});g.options={showLegend:((e=t.showLegend)==null?void 0:e.value)??h.showLegend,ticks:((r=t.ticks)==null?void 0:r.value)??h.ticks,max:((s=t.max)==null?void 0:s.value)??h.max,min:((o=t.min)==null?void 0:o.value)??h.min,graticule:((i=t.graticule)==null?void 0:i.value)??h.graticule}},"setOptions"),Y=l(()=>{z(),g=structuredClone(w)},"clear"),$={getAxes:b,getCurves:q,getOptions:K,setAxes:N,setCurves:Q,setOptions:X,getConfig:j,clear:Y,setAccTitle:D,getAccTitle:E,setDiagramTitle:_,getDiagramTitle:I,getAccDescription:F,setAccDescription:R},Z=l(a=>{k(a,$);const{axes:t,curves:e,options:r}=a;$.setAxes(t),$.setCurves(e),$.setOptions(r)},"populate"),J={parse:l(async a=>{const t=await W("radar",a);H.debug(t),Z(t)},"parse")},tt=l((a,t,e,r)=>{const s=r.db,o=s.getAxes(),i=s.getCurves(),n=s.getOptions(),c=s.getConfig(),d=s.getDiagramTitle(),u=G(t),p=et(u,c),m=n.max??Math.max(...i.map(f=>Math.max(...f.entries))),x=n.min,v=Math.min(c.width,c.height)/2;at(p,o,v,n.ticks,n.graticule),rt(p,o,v,c),M(p,o,i,x,m,n.graticule,c),T(p,i,n.showLegend,c),p.append("text").attr("class","radarTitle").text(d).attr("x",0).attr("y",-c.height/2-c.marginTop)},"draw"),et=l((a,t)=>{const e=t.width+t.marginLeft+t.marginRight,r=t.height+t.marginTop+t.marginBottom,s={x:t.marginLeft+t.width/2,y:t.marginTop+t.height/2};return a.attr("viewbox",`0 0 ${e} ${r}`).attr("width",e).attr("height",r),a.append("g").attr("transform",`translate(${s.x}, ${s.y})`)},"drawFrame"),at=l((a,t,e,r,s)=>{if(s==="circle")for(let o=0;o<r;o++){const i=e*(o+1)/r;a.append("circle").attr("r",i).attr("class","radarGraticule")}else if(s==="polygon"){const o=t.length;for(let i=0;i<r;i++){const n=e*(i+1)/r,c=t.map((d,u)=>{const p=2*u*Math.PI/o-Math.PI/2,m=n*Math.cos(p),x=n*Math.sin(p);return`${m},${x}`}).join(" ");a.append("polygon").attr("points",c).attr("class","radarGraticule")}}},"drawGraticule"),rt=l((a,t,e,r)=>{const s=t.length;for(let o=0;o<s;o++){const i=t[o].label,n=2*o*Math.PI/s-Math.PI/2;a.append("line").attr("x1",0).attr("y1",0).attr("x2",e*r.axisScaleFactor*Math.cos(n)).attr("y2",e*r.axisScaleFactor*Math.sin(n)).attr("class","radarAxisLine"),a.append("text").text(i).attr("x",e*r.axisLabelFactor*Math.cos(n)).attr("y",e*r.axisLabelFactor*Math.sin(n)).attr("class","radarAxisLabel")}},"drawAxes");function M(a,t,e,r,s,o,i){const n=t.length,c=Math.min(i.width,i.height)/2;e.forEach((d,u)=>{if(d.entries.length!==n)return;const p=d.entries.map((m,x)=>{const v=2*Math.PI*x/n-Math.PI/2,f=A(m,r,s,c),O=f*Math.cos(v),S=f*Math.sin(v);return{x:O,y:S}});o==="circle"?a.append("path").attr("d",L(p,i.curveTension)).attr("class",`radarCurve-${u}`):o==="polygon"&&a.append("polygon").attr("points",p.map(m=>`${m.x},${m.y}`).join(" ")).attr("class",`radarCurve-${u}`)})}l(M,"drawCurves");function A(a,t,e,r){const s=Math.min(Math.max(a,t),e);return r*(s-t)/(e-t)}l(A,"relativeRadius");function L(a,t){const e=a.length;let r=`M${a[0].x},${a[0].y}`;for(let s=0;s<e;s++){const o=a[(s-1+e)%e],i=a[s],n=a[(s+1)%e],c=a[(s+2)%e],d={x:i.x+(n.x-o.x)*t,y:i.y+(n.y-o.y)*t},u={x:n.x-(c.x-i.x)*t,y:n.y-(c.y-i.y)*t};r+=` C${d.x},${d.y} ${u.x},${u.y} ${n.x},${n.y}`}return`${r} Z`}l(L,"closedRoundCurve");function T(a,t,e,r){if(!e)return;const s=(r.width/2+r.marginRight)*3/4,o=-(r.height/2+r.marginTop)*3/4,i=20;t.forEach((n,c)=>{const d=a.append("g").attr("transform",`translate(${s}, ${o+c*i})`);d.append("rect").attr("width",12).attr("height",12).attr("class",`radarLegendBox-${c}`),d.append("text").attr("x",16).attr("y",0).attr("class","radarLegendText").text(n.label)})}l(T,"drawLegend");var st={draw:tt},nt=l((a,t)=>{let e="";for(let r=0;r<a.THEME_COLOR_LIMIT;r++){const s=a[`cScale${r}`];e+=`
|
||||
import{p as k}from"./chunk-4BMEZGHF-CbUqLJH_.js";import{_ as l,s as R,g as F,t as I,q as _,a as E,b as D,K as G,z,F as y,G as C,H as P,l as H,Q as V}from"./mermaid-vendor-DGPC_TDM.js";import{p as W}from"./radar-MK3ICKWK-BkgLvISn.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-D-427rzS.js";import"./_basePickBy-CGXZVaMl.js";import"./clone-BvhE3BKU.js";var h={showLegend:!0,ticks:5,max:null,min:0,graticule:"circle"},w={axes:[],curves:[],options:h},g=structuredClone(w),B=P.radar,j=l(()=>y({...B,...C().radar}),"getConfig"),b=l(()=>g.axes,"getAxes"),q=l(()=>g.curves,"getCurves"),K=l(()=>g.options,"getOptions"),N=l(a=>{g.axes=a.map(t=>({name:t.name,label:t.label??t.name}))},"setAxes"),Q=l(a=>{g.curves=a.map(t=>({name:t.name,label:t.label??t.name,entries:U(t.entries)}))},"setCurves"),U=l(a=>{if(a[0].axis==null)return a.map(e=>e.value);const t=b();if(t.length===0)throw new Error("Axes must be populated before curves for reference entries");return t.map(e=>{const r=a.find(s=>{var o;return((o=s.axis)==null?void 0:o.$refText)===e.name});if(r===void 0)throw new Error("Missing entry for axis "+e.label);return r.value})},"computeCurveEntries"),X=l(a=>{var e,r,s,o,i;const t=a.reduce((n,c)=>(n[c.name]=c,n),{});g.options={showLegend:((e=t.showLegend)==null?void 0:e.value)??h.showLegend,ticks:((r=t.ticks)==null?void 0:r.value)??h.ticks,max:((s=t.max)==null?void 0:s.value)??h.max,min:((o=t.min)==null?void 0:o.value)??h.min,graticule:((i=t.graticule)==null?void 0:i.value)??h.graticule}},"setOptions"),Y=l(()=>{z(),g=structuredClone(w)},"clear"),$={getAxes:b,getCurves:q,getOptions:K,setAxes:N,setCurves:Q,setOptions:X,getConfig:j,clear:Y,setAccTitle:D,getAccTitle:E,setDiagramTitle:_,getDiagramTitle:I,getAccDescription:F,setAccDescription:R},Z=l(a=>{k(a,$);const{axes:t,curves:e,options:r}=a;$.setAxes(t),$.setCurves(e),$.setOptions(r)},"populate"),J={parse:l(async a=>{const t=await W("radar",a);H.debug(t),Z(t)},"parse")},tt=l((a,t,e,r)=>{const s=r.db,o=s.getAxes(),i=s.getCurves(),n=s.getOptions(),c=s.getConfig(),d=s.getDiagramTitle(),u=G(t),p=et(u,c),m=n.max??Math.max(...i.map(f=>Math.max(...f.entries))),x=n.min,v=Math.min(c.width,c.height)/2;at(p,o,v,n.ticks,n.graticule),rt(p,o,v,c),M(p,o,i,x,m,n.graticule,c),T(p,i,n.showLegend,c),p.append("text").attr("class","radarTitle").text(d).attr("x",0).attr("y",-c.height/2-c.marginTop)},"draw"),et=l((a,t)=>{const e=t.width+t.marginLeft+t.marginRight,r=t.height+t.marginTop+t.marginBottom,s={x:t.marginLeft+t.width/2,y:t.marginTop+t.height/2};return a.attr("viewbox",`0 0 ${e} ${r}`).attr("width",e).attr("height",r),a.append("g").attr("transform",`translate(${s.x}, ${s.y})`)},"drawFrame"),at=l((a,t,e,r,s)=>{if(s==="circle")for(let o=0;o<r;o++){const i=e*(o+1)/r;a.append("circle").attr("r",i).attr("class","radarGraticule")}else if(s==="polygon"){const o=t.length;for(let i=0;i<r;i++){const n=e*(i+1)/r,c=t.map((d,u)=>{const p=2*u*Math.PI/o-Math.PI/2,m=n*Math.cos(p),x=n*Math.sin(p);return`${m},${x}`}).join(" ");a.append("polygon").attr("points",c).attr("class","radarGraticule")}}},"drawGraticule"),rt=l((a,t,e,r)=>{const s=t.length;for(let o=0;o<s;o++){const i=t[o].label,n=2*o*Math.PI/s-Math.PI/2;a.append("line").attr("x1",0).attr("y1",0).attr("x2",e*r.axisScaleFactor*Math.cos(n)).attr("y2",e*r.axisScaleFactor*Math.sin(n)).attr("class","radarAxisLine"),a.append("text").text(i).attr("x",e*r.axisLabelFactor*Math.cos(n)).attr("y",e*r.axisLabelFactor*Math.sin(n)).attr("class","radarAxisLabel")}},"drawAxes");function M(a,t,e,r,s,o,i){const n=t.length,c=Math.min(i.width,i.height)/2;e.forEach((d,u)=>{if(d.entries.length!==n)return;const p=d.entries.map((m,x)=>{const v=2*Math.PI*x/n-Math.PI/2,f=A(m,r,s,c),O=f*Math.cos(v),S=f*Math.sin(v);return{x:O,y:S}});o==="circle"?a.append("path").attr("d",L(p,i.curveTension)).attr("class",`radarCurve-${u}`):o==="polygon"&&a.append("polygon").attr("points",p.map(m=>`${m.x},${m.y}`).join(" ")).attr("class",`radarCurve-${u}`)})}l(M,"drawCurves");function A(a,t,e,r){const s=Math.min(Math.max(a,t),e);return r*(s-t)/(e-t)}l(A,"relativeRadius");function L(a,t){const e=a.length;let r=`M${a[0].x},${a[0].y}`;for(let s=0;s<e;s++){const o=a[(s-1+e)%e],i=a[s],n=a[(s+1)%e],c=a[(s+2)%e],d={x:i.x+(n.x-o.x)*t,y:i.y+(n.y-o.y)*t},u={x:n.x-(c.x-i.x)*t,y:n.y-(c.y-i.y)*t};r+=` C${d.x},${d.y} ${u.x},${u.y} ${n.x},${n.y}`}return`${r} Z`}l(L,"closedRoundCurve");function T(a,t,e,r){if(!e)return;const s=(r.width/2+r.marginRight)*3/4,o=-(r.height/2+r.marginTop)*3/4,i=20;t.forEach((n,c)=>{const d=a.append("g").attr("transform",`translate(${s}, ${o+c*i})`);d.append("rect").attr("width",12).attr("height",12).attr("class",`radarLegendBox-${c}`),d.append("text").attr("x",16).attr("y",0).attr("class","radarLegendText").text(n.label)})}l(T,"drawLegend");var st={draw:tt},nt=l((a,t)=>{let e="";for(let r=0;r<a.THEME_COLOR_LIMIT;r++){const s=a[`cScale${r}`];e+=`
|
||||
.radarCurve-${r} {
|
||||
color: ${s};
|
||||
fill: ${s};
|
|
@ -1,4 +1,4 @@
|
|||
import{p as w}from"./chunk-4BMEZGHF-DcXQJx5Q.js";import{_ as n,s as B,g as S,t as F,q as z,a as P,b as W,F as x,K as T,e as D,z as _,G as A,H as E,l as v}from"./mermaid-vendor-CY273lNM.js";import{p as N}from"./radar-MK3ICKWK-D66UL2n2.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-BmMh76ze.js";import"./_basePickBy-deWrp6cQ.js";import"./clone-DjMnmZIs.js";var C={packet:[]},h=structuredClone(C),L=E.packet,Y=n(()=>{const t=x({...L,...A().packet});return t.showBits&&(t.paddingY+=10),t},"getConfig"),G=n(()=>h.packet,"getPacket"),H=n(t=>{t.length>0&&h.packet.push(t)},"pushWord"),I=n(()=>{_(),h=structuredClone(C)},"clear"),m={pushWord:H,getPacket:G,getConfig:Y,clear:I,setAccTitle:W,getAccTitle:P,setDiagramTitle:z,getDiagramTitle:F,getAccDescription:S,setAccDescription:B},K=1e4,M=n(t=>{w(t,m);let e=-1,o=[],s=1;const{bitsPerRow:i}=m.getConfig();for(let{start:a,end:r,label:p}of t.blocks){if(r&&r<a)throw new Error(`Packet block ${a} - ${r} is invalid. End must be greater than start.`);if(a!==e+1)throw new Error(`Packet block ${a} - ${r??a} is not contiguous. It should start from ${e+1}.`);for(e=r??a,v.debug(`Packet block ${a} - ${e} with label ${p}`);o.length<=i+1&&m.getPacket().length<K;){const[b,c]=O({start:a,end:r,label:p},s,i);if(o.push(b),b.end+1===s*i&&(m.pushWord(o),o=[],s++),!c)break;({start:a,end:r,label:p}=c)}}m.pushWord(o)},"populate"),O=n((t,e,o)=>{if(t.end===void 0&&(t.end=t.start),t.start>t.end)throw new Error(`Block start ${t.start} is greater than block end ${t.end}.`);return t.end+1<=e*o?[t,void 0]:[{start:t.start,end:e*o-1,label:t.label},{start:e*o,end:t.end,label:t.label}]},"getNextFittingBlock"),q={parse:n(async t=>{const e=await N("packet",t);v.debug(e),M(e)},"parse")},R=n((t,e,o,s)=>{const i=s.db,a=i.getConfig(),{rowHeight:r,paddingY:p,bitWidth:b,bitsPerRow:c}=a,u=i.getPacket(),l=i.getDiagramTitle(),g=r+p,d=g*(u.length+1)-(l?0:r),k=b*c+2,f=T(e);f.attr("viewbox",`0 0 ${k} ${d}`),D(f,d,k,a.useMaxWidth);for(const[$,y]of u.entries())U(f,y,$,a);f.append("text").text(l).attr("x",k/2).attr("y",d-g/2).attr("dominant-baseline","middle").attr("text-anchor","middle").attr("class","packetTitle")},"draw"),U=n((t,e,o,{rowHeight:s,paddingX:i,paddingY:a,bitWidth:r,bitsPerRow:p,showBits:b})=>{const c=t.append("g"),u=o*(s+a)+a;for(const l of e){const g=l.start%p*r+1,d=(l.end-l.start+1)*r-i;if(c.append("rect").attr("x",g).attr("y",u).attr("width",d).attr("height",s).attr("class","packetBlock"),c.append("text").attr("x",g+d/2).attr("y",u+s/2).attr("class","packetLabel").attr("dominant-baseline","middle").attr("text-anchor","middle").text(l.label),!b)continue;const k=l.end===l.start,f=u-2;c.append("text").attr("x",g+(k?d/2:0)).attr("y",f).attr("class","packetByte start").attr("dominant-baseline","auto").attr("text-anchor",k?"middle":"start").text(l.start),k||c.append("text").attr("x",g+d).attr("y",f).attr("class","packetByte end").attr("dominant-baseline","auto").attr("text-anchor","end").text(l.end)}},"drawWord"),X={draw:R},j={byteFontSize:"10px",startByteColor:"black",endByteColor:"black",labelColor:"black",labelFontSize:"12px",titleColor:"black",titleFontSize:"14px",blockStrokeColor:"black",blockStrokeWidth:"1",blockFillColor:"#efefef"},J=n(({packet:t}={})=>{const e=x(j,t);return`
|
||||
import{p as w}from"./chunk-4BMEZGHF-CbUqLJH_.js";import{_ as n,s as B,g as S,t as F,q as z,a as P,b as W,F as x,K as T,e as D,z as _,G as A,H as E,l as v}from"./mermaid-vendor-DGPC_TDM.js";import{p as N}from"./radar-MK3ICKWK-BkgLvISn.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-D-427rzS.js";import"./_basePickBy-CGXZVaMl.js";import"./clone-BvhE3BKU.js";var C={packet:[]},h=structuredClone(C),L=E.packet,Y=n(()=>{const t=x({...L,...A().packet});return t.showBits&&(t.paddingY+=10),t},"getConfig"),G=n(()=>h.packet,"getPacket"),H=n(t=>{t.length>0&&h.packet.push(t)},"pushWord"),I=n(()=>{_(),h=structuredClone(C)},"clear"),m={pushWord:H,getPacket:G,getConfig:Y,clear:I,setAccTitle:W,getAccTitle:P,setDiagramTitle:z,getDiagramTitle:F,getAccDescription:S,setAccDescription:B},K=1e4,M=n(t=>{w(t,m);let e=-1,o=[],s=1;const{bitsPerRow:i}=m.getConfig();for(let{start:a,end:r,label:p}of t.blocks){if(r&&r<a)throw new Error(`Packet block ${a} - ${r} is invalid. End must be greater than start.`);if(a!==e+1)throw new Error(`Packet block ${a} - ${r??a} is not contiguous. It should start from ${e+1}.`);for(e=r??a,v.debug(`Packet block ${a} - ${e} with label ${p}`);o.length<=i+1&&m.getPacket().length<K;){const[b,c]=O({start:a,end:r,label:p},s,i);if(o.push(b),b.end+1===s*i&&(m.pushWord(o),o=[],s++),!c)break;({start:a,end:r,label:p}=c)}}m.pushWord(o)},"populate"),O=n((t,e,o)=>{if(t.end===void 0&&(t.end=t.start),t.start>t.end)throw new Error(`Block start ${t.start} is greater than block end ${t.end}.`);return t.end+1<=e*o?[t,void 0]:[{start:t.start,end:e*o-1,label:t.label},{start:e*o,end:t.end,label:t.label}]},"getNextFittingBlock"),q={parse:n(async t=>{const e=await N("packet",t);v.debug(e),M(e)},"parse")},R=n((t,e,o,s)=>{const i=s.db,a=i.getConfig(),{rowHeight:r,paddingY:p,bitWidth:b,bitsPerRow:c}=a,u=i.getPacket(),l=i.getDiagramTitle(),g=r+p,d=g*(u.length+1)-(l?0:r),k=b*c+2,f=T(e);f.attr("viewbox",`0 0 ${k} ${d}`),D(f,d,k,a.useMaxWidth);for(const[$,y]of u.entries())U(f,y,$,a);f.append("text").text(l).attr("x",k/2).attr("y",d-g/2).attr("dominant-baseline","middle").attr("text-anchor","middle").attr("class","packetTitle")},"draw"),U=n((t,e,o,{rowHeight:s,paddingX:i,paddingY:a,bitWidth:r,bitsPerRow:p,showBits:b})=>{const c=t.append("g"),u=o*(s+a)+a;for(const l of e){const g=l.start%p*r+1,d=(l.end-l.start+1)*r-i;if(c.append("rect").attr("x",g).attr("y",u).attr("width",d).attr("height",s).attr("class","packetBlock"),c.append("text").attr("x",g+d/2).attr("y",u+s/2).attr("class","packetLabel").attr("dominant-baseline","middle").attr("text-anchor","middle").text(l.label),!b)continue;const k=l.end===l.start,f=u-2;c.append("text").attr("x",g+(k?d/2:0)).attr("y",f).attr("class","packetByte start").attr("dominant-baseline","auto").attr("text-anchor",k?"middle":"start").text(l.start),k||c.append("text").attr("x",g+d).attr("y",f).attr("class","packetByte end").attr("dominant-baseline","auto").attr("text-anchor","end").text(l.end)}},"drawWord"),X={draw:R},j={byteFontSize:"10px",startByteColor:"black",endByteColor:"black",labelColor:"black",labelFontSize:"12px",titleColor:"black",titleFontSize:"14px",blockStrokeColor:"black",blockStrokeWidth:"1",blockFillColor:"#efefef"},J=n(({packet:t}={})=>{const e=x(j,t);return`
|
||||
.packetByte {
|
||||
font-size: ${e.byteFontSize};
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
import{_ as m,o as O1,l as Z,c as Ge,d as Ce,p as H1,r as q1,u as i1,b as X1,s as Q1,q as J1,a as Z1,g as $1,t as et,k as tt,v as st,J as it,x as rt,y as s1,z as nt,A as at,B as ut,C as lt}from"./mermaid-vendor-CY273lNM.js";import{g as ot,s as ct}from"./chunk-RZ5BOZE2-BJpHTKlK.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var ht="flowchart-",Pe,dt=(Pe=class{constructor(){this.vertexCounter=0,this.config=Ge(),this.vertices=new Map,this.edges=[],this.classes=new Map,this.subGraphs=[],this.subGraphLookup=new Map,this.tooltips=new Map,this.subCount=0,this.firstGraphFlag=!0,this.secCount=-1,this.posCrossRef=[],this.funs=[],this.setAccTitle=X1,this.setAccDescription=Q1,this.setDiagramTitle=J1,this.getAccTitle=Z1,this.getAccDescription=$1,this.getDiagramTitle=et,this.funs.push(this.setupToolTips.bind(this)),this.addVertex=this.addVertex.bind(this),this.firstGraph=this.firstGraph.bind(this),this.setDirection=this.setDirection.bind(this),this.addSubGraph=this.addSubGraph.bind(this),this.addLink=this.addLink.bind(this),this.setLink=this.setLink.bind(this),this.updateLink=this.updateLink.bind(this),this.addClass=this.addClass.bind(this),this.setClass=this.setClass.bind(this),this.destructLink=this.destructLink.bind(this),this.setClickEvent=this.setClickEvent.bind(this),this.setTooltip=this.setTooltip.bind(this),this.updateLinkInterpolate=this.updateLinkInterpolate.bind(this),this.setClickFun=this.setClickFun.bind(this),this.bindFunctions=this.bindFunctions.bind(this),this.lex={firstGraph:this.firstGraph.bind(this)},this.clear(),this.setGen("gen-2")}sanitizeText(i){return tt.sanitizeText(i,this.config)}lookUpDomId(i){for(const n of this.vertices.values())if(n.id===i)return n.domId;return i}addVertex(i,n,a,u,l,f,c={},A){var U,T;if(!i||i.trim().length===0)return;let r;if(A!==void 0){let d;A.includes(`
|
||||
import{_ as m,o as O1,l as Z,c as Ge,d as Ce,p as H1,r as q1,u as i1,b as X1,s as Q1,q as J1,a as Z1,g as $1,t as et,k as tt,v as st,J as it,x as rt,y as s1,z as nt,A as at,B as ut,C as lt}from"./mermaid-vendor-DGPC_TDM.js";import{g as ot,s as ct}from"./chunk-RZ5BOZE2-BhCqM4LN.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var ht="flowchart-",Pe,dt=(Pe=class{constructor(){this.vertexCounter=0,this.config=Ge(),this.vertices=new Map,this.edges=[],this.classes=new Map,this.subGraphs=[],this.subGraphLookup=new Map,this.tooltips=new Map,this.subCount=0,this.firstGraphFlag=!0,this.secCount=-1,this.posCrossRef=[],this.funs=[],this.setAccTitle=X1,this.setAccDescription=Q1,this.setDiagramTitle=J1,this.getAccTitle=Z1,this.getAccDescription=$1,this.getDiagramTitle=et,this.funs.push(this.setupToolTips.bind(this)),this.addVertex=this.addVertex.bind(this),this.firstGraph=this.firstGraph.bind(this),this.setDirection=this.setDirection.bind(this),this.addSubGraph=this.addSubGraph.bind(this),this.addLink=this.addLink.bind(this),this.setLink=this.setLink.bind(this),this.updateLink=this.updateLink.bind(this),this.addClass=this.addClass.bind(this),this.setClass=this.setClass.bind(this),this.destructLink=this.destructLink.bind(this),this.setClickEvent=this.setClickEvent.bind(this),this.setTooltip=this.setTooltip.bind(this),this.updateLinkInterpolate=this.updateLinkInterpolate.bind(this),this.setClickFun=this.setClickFun.bind(this),this.bindFunctions=this.bindFunctions.bind(this),this.lex={firstGraph:this.firstGraph.bind(this)},this.clear(),this.setGen("gen-2")}sanitizeText(i){return tt.sanitizeText(i,this.config)}lookUpDomId(i){for(const n of this.vertices.values())if(n.id===i)return n.domId;return i}addVertex(i,n,a,u,l,f,c={},A){var U,T;if(!i||i.trim().length===0)return;let r;if(A!==void 0){let d;A.includes(`
|
||||
`)?d=A+`
|
||||
`:d=`{
|
||||
`+A+`
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,2 +1,2 @@
|
|||
import{_ as e,l as o,K as i,e as n,L as p}from"./mermaid-vendor-CY273lNM.js";import{p as m}from"./radar-MK3ICKWK-D66UL2n2.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-BmMh76ze.js";import"./_basePickBy-deWrp6cQ.js";import"./clone-DjMnmZIs.js";var g={parse:e(async r=>{const a=await m("info",r);o.debug(a)},"parse")},v={version:p.version},d=e(()=>v.version,"getVersion"),c={getVersion:d},l=e((r,a,s)=>{o.debug(`rendering info diagram
|
||||
import{_ as e,l as o,K as i,e as n,L as p}from"./mermaid-vendor-DGPC_TDM.js";import{p as m}from"./radar-MK3ICKWK-BkgLvISn.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-D-427rzS.js";import"./_basePickBy-CGXZVaMl.js";import"./clone-BvhE3BKU.js";var g={parse:e(async r=>{const a=await m("info",r);o.debug(a)},"parse")},v={version:p.version},d=e(()=>v.version,"getVersion"),c={getVersion:d},l=e((r,a,s)=>{o.debug(`rendering info diagram
|
||||
`+r);const t=i(a);n(t,100,400,!0),t.append("g").append("text").attr("x",100).attr("y",40).attr("class","version").attr("font-size",32).style("text-anchor","middle").text(`v${s}`)},"draw"),f={draw:l},L={parser:g,db:c,renderer:f};export{L as diagram};
|
|
@ -1,4 +1,4 @@
|
|||
import{a as pt,g as at,f as gt,d as mt}from"./chunk-D6G4REZN-Cg1L9G2i.js";import{_ as s,g as xt,s as kt,a as _t,b as bt,t as vt,q as wt,c as A,d as W,e as Tt,z as St,N as tt}from"./mermaid-vendor-CY273lNM.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var H=function(){var t=s(function(g,r,a,l){for(a=a||{},l=g.length;l--;a[g[l]]=r);return a},"o"),e=[6,8,10,11,12,14,16,17,18],i=[1,9],c=[1,10],n=[1,11],u=[1,12],h=[1,13],f=[1,14],d={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,journey:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,title:11,acc_title:12,acc_title_value:13,acc_descr:14,acc_descr_value:15,acc_descr_multiline_value:16,section:17,taskName:18,taskData:19,$accept:0,$end:1},terminals_:{2:"error",4:"journey",6:"EOF",8:"SPACE",10:"NEWLINE",11:"title",12:"acc_title",13:"acc_title_value",14:"acc_descr",15:"acc_descr_value",16:"acc_descr_multiline_value",17:"section",18:"taskName",19:"taskData"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,2]],performAction:s(function(r,a,l,y,p,o,S){var _=o.length-1;switch(p){case 1:return o[_-1];case 2:this.$=[];break;case 3:o[_-1].push(o[_]),this.$=o[_-1];break;case 4:case 5:this.$=o[_];break;case 6:case 7:this.$=[];break;case 8:y.setDiagramTitle(o[_].substr(6)),this.$=o[_].substr(6);break;case 9:this.$=o[_].trim(),y.setAccTitle(this.$);break;case 10:case 11:this.$=o[_].trim(),y.setAccDescription(this.$);break;case 12:y.addSection(o[_].substr(8)),this.$=o[_].substr(8);break;case 13:y.addTask(o[_-1],o[_]),this.$="task";break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},t(e,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:i,12:c,14:n,16:u,17:h,18:f},t(e,[2,7],{1:[2,1]}),t(e,[2,3]),{9:15,11:i,12:c,14:n,16:u,17:h,18:f},t(e,[2,5]),t(e,[2,6]),t(e,[2,8]),{13:[1,16]},{15:[1,17]},t(e,[2,11]),t(e,[2,12]),{19:[1,18]},t(e,[2,4]),t(e,[2,9]),t(e,[2,10]),t(e,[2,13])],defaultActions:{},parseError:s(function(r,a){if(a.recoverable)this.trace(r);else{var l=new Error(r);throw l.hash=a,l}},"parseError"),parse:s(function(r){var a=this,l=[0],y=[],p=[null],o=[],S=this.table,_="",B=0,J=0,ut=2,K=1,yt=o.slice.call(arguments,1),k=Object.create(this.lexer),E={yy:{}};for(var O in this.yy)Object.prototype.hasOwnProperty.call(this.yy,O)&&(E.yy[O]=this.yy[O]);k.setInput(r,E.yy),E.yy.lexer=k,E.yy.parser=this,typeof k.yylloc>"u"&&(k.yylloc={});var Y=k.yylloc;o.push(Y);var dt=k.options&&k.options.ranges;typeof E.yy.parseError=="function"?this.parseError=E.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function ft(v){l.length=l.length-2*v,p.length=p.length-v,o.length=o.length-v}s(ft,"popStack");function Q(){var v;return v=y.pop()||k.lex()||K,typeof v!="number"&&(v instanceof Array&&(y=v,v=y.pop()),v=a.symbols_[v]||v),v}s(Q,"lex");for(var b,P,w,q,C={},N,$,D,j;;){if(P=l[l.length-1],this.defaultActions[P]?w=this.defaultActions[P]:((b===null||typeof b>"u")&&(b=Q()),w=S[P]&&S[P][b]),typeof w>"u"||!w.length||!w[0]){var G="";j=[];for(N in S[P])this.terminals_[N]&&N>ut&&j.push("'"+this.terminals_[N]+"'");k.showPosition?G="Parse error on line "+(B+1)+`:
|
||||
import{a as pt,g as at,f as gt,d as mt}from"./chunk-D6G4REZN-5bNYmyqV.js";import{_ as s,g as xt,s as kt,a as _t,b as bt,t as vt,q as wt,c as A,d as W,e as Tt,z as St,N as tt}from"./mermaid-vendor-DGPC_TDM.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var H=function(){var t=s(function(g,r,a,l){for(a=a||{},l=g.length;l--;a[g[l]]=r);return a},"o"),e=[6,8,10,11,12,14,16,17,18],i=[1,9],c=[1,10],n=[1,11],u=[1,12],h=[1,13],f=[1,14],d={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,journey:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,title:11,acc_title:12,acc_title_value:13,acc_descr:14,acc_descr_value:15,acc_descr_multiline_value:16,section:17,taskName:18,taskData:19,$accept:0,$end:1},terminals_:{2:"error",4:"journey",6:"EOF",8:"SPACE",10:"NEWLINE",11:"title",12:"acc_title",13:"acc_title_value",14:"acc_descr",15:"acc_descr_value",16:"acc_descr_multiline_value",17:"section",18:"taskName",19:"taskData"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,2]],performAction:s(function(r,a,l,y,p,o,S){var _=o.length-1;switch(p){case 1:return o[_-1];case 2:this.$=[];break;case 3:o[_-1].push(o[_]),this.$=o[_-1];break;case 4:case 5:this.$=o[_];break;case 6:case 7:this.$=[];break;case 8:y.setDiagramTitle(o[_].substr(6)),this.$=o[_].substr(6);break;case 9:this.$=o[_].trim(),y.setAccTitle(this.$);break;case 10:case 11:this.$=o[_].trim(),y.setAccDescription(this.$);break;case 12:y.addSection(o[_].substr(8)),this.$=o[_].substr(8);break;case 13:y.addTask(o[_-1],o[_]),this.$="task";break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},t(e,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:i,12:c,14:n,16:u,17:h,18:f},t(e,[2,7],{1:[2,1]}),t(e,[2,3]),{9:15,11:i,12:c,14:n,16:u,17:h,18:f},t(e,[2,5]),t(e,[2,6]),t(e,[2,8]),{13:[1,16]},{15:[1,17]},t(e,[2,11]),t(e,[2,12]),{19:[1,18]},t(e,[2,4]),t(e,[2,9]),t(e,[2,10]),t(e,[2,13])],defaultActions:{},parseError:s(function(r,a){if(a.recoverable)this.trace(r);else{var l=new Error(r);throw l.hash=a,l}},"parseError"),parse:s(function(r){var a=this,l=[0],y=[],p=[null],o=[],S=this.table,_="",B=0,J=0,ut=2,K=1,yt=o.slice.call(arguments,1),k=Object.create(this.lexer),E={yy:{}};for(var O in this.yy)Object.prototype.hasOwnProperty.call(this.yy,O)&&(E.yy[O]=this.yy[O]);k.setInput(r,E.yy),E.yy.lexer=k,E.yy.parser=this,typeof k.yylloc>"u"&&(k.yylloc={});var Y=k.yylloc;o.push(Y);var dt=k.options&&k.options.ranges;typeof E.yy.parseError=="function"?this.parseError=E.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function ft(v){l.length=l.length-2*v,p.length=p.length-v,o.length=o.length-v}s(ft,"popStack");function Q(){var v;return v=y.pop()||k.lex()||K,typeof v!="number"&&(v instanceof Array&&(y=v,v=y.pop()),v=a.symbols_[v]||v),v}s(Q,"lex");for(var b,P,w,q,C={},N,$,D,j;;){if(P=l[l.length-1],this.defaultActions[P]?w=this.defaultActions[P]:((b===null||typeof b>"u")&&(b=Q()),w=S[P]&&S[P][b]),typeof w>"u"||!w.length||!w[0]){var G="";j=[];for(N in S[P])this.terminals_[N]&&N>ut&&j.push("'"+this.terminals_[N]+"'");k.showPosition?G="Parse error on line "+(B+1)+`:
|
||||
`+k.showPosition()+`
|
||||
Expecting `+j.join(", ")+", got '"+(this.terminals_[b]||b)+"'":G="Parse error on line "+(B+1)+": Unexpected "+(b==K?"end of input":"'"+(this.terminals_[b]||b)+"'"),this.parseError(G,{text:k.match,token:this.terminals_[b]||b,line:k.yylineno,loc:Y,expected:j})}if(w[0]instanceof Array&&w.length>1)throw new Error("Parse Error: multiple actions possible at state: "+P+", token: "+b);switch(w[0]){case 1:l.push(b),p.push(k.yytext),o.push(k.yylloc),l.push(w[1]),b=null,J=k.yyleng,_=k.yytext,B=k.yylineno,Y=k.yylloc;break;case 2:if($=this.productions_[w[1]][1],C.$=p[p.length-$],C._$={first_line:o[o.length-($||1)].first_line,last_line:o[o.length-1].last_line,first_column:o[o.length-($||1)].first_column,last_column:o[o.length-1].last_column},dt&&(C._$.range=[o[o.length-($||1)].range[0],o[o.length-1].range[1]]),q=this.performAction.apply(C,[_,J,B,E.yy,w[1],p,o].concat(yt)),typeof q<"u")return q;$&&(l=l.slice(0,-1*$*2),p=p.slice(0,-1*$),o=o.slice(0,-1*$)),l.push(this.productions_[w[1]][0]),p.push(C.$),o.push(C._$),D=S[l[l.length-2]][l[l.length-1]],l.push(D);break;case 3:return!0}}return!0},"parse")},x=function(){var g={EOF:1,parseError:s(function(a,l){if(this.yy.parser)this.yy.parser.parseError(a,l);else throw new Error(a)},"parseError"),setInput:s(function(r,a){return this.yy=a||this.yy||{},this._input=r,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var r=this._input[0];this.yytext+=r,this.yyleng++,this.offset++,this.match+=r,this.matched+=r;var a=r.match(/(?:\r\n?|\n).*/g);return a?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),r},"input"),unput:s(function(r){var a=r.length,l=r.split(/(?:\r\n?|\n)/g);this._input=r+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-a),this.offset-=a;var y=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),l.length-1&&(this.yylineno-=l.length-1);var p=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:l?(l.length===y.length?this.yylloc.first_column:0)+y[y.length-l.length].length-l[0].length:this.yylloc.first_column-a},this.options.ranges&&(this.yylloc.range=[p[0],p[0]+this.yyleng-a]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
|
||||
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(r){this.unput(this.match.slice(r))},"less"),pastInput:s(function(){var r=this.matched.substr(0,this.matched.length-this.match.length);return(r.length>20?"...":"")+r.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var r=this.match;return r.length<20&&(r+=this._input.substr(0,20-r.length)),(r.substr(0,20)+(r.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var r=this.pastInput(),a=new Array(r.length+1).join("-");return r+this.upcomingInput()+`
|
|
@ -1,4 +1,4 @@
|
|||
import{_ as c,l as te,c as W,K as fe,a7 as ye,a8 as be,a9 as me,a2 as _e,H as Y,i as G,v as Ee,J as ke,a3 as Se,a4 as le,a5 as ce}from"./mermaid-vendor-CY273lNM.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var $=function(){var t=c(function(_,i,n,a){for(n=n||{},a=_.length;a--;n[_[a]]=i);return n},"o"),g=[1,4],d=[1,13],r=[1,12],p=[1,15],E=[1,16],f=[1,20],h=[1,19],L=[6,7,8],C=[1,26],w=[1,24],N=[1,25],s=[6,7,11],H=[1,31],x=[6,7,11,24],P=[1,6,13,16,17,20,23],M=[1,35],U=[1,36],A=[1,6,7,11,13,16,17,20,23],j=[1,38],V={trace:c(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mindMap:4,spaceLines:5,SPACELINE:6,NL:7,KANBAN:8,document:9,stop:10,EOF:11,statement:12,SPACELIST:13,node:14,shapeData:15,ICON:16,CLASS:17,nodeWithId:18,nodeWithoutId:19,NODE_DSTART:20,NODE_DESCR:21,NODE_DEND:22,NODE_ID:23,SHAPE_DATA:24,$accept:0,$end:1},terminals_:{2:"error",6:"SPACELINE",7:"NL",8:"KANBAN",11:"EOF",13:"SPACELIST",16:"ICON",17:"CLASS",20:"NODE_DSTART",21:"NODE_DESCR",22:"NODE_DEND",23:"NODE_ID",24:"SHAPE_DATA"},productions_:[0,[3,1],[3,2],[5,1],[5,2],[5,2],[4,2],[4,3],[10,1],[10,1],[10,1],[10,2],[10,2],[9,3],[9,2],[12,3],[12,2],[12,2],[12,2],[12,1],[12,2],[12,1],[12,1],[12,1],[12,1],[14,1],[14,1],[19,3],[18,1],[18,4],[15,2],[15,1]],performAction:c(function(i,n,a,o,u,e,B){var l=e.length-1;switch(u){case 6:case 7:return o;case 8:o.getLogger().trace("Stop NL ");break;case 9:o.getLogger().trace("Stop EOF ");break;case 11:o.getLogger().trace("Stop NL2 ");break;case 12:o.getLogger().trace("Stop EOF2 ");break;case 15:o.getLogger().info("Node: ",e[l-1].id),o.addNode(e[l-2].length,e[l-1].id,e[l-1].descr,e[l-1].type,e[l]);break;case 16:o.getLogger().info("Node: ",e[l].id),o.addNode(e[l-1].length,e[l].id,e[l].descr,e[l].type);break;case 17:o.getLogger().trace("Icon: ",e[l]),o.decorateNode({icon:e[l]});break;case 18:case 23:o.decorateNode({class:e[l]});break;case 19:o.getLogger().trace("SPACELIST");break;case 20:o.getLogger().trace("Node: ",e[l-1].id),o.addNode(0,e[l-1].id,e[l-1].descr,e[l-1].type,e[l]);break;case 21:o.getLogger().trace("Node: ",e[l].id),o.addNode(0,e[l].id,e[l].descr,e[l].type);break;case 22:o.decorateNode({icon:e[l]});break;case 27:o.getLogger().trace("node found ..",e[l-2]),this.$={id:e[l-1],descr:e[l-1],type:o.getType(e[l-2],e[l])};break;case 28:this.$={id:e[l],descr:e[l],type:0};break;case 29:o.getLogger().trace("node found ..",e[l-3]),this.$={id:e[l-3],descr:e[l-1],type:o.getType(e[l-2],e[l])};break;case 30:this.$=e[l-1]+e[l];break;case 31:this.$=e[l];break}},"anonymous"),table:[{3:1,4:2,5:3,6:[1,5],8:g},{1:[3]},{1:[2,1]},{4:6,6:[1,7],7:[1,8],8:g},{6:d,7:[1,10],9:9,12:11,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},t(L,[2,3]),{1:[2,2]},t(L,[2,4]),t(L,[2,5]),{1:[2,6],6:d,12:21,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},{6:d,9:22,12:11,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},{6:C,7:w,10:23,11:N},t(s,[2,24],{18:17,19:18,14:27,16:[1,28],17:[1,29],20:f,23:h}),t(s,[2,19]),t(s,[2,21],{15:30,24:H}),t(s,[2,22]),t(s,[2,23]),t(x,[2,25]),t(x,[2,26]),t(x,[2,28],{20:[1,32]}),{21:[1,33]},{6:C,7:w,10:34,11:N},{1:[2,7],6:d,12:21,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},t(P,[2,14],{7:M,11:U}),t(A,[2,8]),t(A,[2,9]),t(A,[2,10]),t(s,[2,16],{15:37,24:H}),t(s,[2,17]),t(s,[2,18]),t(s,[2,20],{24:j}),t(x,[2,31]),{21:[1,39]},{22:[1,40]},t(P,[2,13],{7:M,11:U}),t(A,[2,11]),t(A,[2,12]),t(s,[2,15],{24:j}),t(x,[2,30]),{22:[1,41]},t(x,[2,27]),t(x,[2,29])],defaultActions:{2:[2,1],6:[2,2]},parseError:c(function(i,n){if(n.recoverable)this.trace(i);else{var a=new Error(i);throw a.hash=n,a}},"parseError"),parse:c(function(i){var n=this,a=[0],o=[],u=[null],e=[],B=this.table,l="",z=0,se=0,ue=2,re=1,ge=e.slice.call(arguments,1),b=Object.create(this.lexer),T={yy:{}};for(var J in this.yy)Object.prototype.hasOwnProperty.call(this.yy,J)&&(T.yy[J]=this.yy[J]);b.setInput(i,T.yy),T.yy.lexer=b,T.yy.parser=this,typeof b.yylloc>"u"&&(b.yylloc={});var q=b.yylloc;e.push(q);var de=b.options&&b.options.ranges;typeof T.yy.parseError=="function"?this.parseError=T.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function pe(S){a.length=a.length-2*S,u.length=u.length-S,e.length=e.length-S}c(pe,"popStack");function ae(){var S;return S=o.pop()||b.lex()||re,typeof S!="number"&&(S instanceof Array&&(o=S,S=o.pop()),S=n.symbols_[S]||S),S}c(ae,"lex");for(var k,R,v,Q,F={},K,I,oe,X;;){if(R=a[a.length-1],this.defaultActions[R]?v=this.defaultActions[R]:((k===null||typeof k>"u")&&(k=ae()),v=B[R]&&B[R][k]),typeof v>"u"||!v.length||!v[0]){var Z="";X=[];for(K in B[R])this.terminals_[K]&&K>ue&&X.push("'"+this.terminals_[K]+"'");b.showPosition?Z="Parse error on line "+(z+1)+`:
|
||||
import{_ as c,l as te,c as W,K as fe,a7 as ye,a8 as be,a9 as me,a2 as _e,H as Y,i as G,v as Ee,J as ke,a3 as Se,a4 as le,a5 as ce}from"./mermaid-vendor-DGPC_TDM.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var $=function(){var t=c(function(_,i,n,a){for(n=n||{},a=_.length;a--;n[_[a]]=i);return n},"o"),g=[1,4],d=[1,13],r=[1,12],p=[1,15],E=[1,16],f=[1,20],h=[1,19],L=[6,7,8],C=[1,26],w=[1,24],N=[1,25],s=[6,7,11],H=[1,31],x=[6,7,11,24],P=[1,6,13,16,17,20,23],M=[1,35],U=[1,36],A=[1,6,7,11,13,16,17,20,23],j=[1,38],V={trace:c(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mindMap:4,spaceLines:5,SPACELINE:6,NL:7,KANBAN:8,document:9,stop:10,EOF:11,statement:12,SPACELIST:13,node:14,shapeData:15,ICON:16,CLASS:17,nodeWithId:18,nodeWithoutId:19,NODE_DSTART:20,NODE_DESCR:21,NODE_DEND:22,NODE_ID:23,SHAPE_DATA:24,$accept:0,$end:1},terminals_:{2:"error",6:"SPACELINE",7:"NL",8:"KANBAN",11:"EOF",13:"SPACELIST",16:"ICON",17:"CLASS",20:"NODE_DSTART",21:"NODE_DESCR",22:"NODE_DEND",23:"NODE_ID",24:"SHAPE_DATA"},productions_:[0,[3,1],[3,2],[5,1],[5,2],[5,2],[4,2],[4,3],[10,1],[10,1],[10,1],[10,2],[10,2],[9,3],[9,2],[12,3],[12,2],[12,2],[12,2],[12,1],[12,2],[12,1],[12,1],[12,1],[12,1],[14,1],[14,1],[19,3],[18,1],[18,4],[15,2],[15,1]],performAction:c(function(i,n,a,o,u,e,B){var l=e.length-1;switch(u){case 6:case 7:return o;case 8:o.getLogger().trace("Stop NL ");break;case 9:o.getLogger().trace("Stop EOF ");break;case 11:o.getLogger().trace("Stop NL2 ");break;case 12:o.getLogger().trace("Stop EOF2 ");break;case 15:o.getLogger().info("Node: ",e[l-1].id),o.addNode(e[l-2].length,e[l-1].id,e[l-1].descr,e[l-1].type,e[l]);break;case 16:o.getLogger().info("Node: ",e[l].id),o.addNode(e[l-1].length,e[l].id,e[l].descr,e[l].type);break;case 17:o.getLogger().trace("Icon: ",e[l]),o.decorateNode({icon:e[l]});break;case 18:case 23:o.decorateNode({class:e[l]});break;case 19:o.getLogger().trace("SPACELIST");break;case 20:o.getLogger().trace("Node: ",e[l-1].id),o.addNode(0,e[l-1].id,e[l-1].descr,e[l-1].type,e[l]);break;case 21:o.getLogger().trace("Node: ",e[l].id),o.addNode(0,e[l].id,e[l].descr,e[l].type);break;case 22:o.decorateNode({icon:e[l]});break;case 27:o.getLogger().trace("node found ..",e[l-2]),this.$={id:e[l-1],descr:e[l-1],type:o.getType(e[l-2],e[l])};break;case 28:this.$={id:e[l],descr:e[l],type:0};break;case 29:o.getLogger().trace("node found ..",e[l-3]),this.$={id:e[l-3],descr:e[l-1],type:o.getType(e[l-2],e[l])};break;case 30:this.$=e[l-1]+e[l];break;case 31:this.$=e[l];break}},"anonymous"),table:[{3:1,4:2,5:3,6:[1,5],8:g},{1:[3]},{1:[2,1]},{4:6,6:[1,7],7:[1,8],8:g},{6:d,7:[1,10],9:9,12:11,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},t(L,[2,3]),{1:[2,2]},t(L,[2,4]),t(L,[2,5]),{1:[2,6],6:d,12:21,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},{6:d,9:22,12:11,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},{6:C,7:w,10:23,11:N},t(s,[2,24],{18:17,19:18,14:27,16:[1,28],17:[1,29],20:f,23:h}),t(s,[2,19]),t(s,[2,21],{15:30,24:H}),t(s,[2,22]),t(s,[2,23]),t(x,[2,25]),t(x,[2,26]),t(x,[2,28],{20:[1,32]}),{21:[1,33]},{6:C,7:w,10:34,11:N},{1:[2,7],6:d,12:21,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},t(P,[2,14],{7:M,11:U}),t(A,[2,8]),t(A,[2,9]),t(A,[2,10]),t(s,[2,16],{15:37,24:H}),t(s,[2,17]),t(s,[2,18]),t(s,[2,20],{24:j}),t(x,[2,31]),{21:[1,39]},{22:[1,40]},t(P,[2,13],{7:M,11:U}),t(A,[2,11]),t(A,[2,12]),t(s,[2,15],{24:j}),t(x,[2,30]),{22:[1,41]},t(x,[2,27]),t(x,[2,29])],defaultActions:{2:[2,1],6:[2,2]},parseError:c(function(i,n){if(n.recoverable)this.trace(i);else{var a=new Error(i);throw a.hash=n,a}},"parseError"),parse:c(function(i){var n=this,a=[0],o=[],u=[null],e=[],B=this.table,l="",z=0,se=0,ue=2,re=1,ge=e.slice.call(arguments,1),b=Object.create(this.lexer),T={yy:{}};for(var J in this.yy)Object.prototype.hasOwnProperty.call(this.yy,J)&&(T.yy[J]=this.yy[J]);b.setInput(i,T.yy),T.yy.lexer=b,T.yy.parser=this,typeof b.yylloc>"u"&&(b.yylloc={});var q=b.yylloc;e.push(q);var de=b.options&&b.options.ranges;typeof T.yy.parseError=="function"?this.parseError=T.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function pe(S){a.length=a.length-2*S,u.length=u.length-S,e.length=e.length-S}c(pe,"popStack");function ae(){var S;return S=o.pop()||b.lex()||re,typeof S!="number"&&(S instanceof Array&&(o=S,S=o.pop()),S=n.symbols_[S]||S),S}c(ae,"lex");for(var k,R,v,Q,F={},K,I,oe,X;;){if(R=a[a.length-1],this.defaultActions[R]?v=this.defaultActions[R]:((k===null||typeof k>"u")&&(k=ae()),v=B[R]&&B[R][k]),typeof v>"u"||!v.length||!v[0]){var Z="";X=[];for(K in B[R])this.terminals_[K]&&K>ue&&X.push("'"+this.terminals_[K]+"'");b.showPosition?Z="Parse error on line "+(z+1)+`:
|
||||
`+b.showPosition()+`
|
||||
Expecting `+X.join(", ")+", got '"+(this.terminals_[k]||k)+"'":Z="Parse error on line "+(z+1)+": Unexpected "+(k==re?"end of input":"'"+(this.terminals_[k]||k)+"'"),this.parseError(Z,{text:b.match,token:this.terminals_[k]||k,line:b.yylineno,loc:q,expected:X})}if(v[0]instanceof Array&&v.length>1)throw new Error("Parse Error: multiple actions possible at state: "+R+", token: "+k);switch(v[0]){case 1:a.push(k),u.push(b.yytext),e.push(b.yylloc),a.push(v[1]),k=null,se=b.yyleng,l=b.yytext,z=b.yylineno,q=b.yylloc;break;case 2:if(I=this.productions_[v[1]][1],F.$=u[u.length-I],F._$={first_line:e[e.length-(I||1)].first_line,last_line:e[e.length-1].last_line,first_column:e[e.length-(I||1)].first_column,last_column:e[e.length-1].last_column},de&&(F._$.range=[e[e.length-(I||1)].range[0],e[e.length-1].range[1]]),Q=this.performAction.apply(F,[l,se,z,T.yy,v[1],u,e].concat(ge)),typeof Q<"u")return Q;I&&(a=a.slice(0,-1*I*2),u=u.slice(0,-1*I),e=e.slice(0,-1*I)),a.push(this.productions_[v[1]][0]),u.push(F.$),e.push(F._$),oe=B[a[a.length-2]][a[a.length-1]],a.push(oe);break;case 3:return!0}}return!0},"parse")},m=function(){var _={EOF:1,parseError:c(function(n,a){if(this.yy.parser)this.yy.parser.parseError(n,a);else throw new Error(n)},"parseError"),setInput:c(function(i,n){return this.yy=n||this.yy||{},this._input=i,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:c(function(){var i=this._input[0];this.yytext+=i,this.yyleng++,this.offset++,this.match+=i,this.matched+=i;var n=i.match(/(?:\r\n?|\n).*/g);return n?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),i},"input"),unput:c(function(i){var n=i.length,a=i.split(/(?:\r\n?|\n)/g);this._input=i+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-n),this.offset-=n;var o=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),a.length-1&&(this.yylineno-=a.length-1);var u=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:a?(a.length===o.length?this.yylloc.first_column:0)+o[o.length-a.length].length-a[0].length:this.yylloc.first_column-n},this.options.ranges&&(this.yylloc.range=[u[0],u[0]+this.yyleng-n]),this.yyleng=this.yytext.length,this},"unput"),more:c(function(){return this._more=!0,this},"more"),reject:c(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
|
||||
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:c(function(i){this.unput(this.match.slice(i))},"less"),pastInput:c(function(){var i=this.matched.substr(0,this.matched.length-this.match.length);return(i.length>20?"...":"")+i.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:c(function(){var i=this.match;return i.length<20&&(i+=this._input.substr(0,20-i.length)),(i.substr(0,20)+(i.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:c(function(){var i=this.pastInput(),n=new Array(i.length+1).join("-");return i+this.upcomingInput()+`
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
import{p as N}from"./chunk-4BMEZGHF-DcXQJx5Q.js";import{_ as i,g as B,s as U,a as q,b as H,t as K,q as V,l as C,c as Z,F as j,K as J,M as Q,N as z,O as X,e as Y,z as tt,P as et,H as at}from"./mermaid-vendor-CY273lNM.js";import{p as rt}from"./radar-MK3ICKWK-D66UL2n2.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-BmMh76ze.js";import"./_basePickBy-deWrp6cQ.js";import"./clone-DjMnmZIs.js";var it=at.pie,D={sections:new Map,showData:!1},f=D.sections,w=D.showData,st=structuredClone(it),ot=i(()=>structuredClone(st),"getConfig"),nt=i(()=>{f=new Map,w=D.showData,tt()},"clear"),lt=i(({label:t,value:a})=>{f.has(t)||(f.set(t,a),C.debug(`added new section: ${t}, with value: ${a}`))},"addSection"),ct=i(()=>f,"getSections"),pt=i(t=>{w=t},"setShowData"),dt=i(()=>w,"getShowData"),F={getConfig:ot,clear:nt,setDiagramTitle:V,getDiagramTitle:K,setAccTitle:H,getAccTitle:q,setAccDescription:U,getAccDescription:B,addSection:lt,getSections:ct,setShowData:pt,getShowData:dt},gt=i((t,a)=>{N(t,a),a.setShowData(t.showData),t.sections.map(a.addSection)},"populateDb"),ut={parse:i(async t=>{const a=await rt("pie",t);C.debug(a),gt(a,F)},"parse")},mt=i(t=>`
|
||||
import{p as N}from"./chunk-4BMEZGHF-CbUqLJH_.js";import{_ as i,g as B,s as U,a as q,b as H,t as K,q as V,l as C,c as Z,F as j,K as J,M as Q,N as z,O as X,e as Y,z as tt,P as et,H as at}from"./mermaid-vendor-DGPC_TDM.js";import{p as rt}from"./radar-MK3ICKWK-BkgLvISn.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-D-427rzS.js";import"./_basePickBy-CGXZVaMl.js";import"./clone-BvhE3BKU.js";var it=at.pie,D={sections:new Map,showData:!1},f=D.sections,w=D.showData,st=structuredClone(it),ot=i(()=>structuredClone(st),"getConfig"),nt=i(()=>{f=new Map,w=D.showData,tt()},"clear"),lt=i(({label:t,value:a})=>{f.has(t)||(f.set(t,a),C.debug(`added new section: ${t}, with value: ${a}`))},"addSection"),ct=i(()=>f,"getSections"),pt=i(t=>{w=t},"setShowData"),dt=i(()=>w,"getShowData"),F={getConfig:ot,clear:nt,setDiagramTitle:V,getDiagramTitle:K,setAccTitle:H,getAccTitle:q,setAccDescription:U,getAccDescription:B,addSection:lt,getSections:ct,setShowData:pt,getShowData:dt},gt=i((t,a)=>{N(t,a),a.setShowData(t.showData),t.sections.map(a.addSection)},"populateDb"),ut={parse:i(async t=>{const a=await rt("pie",t);C.debug(a),gt(a,F)},"parse")},mt=i(t=>`
|
||||
.pieCircle{
|
||||
stroke: ${t.pieStrokeColor};
|
||||
stroke-width : ${t.pieStrokeWidth};
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
import{s as r,b as e,a,S as s}from"./chunk-AEK57VVT-kQQtlouG.js";import{_ as i}from"./mermaid-vendor-CY273lNM.js";import"./chunk-RZ5BOZE2-BJpHTKlK.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var b={parser:a,get db(){return new s(2)},renderer:e,styles:r,init:i(t=>{t.state||(t.state={}),t.state.arrowMarkerAbsolute=t.arrowMarkerAbsolute},"init")};export{b as diagram};
|
||||
import{s as r,b as e,a,S as s}from"./chunk-AEK57VVT--Edz4Twz.js";import{_ as i}from"./mermaid-vendor-DGPC_TDM.js";import"./chunk-RZ5BOZE2-BhCqM4LN.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var b={parser:a,get db(){return new s(2)},renderer:e,styles:r,init:i(t=>{t.state||(t.state={}),t.state.arrowMarkerAbsolute=t.arrowMarkerAbsolute},"init")};export{b as diagram};
|
|
@ -1,4 +1,4 @@
|
|||
import{_ as s,c as xt,l as T,d as q,a2 as kt,a3 as _t,a4 as bt,a5 as vt,N as nt,D as wt,a6 as St,z as Et}from"./mermaid-vendor-CY273lNM.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var X=function(){var n=s(function(f,i,a,d){for(a=a||{},d=f.length;d--;a[f[d]]=i);return a},"o"),t=[6,8,10,11,12,14,16,17,20,21],e=[1,9],l=[1,10],r=[1,11],h=[1,12],c=[1,13],g=[1,16],m=[1,17],p={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,timeline:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,title:11,acc_title:12,acc_title_value:13,acc_descr:14,acc_descr_value:15,acc_descr_multiline_value:16,section:17,period_statement:18,event_statement:19,period:20,event:21,$accept:0,$end:1},terminals_:{2:"error",4:"timeline",6:"EOF",8:"SPACE",10:"NEWLINE",11:"title",12:"acc_title",13:"acc_title_value",14:"acc_descr",15:"acc_descr_value",16:"acc_descr_multiline_value",17:"section",20:"period",21:"event"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,1],[9,1],[18,1],[19,1]],performAction:s(function(i,a,d,u,y,o,S){var k=o.length-1;switch(y){case 1:return o[k-1];case 2:this.$=[];break;case 3:o[k-1].push(o[k]),this.$=o[k-1];break;case 4:case 5:this.$=o[k];break;case 6:case 7:this.$=[];break;case 8:u.getCommonDb().setDiagramTitle(o[k].substr(6)),this.$=o[k].substr(6);break;case 9:this.$=o[k].trim(),u.getCommonDb().setAccTitle(this.$);break;case 10:case 11:this.$=o[k].trim(),u.getCommonDb().setAccDescription(this.$);break;case 12:u.addSection(o[k].substr(8)),this.$=o[k].substr(8);break;case 15:u.addTask(o[k],0,""),this.$=o[k];break;case 16:u.addEvent(o[k].substr(2)),this.$=o[k];break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},n(t,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:e,12:l,14:r,16:h,17:c,18:14,19:15,20:g,21:m},n(t,[2,7],{1:[2,1]}),n(t,[2,3]),{9:18,11:e,12:l,14:r,16:h,17:c,18:14,19:15,20:g,21:m},n(t,[2,5]),n(t,[2,6]),n(t,[2,8]),{13:[1,19]},{15:[1,20]},n(t,[2,11]),n(t,[2,12]),n(t,[2,13]),n(t,[2,14]),n(t,[2,15]),n(t,[2,16]),n(t,[2,4]),n(t,[2,9]),n(t,[2,10])],defaultActions:{},parseError:s(function(i,a){if(a.recoverable)this.trace(i);else{var d=new Error(i);throw d.hash=a,d}},"parseError"),parse:s(function(i){var a=this,d=[0],u=[],y=[null],o=[],S=this.table,k="",M=0,P=0,B=2,J=1,O=o.slice.call(arguments,1),_=Object.create(this.lexer),E={yy:{}};for(var v in this.yy)Object.prototype.hasOwnProperty.call(this.yy,v)&&(E.yy[v]=this.yy[v]);_.setInput(i,E.yy),E.yy.lexer=_,E.yy.parser=this,typeof _.yylloc>"u"&&(_.yylloc={});var L=_.yylloc;o.push(L);var A=_.options&&_.options.ranges;typeof E.yy.parseError=="function"?this.parseError=E.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function R(I){d.length=d.length-2*I,y.length=y.length-I,o.length=o.length-I}s(R,"popStack");function z(){var I;return I=u.pop()||_.lex()||J,typeof I!="number"&&(I instanceof Array&&(u=I,I=u.pop()),I=a.symbols_[I]||I),I}s(z,"lex");for(var w,C,N,K,F={},j,$,et,G;;){if(C=d[d.length-1],this.defaultActions[C]?N=this.defaultActions[C]:((w===null||typeof w>"u")&&(w=z()),N=S[C]&&S[C][w]),typeof N>"u"||!N.length||!N[0]){var Q="";G=[];for(j in S[C])this.terminals_[j]&&j>B&&G.push("'"+this.terminals_[j]+"'");_.showPosition?Q="Parse error on line "+(M+1)+`:
|
||||
import{_ as s,c as xt,l as T,d as q,a2 as kt,a3 as _t,a4 as bt,a5 as vt,N as nt,D as wt,a6 as St,z as Et}from"./mermaid-vendor-DGPC_TDM.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var X=function(){var n=s(function(f,i,a,d){for(a=a||{},d=f.length;d--;a[f[d]]=i);return a},"o"),t=[6,8,10,11,12,14,16,17,20,21],e=[1,9],l=[1,10],r=[1,11],h=[1,12],c=[1,13],g=[1,16],m=[1,17],p={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,timeline:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,title:11,acc_title:12,acc_title_value:13,acc_descr:14,acc_descr_value:15,acc_descr_multiline_value:16,section:17,period_statement:18,event_statement:19,period:20,event:21,$accept:0,$end:1},terminals_:{2:"error",4:"timeline",6:"EOF",8:"SPACE",10:"NEWLINE",11:"title",12:"acc_title",13:"acc_title_value",14:"acc_descr",15:"acc_descr_value",16:"acc_descr_multiline_value",17:"section",20:"period",21:"event"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,1],[9,1],[18,1],[19,1]],performAction:s(function(i,a,d,u,y,o,S){var k=o.length-1;switch(y){case 1:return o[k-1];case 2:this.$=[];break;case 3:o[k-1].push(o[k]),this.$=o[k-1];break;case 4:case 5:this.$=o[k];break;case 6:case 7:this.$=[];break;case 8:u.getCommonDb().setDiagramTitle(o[k].substr(6)),this.$=o[k].substr(6);break;case 9:this.$=o[k].trim(),u.getCommonDb().setAccTitle(this.$);break;case 10:case 11:this.$=o[k].trim(),u.getCommonDb().setAccDescription(this.$);break;case 12:u.addSection(o[k].substr(8)),this.$=o[k].substr(8);break;case 15:u.addTask(o[k],0,""),this.$=o[k];break;case 16:u.addEvent(o[k].substr(2)),this.$=o[k];break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},n(t,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:e,12:l,14:r,16:h,17:c,18:14,19:15,20:g,21:m},n(t,[2,7],{1:[2,1]}),n(t,[2,3]),{9:18,11:e,12:l,14:r,16:h,17:c,18:14,19:15,20:g,21:m},n(t,[2,5]),n(t,[2,6]),n(t,[2,8]),{13:[1,19]},{15:[1,20]},n(t,[2,11]),n(t,[2,12]),n(t,[2,13]),n(t,[2,14]),n(t,[2,15]),n(t,[2,16]),n(t,[2,4]),n(t,[2,9]),n(t,[2,10])],defaultActions:{},parseError:s(function(i,a){if(a.recoverable)this.trace(i);else{var d=new Error(i);throw d.hash=a,d}},"parseError"),parse:s(function(i){var a=this,d=[0],u=[],y=[null],o=[],S=this.table,k="",M=0,P=0,B=2,J=1,O=o.slice.call(arguments,1),_=Object.create(this.lexer),E={yy:{}};for(var v in this.yy)Object.prototype.hasOwnProperty.call(this.yy,v)&&(E.yy[v]=this.yy[v]);_.setInput(i,E.yy),E.yy.lexer=_,E.yy.parser=this,typeof _.yylloc>"u"&&(_.yylloc={});var L=_.yylloc;o.push(L);var A=_.options&&_.options.ranges;typeof E.yy.parseError=="function"?this.parseError=E.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function R(I){d.length=d.length-2*I,y.length=y.length-I,o.length=o.length-I}s(R,"popStack");function z(){var I;return I=u.pop()||_.lex()||J,typeof I!="number"&&(I instanceof Array&&(u=I,I=u.pop()),I=a.symbols_[I]||I),I}s(z,"lex");for(var w,C,N,K,F={},j,$,et,G;;){if(C=d[d.length-1],this.defaultActions[C]?N=this.defaultActions[C]:((w===null||typeof w>"u")&&(w=z()),N=S[C]&&S[C][w]),typeof N>"u"||!N.length||!N[0]){var Q="";G=[];for(j in S[C])this.terminals_[j]&&j>B&&G.push("'"+this.terminals_[j]+"'");_.showPosition?Q="Parse error on line "+(M+1)+`:
|
||||
`+_.showPosition()+`
|
||||
Expecting `+G.join(", ")+", got '"+(this.terminals_[w]||w)+"'":Q="Parse error on line "+(M+1)+": Unexpected "+(w==J?"end of input":"'"+(this.terminals_[w]||w)+"'"),this.parseError(Q,{text:_.match,token:this.terminals_[w]||w,line:_.yylineno,loc:L,expected:G})}if(N[0]instanceof Array&&N.length>1)throw new Error("Parse Error: multiple actions possible at state: "+C+", token: "+w);switch(N[0]){case 1:d.push(w),y.push(_.yytext),o.push(_.yylloc),d.push(N[1]),w=null,P=_.yyleng,k=_.yytext,M=_.yylineno,L=_.yylloc;break;case 2:if($=this.productions_[N[1]][1],F.$=y[y.length-$],F._$={first_line:o[o.length-($||1)].first_line,last_line:o[o.length-1].last_line,first_column:o[o.length-($||1)].first_column,last_column:o[o.length-1].last_column},A&&(F._$.range=[o[o.length-($||1)].range[0],o[o.length-1].range[1]]),K=this.performAction.apply(F,[k,P,M,E.yy,N[1],y,o].concat(O)),typeof K<"u")return K;$&&(d=d.slice(0,-1*$*2),y=y.slice(0,-1*$),o=o.slice(0,-1*$)),d.push(this.productions_[N[1]][0]),y.push(F.$),o.push(F._$),et=S[d[d.length-2]][d[d.length-1]],d.push(et);break;case 3:return!0}}return!0},"parse")},x=function(){var f={EOF:1,parseError:s(function(a,d){if(this.yy.parser)this.yy.parser.parseError(a,d);else throw new Error(a)},"parseError"),setInput:s(function(i,a){return this.yy=a||this.yy||{},this._input=i,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var i=this._input[0];this.yytext+=i,this.yyleng++,this.offset++,this.match+=i,this.matched+=i;var a=i.match(/(?:\r\n?|\n).*/g);return a?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),i},"input"),unput:s(function(i){var a=i.length,d=i.split(/(?:\r\n?|\n)/g);this._input=i+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-a),this.offset-=a;var u=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),d.length-1&&(this.yylineno-=d.length-1);var y=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:d?(d.length===u.length?this.yylloc.first_column:0)+u[u.length-d.length].length-d[0].length:this.yylloc.first_column-a},this.options.ranges&&(this.yylloc.range=[y[0],y[0]+this.yyleng-a]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
|
||||
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(i){this.unput(this.match.slice(i))},"less"),pastInput:s(function(){var i=this.matched.substr(0,this.matched.length-this.match.length);return(i.length>20?"...":"")+i.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var i=this.match;return i.length<20&&(i+=this._input.substr(0,20-i.length)),(i.substr(0,20)+(i.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var i=this.pastInput(),a=new Array(i.length+1).join("-");return i+this.upcomingInput()+`
|
File diff suppressed because one or more lines are too long
|
@ -8,18 +8,18 @@
|
|||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Lightrag</title>
|
||||
<script type="module" crossorigin src="/webui/assets/index-BghbDBWC.js"></script>
|
||||
<script type="module" crossorigin src="/webui/assets/index-C3sDhLdo.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/react-vendor-DEwriMA6.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/ui-vendor-CeCm8EER.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/graph-vendor-B-X5JegA.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/utils-vendor-BysuhMZA.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/feature-graph-C2oTJZJ8.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/mermaid-vendor-CY273lNM.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/feature-graph-Chuw_ipx.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/feature-documents-36AWafKt.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/mermaid-vendor-DGPC_TDM.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/markdown-vendor-DmIvJdn7.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/feature-retrieval-CpuDBjCo.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/feature-documents-CHAt2SlA.js">
|
||||
<link rel="modulepreload" crossorigin href="/webui/assets/feature-retrieval-Bsekt8JN.js">
|
||||
<link rel="stylesheet" crossorigin href="/webui/assets/feature-graph-BipNuM18.css">
|
||||
<link rel="stylesheet" crossorigin href="/webui/assets/index-DwO2XWaU.css">
|
||||
<link rel="stylesheet" crossorigin href="/webui/assets/index-CafJWW1u.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
@ -23,6 +23,11 @@ from .constants import (
|
|||
DEFAULT_MAX_TOTAL_TOKENS,
|
||||
DEFAULT_HISTORY_TURNS,
|
||||
DEFAULT_ENABLE_RERANK,
|
||||
DEFAULT_OLLAMA_MODEL_NAME,
|
||||
DEFAULT_OLLAMA_MODEL_TAG,
|
||||
DEFAULT_OLLAMA_MODEL_SIZE,
|
||||
DEFAULT_OLLAMA_CREATED_AT,
|
||||
DEFAULT_OLLAMA_DIGEST,
|
||||
)
|
||||
|
||||
# use the .env that is inside the current folder
|
||||
|
@ -31,6 +36,39 @@ from .constants import (
|
|||
load_dotenv(dotenv_path=".env", override=False)
|
||||
|
||||
|
||||
class OllamaServerInfos:
|
||||
def __init__(self, name=None, tag=None):
|
||||
self._lightrag_name = name or os.getenv(
|
||||
"OLLAMA_EMULATING_MODEL_NAME", DEFAULT_OLLAMA_MODEL_NAME
|
||||
)
|
||||
self._lightrag_tag = tag or os.getenv(
|
||||
"OLLAMA_EMULATING_MODEL_TAG", DEFAULT_OLLAMA_MODEL_TAG
|
||||
)
|
||||
self.LIGHTRAG_SIZE = DEFAULT_OLLAMA_MODEL_SIZE
|
||||
self.LIGHTRAG_CREATED_AT = DEFAULT_OLLAMA_CREATED_AT
|
||||
self.LIGHTRAG_DIGEST = DEFAULT_OLLAMA_DIGEST
|
||||
|
||||
@property
|
||||
def LIGHTRAG_NAME(self):
|
||||
return self._lightrag_name
|
||||
|
||||
@LIGHTRAG_NAME.setter
|
||||
def LIGHTRAG_NAME(self, value):
|
||||
self._lightrag_name = value
|
||||
|
||||
@property
|
||||
def LIGHTRAG_TAG(self):
|
||||
return self._lightrag_tag
|
||||
|
||||
@LIGHTRAG_TAG.setter
|
||||
def LIGHTRAG_TAG(self, value):
|
||||
self._lightrag_tag = value
|
||||
|
||||
@property
|
||||
def LIGHTRAG_MODEL(self):
|
||||
return f"{self._lightrag_name}:{self._lightrag_tag}"
|
||||
|
||||
|
||||
class TextChunkSchema(TypedDict):
|
||||
tokens: int
|
||||
content: str
|
||||
|
@ -100,6 +138,7 @@ class QueryParam:
|
|||
Format: [{"role": "user/assistant", "content": "message"}].
|
||||
"""
|
||||
|
||||
# Deprecated: history message have negtive effect on query performance
|
||||
history_turns: int = int(os.getenv("HISTORY_TURNS", str(DEFAULT_HISTORY_TURNS)))
|
||||
"""Number of complete conversation turns (user-assistant pairs) to consider in the response context."""
|
||||
|
||||
|
@ -629,8 +668,6 @@ class DocStatus(str, Enum):
|
|||
class DocProcessingStatus:
|
||||
"""Document processing status data structure"""
|
||||
|
||||
content: str
|
||||
"""Original content of the document"""
|
||||
content_summary: str
|
||||
"""First 100 chars of document content, used for preview"""
|
||||
content_length: int
|
||||
|
@ -643,11 +680,13 @@ class DocProcessingStatus:
|
|||
"""ISO format timestamp when document was created"""
|
||||
updated_at: str
|
||||
"""ISO format timestamp when document was last updated"""
|
||||
track_id: str | None = None
|
||||
"""Tracking ID for monitoring progress"""
|
||||
chunks_count: int | None = None
|
||||
"""Number of chunks after splitting, used for processing"""
|
||||
chunks_list: list[str] | None = field(default_factory=list)
|
||||
"""List of chunk IDs associated with this document, used for deletion"""
|
||||
error: str | None = None
|
||||
error_msg: str | None = None
|
||||
"""Error message if failed"""
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
"""Additional metadata"""
|
||||
|
@ -667,6 +706,42 @@ class DocStatusStorage(BaseKVStorage, ABC):
|
|||
) -> dict[str, DocProcessingStatus]:
|
||||
"""Get all documents with a specific status"""
|
||||
|
||||
@abstractmethod
|
||||
async def get_docs_by_track_id(
|
||||
self, track_id: str
|
||||
) -> dict[str, DocProcessingStatus]:
|
||||
"""Get all documents with a specific track_id"""
|
||||
|
||||
@abstractmethod
|
||||
async def get_docs_paginated(
|
||||
self,
|
||||
status_filter: DocStatus | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
sort_field: str = "updated_at",
|
||||
sort_direction: str = "desc",
|
||||
) -> tuple[list[tuple[str, DocProcessingStatus]], int]:
|
||||
"""Get documents with pagination support
|
||||
|
||||
Args:
|
||||
status_filter: Filter by document status, None for all statuses
|
||||
page: Page number (1-based)
|
||||
page_size: Number of documents per page (10-200)
|
||||
sort_field: Field to sort by ('created_at', 'updated_at', 'id')
|
||||
sort_direction: Sort direction ('asc' or 'desc')
|
||||
|
||||
Returns:
|
||||
Tuple of (list of (doc_id, DocProcessingStatus) tuples, total_count)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def get_all_status_counts(self) -> dict[str, int]:
|
||||
"""Get counts of documents in each status for all documents
|
||||
|
||||
Returns:
|
||||
Dictionary mapping status names to counts
|
||||
"""
|
||||
|
||||
async def drop_cache_by_modes(self, modes: list[str] | None = None) -> bool:
|
||||
"""Drop cache is not supported for Doc Status storage"""
|
||||
return False
|
||||
|
|
|
@ -6,27 +6,59 @@ different parts of the LightRAG system. Centralizing these values ensures
|
|||
consistency and makes maintenance easier.
|
||||
"""
|
||||
|
||||
# Default values for environment variables
|
||||
DEFAULT_MAX_GLEANING = 1
|
||||
DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE = 4
|
||||
# Default values for server settings
|
||||
DEFAULT_WOKERS = 2
|
||||
DEFAULT_TIMEOUT = 150
|
||||
DEFAULT_MAX_GRAPH_NODES = 1000
|
||||
|
||||
# Default values for extraction settings
|
||||
DEFAULT_SUMMARY_LANGUAGE = "English" # Default language for summaries
|
||||
DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE = 4
|
||||
DEFAULT_MAX_GLEANING = 1
|
||||
DEFAULT_SUMMARY_MAX_TOKENS = 10000 # Default maximum token size
|
||||
|
||||
# Separator for graph fields
|
||||
GRAPH_FIELD_SEP = "<SEP>"
|
||||
|
||||
# Query and retrieval configuration defaults
|
||||
DEFAULT_TOP_K = 40
|
||||
DEFAULT_CHUNK_TOP_K = 10
|
||||
DEFAULT_MAX_ENTITY_TOKENS = 10000
|
||||
DEFAULT_MAX_RELATION_TOKENS = 10000
|
||||
DEFAULT_MAX_TOTAL_TOKENS = 32000
|
||||
DEFAULT_HISTORY_TURNS = 0
|
||||
DEFAULT_ENABLE_RERANK = True
|
||||
DEFAULT_MAX_TOTAL_TOKENS = 30000
|
||||
DEFAULT_COSINE_THRESHOLD = 0.2
|
||||
DEFAULT_RELATED_CHUNK_NUMBER = 10
|
||||
DEFAULT_RELATED_CHUNK_NUMBER = 5
|
||||
# Deprated: history message have negtive effect on query performance
|
||||
DEFAULT_HISTORY_TURNS = 0
|
||||
|
||||
# Separator for graph fields
|
||||
GRAPH_FIELD_SEP = "<SEP>"
|
||||
# Rerank configuration defaults
|
||||
DEFAULT_ENABLE_RERANK = True
|
||||
DEFAULT_MIN_RERANK_SCORE = 0.0
|
||||
|
||||
# File path configuration for vector and graph database(Should not be changed, used in Milvus Schema)
|
||||
DEFAULT_MAX_FILE_PATH_LENGTH = 4090
|
||||
|
||||
# Default temperature for LLM
|
||||
DEFAULT_TEMPERATURE = 1.0
|
||||
|
||||
# Async configuration defaults
|
||||
DEFAULT_MAX_ASYNC = 4 # Default maximum async operations
|
||||
DEFAULT_MAX_PARALLEL_INSERT = 2 # Default maximum parallel insert operations
|
||||
|
||||
# Embedding configuration defaults
|
||||
DEFAULT_EMBEDDING_FUNC_MAX_ASYNC = 8 # Default max async for embedding functions
|
||||
DEFAULT_EMBEDDING_BATCH_NUM = 10 # Default batch size for embedding computations
|
||||
|
||||
# Ollama Server Timetout in seconds
|
||||
DEFAULT_TIMEOUT = 150
|
||||
|
||||
# Logging configuration defaults
|
||||
DEFAULT_LOG_MAX_BYTES = 10485760 # Default 10MB
|
||||
DEFAULT_LOG_BACKUP_COUNT = 5 # Default 5 backups
|
||||
DEFAULT_LOG_FILENAME = "lightrag.log" # Default log filename
|
||||
|
||||
# Ollama server configuration defaults
|
||||
DEFAULT_OLLAMA_MODEL_NAME = "lightrag"
|
||||
DEFAULT_OLLAMA_MODEL_TAG = "latest"
|
||||
DEFAULT_OLLAMA_MODEL_SIZE = 7365960935
|
||||
DEFAULT_OLLAMA_CREATED_AT = "2024-01-15T00:00:00Z"
|
||||
DEFAULT_OLLAMA_DIGEST = "sha256:lightrag"
|
||||
|
|
|
@ -45,21 +45,22 @@ class JsonDocStatusStorage(DocStatusStorage):
|
|||
self._data = None
|
||||
self._storage_lock = None
|
||||
self.storage_updated = None
|
||||
self.final_namespace = f"{self.workspace}_{self.namespace}"
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize storage data"""
|
||||
self._storage_lock = get_storage_lock()
|
||||
self.storage_updated = await get_update_flag(self.namespace)
|
||||
self.storage_updated = await get_update_flag(self.final_namespace)
|
||||
async with get_data_init_lock():
|
||||
# check need_init must before get_namespace_data
|
||||
need_init = await try_initialize_namespace(self.namespace)
|
||||
self._data = await get_namespace_data(self.namespace)
|
||||
need_init = await try_initialize_namespace(self.final_namespace)
|
||||
self._data = await get_namespace_data(self.final_namespace)
|
||||
if need_init:
|
||||
loaded_data = load_json(self._file_name) or {}
|
||||
async with self._storage_lock:
|
||||
self._data.update(loaded_data)
|
||||
logger.info(
|
||||
f"Process {os.getpid()} doc status load {self.namespace} with {len(loaded_data)} records"
|
||||
f"Process {os.getpid()} doc status load {self.final_namespace} with {len(loaded_data)} records"
|
||||
)
|
||||
|
||||
async def filter_keys(self, keys: set[str]) -> set[str]:
|
||||
|
@ -95,12 +96,43 @@ class JsonDocStatusStorage(DocStatusStorage):
|
|||
try:
|
||||
# Make a copy of the data to avoid modifying the original
|
||||
data = v.copy()
|
||||
# If content is missing, use content_summary as content
|
||||
if "content" not in data and "content_summary" in data:
|
||||
data["content"] = data["content_summary"]
|
||||
# Remove deprecated content field if it exists
|
||||
data.pop("content", None)
|
||||
# If file_path is not in data, use document id as file path
|
||||
if "file_path" not in data:
|
||||
data["file_path"] = "no-file-path"
|
||||
# Ensure new fields exist with default values
|
||||
if "metadata" not in data:
|
||||
data["metadata"] = {}
|
||||
if "error_msg" not in data:
|
||||
data["error_msg"] = None
|
||||
result[k] = DocProcessingStatus(**data)
|
||||
except KeyError as e:
|
||||
logger.error(f"Missing required field for document {k}: {e}")
|
||||
continue
|
||||
return result
|
||||
|
||||
async def get_docs_by_track_id(
|
||||
self, track_id: str
|
||||
) -> dict[str, DocProcessingStatus]:
|
||||
"""Get all documents with a specific track_id"""
|
||||
result = {}
|
||||
async with self._storage_lock:
|
||||
for k, v in self._data.items():
|
||||
if v.get("track_id") == track_id:
|
||||
try:
|
||||
# Make a copy of the data to avoid modifying the original
|
||||
data = v.copy()
|
||||
# Remove deprecated content field if it exists
|
||||
data.pop("content", None)
|
||||
# If file_path is not in data, use document id as file path
|
||||
if "file_path" not in data:
|
||||
data["file_path"] = "no-file-path"
|
||||
# Ensure new fields exist with default values
|
||||
if "metadata" not in data:
|
||||
data["metadata"] = {}
|
||||
if "error_msg" not in data:
|
||||
data["error_msg"] = None
|
||||
result[k] = DocProcessingStatus(**data)
|
||||
except KeyError as e:
|
||||
logger.error(f"Missing required field for document {k}: {e}")
|
||||
|
@ -114,10 +146,10 @@ class JsonDocStatusStorage(DocStatusStorage):
|
|||
dict(self._data) if hasattr(self._data, "_getvalue") else self._data
|
||||
)
|
||||
logger.debug(
|
||||
f"Process {os.getpid()} doc status writting {len(data_dict)} records to {self.namespace}"
|
||||
f"Process {os.getpid()} doc status writting {len(data_dict)} records to {self.final_namespace}"
|
||||
)
|
||||
write_json(data_dict, self._file_name)
|
||||
await clear_all_update_flags(self.namespace)
|
||||
await clear_all_update_flags(self.final_namespace)
|
||||
|
||||
async def upsert(self, data: dict[str, dict[str, Any]]) -> None:
|
||||
"""
|
||||
|
@ -127,14 +159,14 @@ class JsonDocStatusStorage(DocStatusStorage):
|
|||
"""
|
||||
if not data:
|
||||
return
|
||||
logger.debug(f"Inserting {len(data)} records to {self.namespace}")
|
||||
logger.debug(f"Inserting {len(data)} records to {self.final_namespace}")
|
||||
async with self._storage_lock:
|
||||
# Ensure chunks_list field exists for new documents
|
||||
for doc_id, doc_data in data.items():
|
||||
if "chunks_list" not in doc_data:
|
||||
doc_data["chunks_list"] = []
|
||||
self._data.update(data)
|
||||
await set_all_update_flags(self.namespace)
|
||||
await set_all_update_flags(self.final_namespace)
|
||||
|
||||
await self.index_done_callback()
|
||||
|
||||
|
@ -142,6 +174,111 @@ class JsonDocStatusStorage(DocStatusStorage):
|
|||
async with self._storage_lock:
|
||||
return self._data.get(id)
|
||||
|
||||
async def get_docs_paginated(
|
||||
self,
|
||||
status_filter: DocStatus | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
sort_field: str = "updated_at",
|
||||
sort_direction: str = "desc",
|
||||
) -> tuple[list[tuple[str, DocProcessingStatus]], int]:
|
||||
"""Get documents with pagination support
|
||||
|
||||
Args:
|
||||
status_filter: Filter by document status, None for all statuses
|
||||
page: Page number (1-based)
|
||||
page_size: Number of documents per page (10-200)
|
||||
sort_field: Field to sort by ('created_at', 'updated_at', 'id')
|
||||
sort_direction: Sort direction ('asc' or 'desc')
|
||||
|
||||
Returns:
|
||||
Tuple of (list of (doc_id, DocProcessingStatus) tuples, total_count)
|
||||
"""
|
||||
# Validate parameters
|
||||
if page < 1:
|
||||
page = 1
|
||||
if page_size < 10:
|
||||
page_size = 10
|
||||
elif page_size > 200:
|
||||
page_size = 200
|
||||
|
||||
if sort_field not in ["created_at", "updated_at", "id", "file_path"]:
|
||||
sort_field = "updated_at"
|
||||
|
||||
if sort_direction.lower() not in ["asc", "desc"]:
|
||||
sort_direction = "desc"
|
||||
|
||||
# For JSON storage, we load all data and sort/filter in memory
|
||||
all_docs = []
|
||||
|
||||
async with self._storage_lock:
|
||||
for doc_id, doc_data in self._data.items():
|
||||
# Apply status filter
|
||||
if (
|
||||
status_filter is not None
|
||||
and doc_data.get("status") != status_filter.value
|
||||
):
|
||||
continue
|
||||
|
||||
try:
|
||||
# Prepare document data
|
||||
data = doc_data.copy()
|
||||
data.pop("content", None)
|
||||
if "file_path" not in data:
|
||||
data["file_path"] = "no-file-path"
|
||||
if "metadata" not in data:
|
||||
data["metadata"] = {}
|
||||
if "error_msg" not in data:
|
||||
data["error_msg"] = None
|
||||
|
||||
doc_status = DocProcessingStatus(**data)
|
||||
|
||||
# Add sort key for sorting
|
||||
if sort_field == "id":
|
||||
doc_status._sort_key = doc_id
|
||||
else:
|
||||
doc_status._sort_key = getattr(doc_status, sort_field, "")
|
||||
|
||||
all_docs.append((doc_id, doc_status))
|
||||
|
||||
except KeyError as e:
|
||||
logger.error(f"Error processing document {doc_id}: {e}")
|
||||
continue
|
||||
|
||||
# Sort documents
|
||||
reverse_sort = sort_direction.lower() == "desc"
|
||||
all_docs.sort(
|
||||
key=lambda x: getattr(x[1], "_sort_key", ""), reverse=reverse_sort
|
||||
)
|
||||
|
||||
# Remove sort key from documents
|
||||
for doc_id, doc in all_docs:
|
||||
if hasattr(doc, "_sort_key"):
|
||||
delattr(doc, "_sort_key")
|
||||
|
||||
total_count = len(all_docs)
|
||||
|
||||
# Apply pagination
|
||||
start_idx = (page - 1) * page_size
|
||||
end_idx = start_idx + page_size
|
||||
paginated_docs = all_docs[start_idx:end_idx]
|
||||
|
||||
return paginated_docs, total_count
|
||||
|
||||
async def get_all_status_counts(self) -> dict[str, int]:
|
||||
"""Get counts of documents in each status for all documents
|
||||
|
||||
Returns:
|
||||
Dictionary mapping status names to counts, including 'all' field
|
||||
"""
|
||||
counts = await self.get_status_counts()
|
||||
|
||||
# Add 'all' field with total count
|
||||
total_count = sum(counts.values())
|
||||
counts["all"] = total_count
|
||||
|
||||
return counts
|
||||
|
||||
async def delete(self, doc_ids: list[str]) -> None:
|
||||
"""Delete specific records from storage by their IDs
|
||||
|
||||
|
@ -163,7 +300,7 @@ class JsonDocStatusStorage(DocStatusStorage):
|
|||
any_deleted = True
|
||||
|
||||
if any_deleted:
|
||||
await set_all_update_flags(self.namespace)
|
||||
await set_all_update_flags(self.final_namespace)
|
||||
|
||||
async def drop(self) -> dict[str, str]:
|
||||
"""Drop all document status data from storage and clean up resources
|
||||
|
@ -181,11 +318,11 @@ class JsonDocStatusStorage(DocStatusStorage):
|
|||
try:
|
||||
async with self._storage_lock:
|
||||
self._data.clear()
|
||||
await set_all_update_flags(self.namespace)
|
||||
await set_all_update_flags(self.final_namespace)
|
||||
|
||||
await self.index_done_callback()
|
||||
logger.info(f"Process {os.getpid()} drop {self.namespace}")
|
||||
logger.info(f"Process {os.getpid()} drop {self.final_namespace}")
|
||||
return {"status": "success", "message": "data dropped"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error dropping {self.namespace}: {e}")
|
||||
logger.error(f"Error dropping {self.final_namespace}: {e}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
|
|
@ -41,15 +41,16 @@ class JsonKVStorage(BaseKVStorage):
|
|||
self._data = None
|
||||
self._storage_lock = None
|
||||
self.storage_updated = None
|
||||
self.final_namespace = f"{self.workspace}_{self.namespace}"
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize storage data"""
|
||||
self._storage_lock = get_storage_lock()
|
||||
self.storage_updated = await get_update_flag(self.namespace)
|
||||
self.storage_updated = await get_update_flag(self.final_namespace)
|
||||
async with get_data_init_lock():
|
||||
# check need_init must before get_namespace_data
|
||||
need_init = await try_initialize_namespace(self.namespace)
|
||||
self._data = await get_namespace_data(self.namespace)
|
||||
need_init = await try_initialize_namespace(self.final_namespace)
|
||||
self._data = await get_namespace_data(self.final_namespace)
|
||||
if need_init:
|
||||
loaded_data = load_json(self._file_name) or {}
|
||||
async with self._storage_lock:
|
||||
|
@ -63,7 +64,7 @@ class JsonKVStorage(BaseKVStorage):
|
|||
data_count = len(loaded_data)
|
||||
|
||||
logger.info(
|
||||
f"Process {os.getpid()} KV load {self.namespace} with {data_count} records"
|
||||
f"Process {os.getpid()} KV load {self.final_namespace} with {data_count} records"
|
||||
)
|
||||
|
||||
async def index_done_callback(self) -> None:
|
||||
|
@ -77,10 +78,10 @@ class JsonKVStorage(BaseKVStorage):
|
|||
data_count = len(data_dict)
|
||||
|
||||
logger.debug(
|
||||
f"Process {os.getpid()} KV writting {data_count} records to {self.namespace}"
|
||||
f"Process {os.getpid()} KV writting {data_count} records to {self.final_namespace}"
|
||||
)
|
||||
write_json(data_dict, self._file_name)
|
||||
await clear_all_update_flags(self.namespace)
|
||||
await clear_all_update_flags(self.final_namespace)
|
||||
|
||||
async def get_all(self) -> dict[str, Any]:
|
||||
"""Get all data from storage
|
||||
|
@ -150,7 +151,7 @@ class JsonKVStorage(BaseKVStorage):
|
|||
|
||||
current_time = int(time.time()) # Get current Unix timestamp
|
||||
|
||||
logger.debug(f"Inserting {len(data)} records to {self.namespace}")
|
||||
logger.debug(f"Inserting {len(data)} records to {self.final_namespace}")
|
||||
async with self._storage_lock:
|
||||
# Add timestamps to data based on whether key exists
|
||||
for k, v in data.items():
|
||||
|
@ -169,7 +170,7 @@ class JsonKVStorage(BaseKVStorage):
|
|||
v["_id"] = k
|
||||
|
||||
self._data.update(data)
|
||||
await set_all_update_flags(self.namespace)
|
||||
await set_all_update_flags(self.final_namespace)
|
||||
|
||||
async def delete(self, ids: list[str]) -> None:
|
||||
"""Delete specific records from storage by their IDs
|
||||
|
@ -192,7 +193,7 @@ class JsonKVStorage(BaseKVStorage):
|
|||
any_deleted = True
|
||||
|
||||
if any_deleted:
|
||||
await set_all_update_flags(self.namespace)
|
||||
await set_all_update_flags(self.final_namespace)
|
||||
|
||||
async def drop_cache_by_modes(self, modes: list[str] | None = None) -> bool:
|
||||
"""Delete specific records from storage by cache mode
|
||||
|
@ -227,7 +228,7 @@ class JsonKVStorage(BaseKVStorage):
|
|||
self._data.pop(key, None)
|
||||
|
||||
if keys_to_delete:
|
||||
await set_all_update_flags(self.namespace)
|
||||
await set_all_update_flags(self.final_namespace)
|
||||
logger.info(
|
||||
f"Dropped {len(keys_to_delete)} cache entries for modes: {modes}"
|
||||
)
|
||||
|
@ -276,7 +277,7 @@ class JsonKVStorage(BaseKVStorage):
|
|||
# del self._data[mode_key]
|
||||
|
||||
# # Set update flags to notify persistence is needed
|
||||
# await set_all_update_flags(self.namespace)
|
||||
# await set_all_update_flags(self.final_namespace)
|
||||
|
||||
# logger.info(f"Cleared cache for {len(chunk_ids)} chunk IDs")
|
||||
# return True
|
||||
|
@ -301,13 +302,13 @@ class JsonKVStorage(BaseKVStorage):
|
|||
try:
|
||||
async with self._storage_lock:
|
||||
self._data.clear()
|
||||
await set_all_update_flags(self.namespace)
|
||||
await set_all_update_flags(self.final_namespace)
|
||||
|
||||
await self.index_done_callback()
|
||||
logger.info(f"Process {os.getpid()} drop {self.namespace}")
|
||||
logger.info(f"Process {os.getpid()} drop {self.final_namespace}")
|
||||
return {"status": "success", "message": "data dropped"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error dropping {self.namespace}: {e}")
|
||||
logger.error(f"Error dropping {self.final_namespace}: {e}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
async def _migrate_legacy_cache_structure(self, data: dict) -> dict:
|
||||
|
|
|
@ -5,6 +5,7 @@ from dataclasses import dataclass
|
|||
import numpy as np
|
||||
from lightrag.utils import logger, compute_mdhash_id
|
||||
from ..base import BaseVectorStorage
|
||||
from ..constants import DEFAULT_MAX_FILE_PATH_LENGTH
|
||||
import pipmaster as pm
|
||||
|
||||
if not pm.is_installed("pymilvus"):
|
||||
|
@ -47,7 +48,7 @@ class MilvusVectorDBStorage(BaseVectorStorage):
|
|||
FieldSchema(
|
||||
name="file_path",
|
||||
dtype=DataType.VARCHAR,
|
||||
max_length=1024,
|
||||
max_length=DEFAULT_MAX_FILE_PATH_LENGTH,
|
||||
nullable=True,
|
||||
),
|
||||
]
|
||||
|
@ -64,7 +65,7 @@ class MilvusVectorDBStorage(BaseVectorStorage):
|
|||
FieldSchema(
|
||||
name="file_path",
|
||||
dtype=DataType.VARCHAR,
|
||||
max_length=1024,
|
||||
max_length=DEFAULT_MAX_FILE_PATH_LENGTH,
|
||||
nullable=True,
|
||||
),
|
||||
]
|
||||
|
@ -536,7 +537,7 @@ class MilvusVectorDBStorage(BaseVectorStorage):
|
|||
# Load the collection if it's not already loaded
|
||||
# In Milvus, collections need to be loaded before they can be searched
|
||||
self._client.load_collection(self.namespace)
|
||||
logger.debug(f"Collection {self.namespace} loaded successfully")
|
||||
# logger.debug(f"Collection {self.namespace} loaded successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load collection {self.namespace}: {e}")
|
||||
|
|
|
@ -321,6 +321,13 @@ class MongoDocStatusStorage(DocStatusStorage):
|
|||
if self.db is None:
|
||||
self.db = await ClientManager.get_client()
|
||||
self._data = await get_or_create_collection(self.db, self._collection_name)
|
||||
|
||||
# Create track_id index for better query performance
|
||||
await self.create_track_id_index_if_not_exists()
|
||||
|
||||
# Create pagination indexes for better query performance
|
||||
await self.create_pagination_indexes_if_not_exists()
|
||||
|
||||
logger.debug(f"Use MongoDB as DocStatus {self._collection_name}")
|
||||
|
||||
async def finalize(self):
|
||||
|
@ -359,7 +366,7 @@ class MongoDocStatusStorage(DocStatusStorage):
|
|||
async def get_status_counts(self) -> dict[str, int]:
|
||||
"""Get counts of documents in each status"""
|
||||
pipeline = [{"$group": {"_id": "$status", "count": {"$sum": 1}}}]
|
||||
cursor = self._data.aggregate(pipeline, allowDiskUse=True)
|
||||
cursor = await self._data.aggregate(pipeline, allowDiskUse=True)
|
||||
result = await cursor.to_list()
|
||||
counts = {}
|
||||
for doc in result:
|
||||
|
@ -372,20 +379,57 @@ class MongoDocStatusStorage(DocStatusStorage):
|
|||
"""Get all documents with a specific status"""
|
||||
cursor = self._data.find({"status": status.value})
|
||||
result = await cursor.to_list()
|
||||
return {
|
||||
doc["_id"]: DocProcessingStatus(
|
||||
content=doc["content"],
|
||||
content_summary=doc.get("content_summary"),
|
||||
content_length=doc["content_length"],
|
||||
status=doc["status"],
|
||||
created_at=doc.get("created_at"),
|
||||
updated_at=doc.get("updated_at"),
|
||||
chunks_count=doc.get("chunks_count", -1),
|
||||
file_path=doc.get("file_path", doc["_id"]),
|
||||
chunks_list=doc.get("chunks_list", []),
|
||||
)
|
||||
for doc in result
|
||||
}
|
||||
processed_result = {}
|
||||
for doc in result:
|
||||
try:
|
||||
# Make a copy of the data to avoid modifying the original
|
||||
data = doc.copy()
|
||||
# Remove deprecated content field if it exists
|
||||
data.pop("content", None)
|
||||
# Remove MongoDB _id field if it exists
|
||||
data.pop("_id", None)
|
||||
# If file_path is not in data, use document id as file path
|
||||
if "file_path" not in data:
|
||||
data["file_path"] = "no-file-path"
|
||||
# Ensure new fields exist with default values
|
||||
if "metadata" not in data:
|
||||
data["metadata"] = {}
|
||||
if "error_msg" not in data:
|
||||
data["error_msg"] = None
|
||||
processed_result[doc["_id"]] = DocProcessingStatus(**data)
|
||||
except KeyError as e:
|
||||
logger.error(f"Missing required field for document {doc['_id']}: {e}")
|
||||
continue
|
||||
return processed_result
|
||||
|
||||
async def get_docs_by_track_id(
|
||||
self, track_id: str
|
||||
) -> dict[str, DocProcessingStatus]:
|
||||
"""Get all documents with a specific track_id"""
|
||||
cursor = self._data.find({"track_id": track_id})
|
||||
result = await cursor.to_list()
|
||||
processed_result = {}
|
||||
for doc in result:
|
||||
try:
|
||||
# Make a copy of the data to avoid modifying the original
|
||||
data = doc.copy()
|
||||
# Remove deprecated content field if it exists
|
||||
data.pop("content", None)
|
||||
# Remove MongoDB _id field if it exists
|
||||
data.pop("_id", None)
|
||||
# If file_path is not in data, use document id as file path
|
||||
if "file_path" not in data:
|
||||
data["file_path"] = "no-file-path"
|
||||
# Ensure new fields exist with default values
|
||||
if "metadata" not in data:
|
||||
data["metadata"] = {}
|
||||
if "error_msg" not in data:
|
||||
data["error_msg"] = None
|
||||
processed_result[doc["_id"]] = DocProcessingStatus(**data)
|
||||
except KeyError as e:
|
||||
logger.error(f"Missing required field for document {doc['_id']}: {e}")
|
||||
continue
|
||||
return processed_result
|
||||
|
||||
async def index_done_callback(self) -> None:
|
||||
# Mongo handles persistence automatically
|
||||
|
@ -415,6 +459,181 @@ class MongoDocStatusStorage(DocStatusStorage):
|
|||
async def delete(self, ids: list[str]) -> None:
|
||||
await self._data.delete_many({"_id": {"$in": ids}})
|
||||
|
||||
async def create_track_id_index_if_not_exists(self):
|
||||
"""Create track_id index for better query performance"""
|
||||
try:
|
||||
# Check if index already exists
|
||||
indexes_cursor = await self._data.list_indexes()
|
||||
existing_indexes = await indexes_cursor.to_list(length=None)
|
||||
track_id_index_exists = any(
|
||||
"track_id" in idx.get("key", {}) for idx in existing_indexes
|
||||
)
|
||||
|
||||
if not track_id_index_exists:
|
||||
await self._data.create_index("track_id")
|
||||
logger.info(
|
||||
f"Created track_id index for collection {self._collection_name}"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"track_id index already exists for collection {self._collection_name}"
|
||||
)
|
||||
|
||||
except PyMongoError as e:
|
||||
logger.error(
|
||||
f"Error creating track_id index for {self._collection_name}: {e}"
|
||||
)
|
||||
|
||||
async def create_pagination_indexes_if_not_exists(self):
|
||||
"""Create indexes to optimize pagination queries"""
|
||||
try:
|
||||
indexes_cursor = await self._data.list_indexes()
|
||||
existing_indexes = await indexes_cursor.to_list(length=None)
|
||||
|
||||
# Define indexes needed for pagination
|
||||
pagination_indexes = [
|
||||
{
|
||||
"name": "status_updated_at",
|
||||
"keys": [("status", 1), ("updated_at", -1)],
|
||||
},
|
||||
{
|
||||
"name": "status_created_at",
|
||||
"keys": [("status", 1), ("created_at", -1)],
|
||||
},
|
||||
{"name": "updated_at", "keys": [("updated_at", -1)]},
|
||||
{"name": "created_at", "keys": [("created_at", -1)]},
|
||||
{"name": "id", "keys": [("_id", 1)]},
|
||||
{"name": "file_path", "keys": [("file_path", 1)]},
|
||||
]
|
||||
|
||||
# Check which indexes already exist
|
||||
existing_index_names = {idx.get("name", "") for idx in existing_indexes}
|
||||
|
||||
for index_info in pagination_indexes:
|
||||
index_name = index_info["name"]
|
||||
if index_name not in existing_index_names:
|
||||
await self._data.create_index(index_info["keys"], name=index_name)
|
||||
logger.info(
|
||||
f"Created pagination index '{index_name}' for collection {self._collection_name}"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"Pagination index '{index_name}' already exists for collection {self._collection_name}"
|
||||
)
|
||||
|
||||
except PyMongoError as e:
|
||||
logger.error(
|
||||
f"Error creating pagination indexes for {self._collection_name}: {e}"
|
||||
)
|
||||
|
||||
async def get_docs_paginated(
|
||||
self,
|
||||
status_filter: DocStatus | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
sort_field: str = "updated_at",
|
||||
sort_direction: str = "desc",
|
||||
) -> tuple[list[tuple[str, DocProcessingStatus]], int]:
|
||||
"""Get documents with pagination support
|
||||
|
||||
Args:
|
||||
status_filter: Filter by document status, None for all statuses
|
||||
page: Page number (1-based)
|
||||
page_size: Number of documents per page (10-200)
|
||||
sort_field: Field to sort by ('created_at', 'updated_at', '_id')
|
||||
sort_direction: Sort direction ('asc' or 'desc')
|
||||
|
||||
Returns:
|
||||
Tuple of (list of (doc_id, DocProcessingStatus) tuples, total_count)
|
||||
"""
|
||||
# Validate parameters
|
||||
if page < 1:
|
||||
page = 1
|
||||
if page_size < 10:
|
||||
page_size = 10
|
||||
elif page_size > 200:
|
||||
page_size = 200
|
||||
|
||||
if sort_field not in ["created_at", "updated_at", "_id", "file_path"]:
|
||||
sort_field = "updated_at"
|
||||
|
||||
if sort_direction.lower() not in ["asc", "desc"]:
|
||||
sort_direction = "desc"
|
||||
|
||||
# Build query filter
|
||||
query_filter = {}
|
||||
if status_filter is not None:
|
||||
query_filter["status"] = status_filter.value
|
||||
|
||||
# Get total count
|
||||
total_count = await self._data.count_documents(query_filter)
|
||||
|
||||
# Calculate skip value
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
# Build sort criteria
|
||||
sort_direction_value = 1 if sort_direction.lower() == "asc" else -1
|
||||
sort_criteria = [(sort_field, sort_direction_value)]
|
||||
|
||||
# Query for paginated data
|
||||
cursor = (
|
||||
self._data.find(query_filter)
|
||||
.sort(sort_criteria)
|
||||
.skip(skip)
|
||||
.limit(page_size)
|
||||
)
|
||||
result = await cursor.to_list(length=page_size)
|
||||
|
||||
# Convert to (doc_id, DocProcessingStatus) tuples
|
||||
documents = []
|
||||
for doc in result:
|
||||
try:
|
||||
doc_id = doc["_id"]
|
||||
|
||||
# Make a copy of the data to avoid modifying the original
|
||||
data = doc.copy()
|
||||
# Remove deprecated content field if it exists
|
||||
data.pop("content", None)
|
||||
# Remove MongoDB _id field if it exists
|
||||
data.pop("_id", None)
|
||||
# If file_path is not in data, use document id as file path
|
||||
if "file_path" not in data:
|
||||
data["file_path"] = "no-file-path"
|
||||
# Ensure new fields exist with default values
|
||||
if "metadata" not in data:
|
||||
data["metadata"] = {}
|
||||
if "error_msg" not in data:
|
||||
data["error_msg"] = None
|
||||
|
||||
doc_status = DocProcessingStatus(**data)
|
||||
documents.append((doc_id, doc_status))
|
||||
except KeyError as e:
|
||||
logger.error(f"Missing required field for document {doc['_id']}: {e}")
|
||||
continue
|
||||
|
||||
return documents, total_count
|
||||
|
||||
async def get_all_status_counts(self) -> dict[str, int]:
|
||||
"""Get counts of documents in each status for all documents
|
||||
|
||||
Returns:
|
||||
Dictionary mapping status names to counts, including 'all' field
|
||||
"""
|
||||
pipeline = [{"$group": {"_id": "$status", "count": {"$sum": 1}}}]
|
||||
cursor = await self._data.aggregate(pipeline, allowDiskUse=True)
|
||||
result = await cursor.to_list()
|
||||
|
||||
counts = {}
|
||||
total_count = 0
|
||||
for doc in result:
|
||||
counts[doc["_id"]] = doc["count"]
|
||||
total_count += doc["count"]
|
||||
|
||||
# Add 'all' field with total count
|
||||
counts["all"] = total_count
|
||||
|
||||
return counts
|
||||
|
||||
|
||||
@final
|
||||
@dataclass
|
||||
|
|
|
@ -9,12 +9,8 @@ from lightrag.utils import (
|
|||
logger,
|
||||
compute_mdhash_id,
|
||||
)
|
||||
import pipmaster as pm
|
||||
|
||||
from lightrag.base import BaseVectorStorage
|
||||
|
||||
if not pm.is_installed("nano-vectordb"):
|
||||
pm.install("nano-vectordb")
|
||||
|
||||
from nano_vectordb import NanoVectorDB
|
||||
from .shared_storage import (
|
||||
get_storage_lock,
|
||||
|
|
|
@ -6,12 +6,6 @@ from lightrag.types import KnowledgeGraph, KnowledgeGraphNode, KnowledgeGraphEdg
|
|||
from lightrag.utils import logger
|
||||
from lightrag.base import BaseGraphStorage
|
||||
from lightrag.constants import GRAPH_FIELD_SEP
|
||||
|
||||
import pipmaster as pm
|
||||
|
||||
if not pm.is_installed("networkx"):
|
||||
pm.install("networkx")
|
||||
|
||||
import networkx as nx
|
||||
from .shared_storage import (
|
||||
get_storage_lock,
|
||||
|
|
|
@ -80,7 +80,7 @@ class PostgreSQLDB:
|
|||
if ssl_mode in ["disable", "allow", "prefer", "require"]:
|
||||
if ssl_mode == "disable":
|
||||
return None
|
||||
elif ssl_mode in ["require", "prefer"]:
|
||||
elif ssl_mode in ["require", "prefer", "allow"]:
|
||||
# Return None for simple SSL requirement, handled in initdb
|
||||
return None
|
||||
|
||||
|
@ -567,6 +567,119 @@ class PostgreSQLDB:
|
|||
f"Failed to add llm_cache_list column to LIGHTRAG_DOC_CHUNKS: {e}"
|
||||
)
|
||||
|
||||
async def _migrate_doc_status_add_track_id(self):
|
||||
"""Add track_id column to LIGHTRAG_DOC_STATUS table if it doesn't exist and create index"""
|
||||
try:
|
||||
# Check if track_id column exists
|
||||
check_column_sql = """
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'lightrag_doc_status'
|
||||
AND column_name = 'track_id'
|
||||
"""
|
||||
|
||||
column_info = await self.query(check_column_sql)
|
||||
if not column_info:
|
||||
logger.info("Adding track_id column to LIGHTRAG_DOC_STATUS table")
|
||||
add_column_sql = """
|
||||
ALTER TABLE LIGHTRAG_DOC_STATUS
|
||||
ADD COLUMN track_id VARCHAR(255) NULL
|
||||
"""
|
||||
await self.execute(add_column_sql)
|
||||
logger.info(
|
||||
"Successfully added track_id column to LIGHTRAG_DOC_STATUS table"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"track_id column already exists in LIGHTRAG_DOC_STATUS table"
|
||||
)
|
||||
|
||||
# Check if track_id index exists
|
||||
check_index_sql = """
|
||||
SELECT indexname
|
||||
FROM pg_indexes
|
||||
WHERE tablename = 'lightrag_doc_status'
|
||||
AND indexname = 'idx_lightrag_doc_status_track_id'
|
||||
"""
|
||||
|
||||
index_info = await self.query(check_index_sql)
|
||||
if not index_info:
|
||||
logger.info(
|
||||
"Creating index on track_id column for LIGHTRAG_DOC_STATUS table"
|
||||
)
|
||||
create_index_sql = """
|
||||
CREATE INDEX idx_lightrag_doc_status_track_id ON LIGHTRAG_DOC_STATUS (track_id)
|
||||
"""
|
||||
await self.execute(create_index_sql)
|
||||
logger.info(
|
||||
"Successfully created index on track_id column for LIGHTRAG_DOC_STATUS table"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"Index on track_id column already exists for LIGHTRAG_DOC_STATUS table"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to add track_id column or index to LIGHTRAG_DOC_STATUS: {e}"
|
||||
)
|
||||
|
||||
async def _migrate_doc_status_add_metadata_error_msg(self):
|
||||
"""Add metadata and error_msg columns to LIGHTRAG_DOC_STATUS table if they don't exist"""
|
||||
try:
|
||||
# Check if metadata column exists
|
||||
check_metadata_sql = """
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'lightrag_doc_status'
|
||||
AND column_name = 'metadata'
|
||||
"""
|
||||
|
||||
metadata_info = await self.query(check_metadata_sql)
|
||||
if not metadata_info:
|
||||
logger.info("Adding metadata column to LIGHTRAG_DOC_STATUS table")
|
||||
add_metadata_sql = """
|
||||
ALTER TABLE LIGHTRAG_DOC_STATUS
|
||||
ADD COLUMN metadata JSONB NULL DEFAULT '{}'::jsonb
|
||||
"""
|
||||
await self.execute(add_metadata_sql)
|
||||
logger.info(
|
||||
"Successfully added metadata column to LIGHTRAG_DOC_STATUS table"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"metadata column already exists in LIGHTRAG_DOC_STATUS table"
|
||||
)
|
||||
|
||||
# Check if error_msg column exists
|
||||
check_error_msg_sql = """
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'lightrag_doc_status'
|
||||
AND column_name = 'error_msg'
|
||||
"""
|
||||
|
||||
error_msg_info = await self.query(check_error_msg_sql)
|
||||
if not error_msg_info:
|
||||
logger.info("Adding error_msg column to LIGHTRAG_DOC_STATUS table")
|
||||
add_error_msg_sql = """
|
||||
ALTER TABLE LIGHTRAG_DOC_STATUS
|
||||
ADD COLUMN error_msg TEXT NULL
|
||||
"""
|
||||
await self.execute(add_error_msg_sql)
|
||||
logger.info(
|
||||
"Successfully added error_msg column to LIGHTRAG_DOC_STATUS table"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"error_msg column already exists in LIGHTRAG_DOC_STATUS table"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to add metadata/error_msg columns to LIGHTRAG_DOC_STATUS: {e}"
|
||||
)
|
||||
|
||||
async def _migrate_field_lengths(self):
|
||||
"""Migrate database field lengths: entity_name, source_id, target_id, and file_path"""
|
||||
# Define the field changes needed
|
||||
|
@ -785,6 +898,85 @@ class PostgreSQLDB:
|
|||
except Exception as e:
|
||||
logger.error(f"PostgreSQL, Failed to migrate field lengths: {e}")
|
||||
|
||||
# Migrate doc status to add track_id field if needed
|
||||
try:
|
||||
await self._migrate_doc_status_add_track_id()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"PostgreSQL, Failed to migrate doc status track_id field: {e}"
|
||||
)
|
||||
|
||||
# Migrate doc status to add metadata and error_msg fields if needed
|
||||
try:
|
||||
await self._migrate_doc_status_add_metadata_error_msg()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"PostgreSQL, Failed to migrate doc status metadata/error_msg fields: {e}"
|
||||
)
|
||||
|
||||
# Create pagination optimization indexes for LIGHTRAG_DOC_STATUS
|
||||
try:
|
||||
await self._create_pagination_indexes()
|
||||
except Exception as e:
|
||||
logger.error(f"PostgreSQL, Failed to create pagination indexes: {e}")
|
||||
|
||||
async def _create_pagination_indexes(self):
|
||||
"""Create indexes to optimize pagination queries for LIGHTRAG_DOC_STATUS"""
|
||||
indexes = [
|
||||
{
|
||||
"name": "idx_lightrag_doc_status_workspace_status_updated_at",
|
||||
"sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_lightrag_doc_status_workspace_status_updated_at ON LIGHTRAG_DOC_STATUS (workspace, status, updated_at DESC)",
|
||||
"description": "Composite index for workspace + status + updated_at pagination",
|
||||
},
|
||||
{
|
||||
"name": "idx_lightrag_doc_status_workspace_status_created_at",
|
||||
"sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_lightrag_doc_status_workspace_status_created_at ON LIGHTRAG_DOC_STATUS (workspace, status, created_at DESC)",
|
||||
"description": "Composite index for workspace + status + created_at pagination",
|
||||
},
|
||||
{
|
||||
"name": "idx_lightrag_doc_status_workspace_updated_at",
|
||||
"sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_lightrag_doc_status_workspace_updated_at ON LIGHTRAG_DOC_STATUS (workspace, updated_at DESC)",
|
||||
"description": "Index for workspace + updated_at pagination (all statuses)",
|
||||
},
|
||||
{
|
||||
"name": "idx_lightrag_doc_status_workspace_created_at",
|
||||
"sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_lightrag_doc_status_workspace_created_at ON LIGHTRAG_DOC_STATUS (workspace, created_at DESC)",
|
||||
"description": "Index for workspace + created_at pagination (all statuses)",
|
||||
},
|
||||
{
|
||||
"name": "idx_lightrag_doc_status_workspace_id",
|
||||
"sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_lightrag_doc_status_workspace_id ON LIGHTRAG_DOC_STATUS (workspace, id)",
|
||||
"description": "Index for workspace + id sorting",
|
||||
},
|
||||
{
|
||||
"name": "idx_lightrag_doc_status_workspace_file_path",
|
||||
"sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_lightrag_doc_status_workspace_file_path ON LIGHTRAG_DOC_STATUS (workspace, file_path)",
|
||||
"description": "Index for workspace + file_path sorting",
|
||||
},
|
||||
]
|
||||
|
||||
for index in indexes:
|
||||
try:
|
||||
# Check if index already exists
|
||||
check_sql = """
|
||||
SELECT indexname
|
||||
FROM pg_indexes
|
||||
WHERE tablename = 'lightrag_doc_status'
|
||||
AND indexname = $1
|
||||
"""
|
||||
|
||||
existing = await self.query(check_sql, {"indexname": index["name"]})
|
||||
|
||||
if not existing:
|
||||
logger.info(f"Creating pagination index: {index['description']}")
|
||||
await self.execute(index["sql"])
|
||||
logger.info(f"Successfully created index: {index['name']}")
|
||||
else:
|
||||
logger.debug(f"Index already exists: {index['name']}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to create index {index['name']}: {e}")
|
||||
|
||||
async def query(
|
||||
self,
|
||||
sql: str,
|
||||
|
@ -1668,12 +1860,19 @@ class PGDocStatusStorage(DocStatusStorage):
|
|||
except json.JSONDecodeError:
|
||||
chunks_list = []
|
||||
|
||||
# Parse metadata JSON string back to dict
|
||||
metadata = result[0].get("metadata", {})
|
||||
if isinstance(metadata, str):
|
||||
try:
|
||||
metadata = json.loads(metadata)
|
||||
except json.JSONDecodeError:
|
||||
metadata = {}
|
||||
|
||||
# Convert datetime objects to ISO format strings with timezone info
|
||||
created_at = self._format_datetime_with_timezone(result[0]["created_at"])
|
||||
updated_at = self._format_datetime_with_timezone(result[0]["updated_at"])
|
||||
|
||||
return dict(
|
||||
content=result[0]["content"],
|
||||
content_length=result[0]["content_length"],
|
||||
content_summary=result[0]["content_summary"],
|
||||
status=result[0]["status"],
|
||||
|
@ -1682,6 +1881,9 @@ class PGDocStatusStorage(DocStatusStorage):
|
|||
updated_at=updated_at,
|
||||
file_path=result[0]["file_path"],
|
||||
chunks_list=chunks_list,
|
||||
metadata=metadata,
|
||||
error_msg=result[0].get("error_msg"),
|
||||
track_id=result[0].get("track_id"),
|
||||
)
|
||||
|
||||
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||
|
@ -1707,13 +1909,20 @@ class PGDocStatusStorage(DocStatusStorage):
|
|||
except json.JSONDecodeError:
|
||||
chunks_list = []
|
||||
|
||||
# Parse metadata JSON string back to dict
|
||||
metadata = row.get("metadata", {})
|
||||
if isinstance(metadata, str):
|
||||
try:
|
||||
metadata = json.loads(metadata)
|
||||
except json.JSONDecodeError:
|
||||
metadata = {}
|
||||
|
||||
# Convert datetime objects to ISO format strings with timezone info
|
||||
created_at = self._format_datetime_with_timezone(row["created_at"])
|
||||
updated_at = self._format_datetime_with_timezone(row["updated_at"])
|
||||
|
||||
processed_results.append(
|
||||
{
|
||||
"content": row["content"],
|
||||
"content_length": row["content_length"],
|
||||
"content_summary": row["content_summary"],
|
||||
"status": row["status"],
|
||||
|
@ -1722,6 +1931,9 @@ class PGDocStatusStorage(DocStatusStorage):
|
|||
"updated_at": updated_at,
|
||||
"file_path": row["file_path"],
|
||||
"chunks_list": chunks_list,
|
||||
"metadata": metadata,
|
||||
"error_msg": row.get("error_msg"),
|
||||
"track_id": row.get("track_id"),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -1757,12 +1969,189 @@ class PGDocStatusStorage(DocStatusStorage):
|
|||
except json.JSONDecodeError:
|
||||
chunks_list = []
|
||||
|
||||
# Parse metadata JSON string back to dict
|
||||
metadata = element.get("metadata", {})
|
||||
if isinstance(metadata, str):
|
||||
try:
|
||||
metadata = json.loads(metadata)
|
||||
except json.JSONDecodeError:
|
||||
metadata = {}
|
||||
# Ensure metadata is a dict
|
||||
if not isinstance(metadata, dict):
|
||||
metadata = {}
|
||||
|
||||
# Safe handling for file_path
|
||||
file_path = element.get("file_path")
|
||||
if file_path is None:
|
||||
file_path = "no-file-path"
|
||||
|
||||
# Convert datetime objects to ISO format strings with timezone info
|
||||
created_at = self._format_datetime_with_timezone(element["created_at"])
|
||||
updated_at = self._format_datetime_with_timezone(element["updated_at"])
|
||||
|
||||
docs_by_status[element["id"]] = DocProcessingStatus(
|
||||
content=element["content"],
|
||||
content_summary=element["content_summary"],
|
||||
content_length=element["content_length"],
|
||||
status=element["status"],
|
||||
created_at=created_at,
|
||||
updated_at=updated_at,
|
||||
chunks_count=element["chunks_count"],
|
||||
file_path=file_path,
|
||||
chunks_list=chunks_list,
|
||||
metadata=metadata,
|
||||
error_msg=element.get("error_msg"),
|
||||
track_id=element.get("track_id"),
|
||||
)
|
||||
|
||||
return docs_by_status
|
||||
|
||||
async def get_docs_by_track_id(
|
||||
self, track_id: str
|
||||
) -> dict[str, DocProcessingStatus]:
|
||||
"""Get all documents with a specific track_id"""
|
||||
sql = "select * from LIGHTRAG_DOC_STATUS where workspace=$1 and track_id=$2"
|
||||
params = {"workspace": self.db.workspace, "track_id": track_id}
|
||||
result = await self.db.query(sql, params, True)
|
||||
|
||||
docs_by_track_id = {}
|
||||
for element in result:
|
||||
# Parse chunks_list JSON string back to list
|
||||
chunks_list = element.get("chunks_list", [])
|
||||
if isinstance(chunks_list, str):
|
||||
try:
|
||||
chunks_list = json.loads(chunks_list)
|
||||
except json.JSONDecodeError:
|
||||
chunks_list = []
|
||||
|
||||
# Parse metadata JSON string back to dict
|
||||
metadata = element.get("metadata", {})
|
||||
if isinstance(metadata, str):
|
||||
try:
|
||||
metadata = json.loads(metadata)
|
||||
except json.JSONDecodeError:
|
||||
metadata = {}
|
||||
# Ensure metadata is a dict
|
||||
if not isinstance(metadata, dict):
|
||||
metadata = {}
|
||||
|
||||
# Safe handling for file_path
|
||||
file_path = element.get("file_path")
|
||||
if file_path is None:
|
||||
file_path = "no-file-path"
|
||||
|
||||
# Convert datetime objects to ISO format strings with timezone info
|
||||
created_at = self._format_datetime_with_timezone(element["created_at"])
|
||||
updated_at = self._format_datetime_with_timezone(element["updated_at"])
|
||||
|
||||
docs_by_track_id[element["id"]] = DocProcessingStatus(
|
||||
content_summary=element["content_summary"],
|
||||
content_length=element["content_length"],
|
||||
status=element["status"],
|
||||
created_at=created_at,
|
||||
updated_at=updated_at,
|
||||
chunks_count=element["chunks_count"],
|
||||
file_path=file_path,
|
||||
chunks_list=chunks_list,
|
||||
track_id=element.get("track_id"),
|
||||
metadata=metadata,
|
||||
error_msg=element.get("error_msg"),
|
||||
)
|
||||
|
||||
return docs_by_track_id
|
||||
|
||||
async def get_docs_paginated(
|
||||
self,
|
||||
status_filter: DocStatus | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
sort_field: str = "updated_at",
|
||||
sort_direction: str = "desc",
|
||||
) -> tuple[list[tuple[str, DocProcessingStatus]], int]:
|
||||
"""Get documents with pagination support
|
||||
|
||||
Args:
|
||||
status_filter: Filter by document status, None for all statuses
|
||||
page: Page number (1-based)
|
||||
page_size: Number of documents per page (10-200)
|
||||
sort_field: Field to sort by ('created_at', 'updated_at', 'id')
|
||||
sort_direction: Sort direction ('asc' or 'desc')
|
||||
|
||||
Returns:
|
||||
Tuple of (list of (doc_id, DocProcessingStatus) tuples, total_count)
|
||||
"""
|
||||
# Validate parameters
|
||||
if page < 1:
|
||||
page = 1
|
||||
if page_size < 10:
|
||||
page_size = 10
|
||||
elif page_size > 200:
|
||||
page_size = 200
|
||||
|
||||
if sort_field not in ["created_at", "updated_at", "id", "file_path"]:
|
||||
sort_field = "updated_at"
|
||||
|
||||
if sort_direction.lower() not in ["asc", "desc"]:
|
||||
sort_direction = "desc"
|
||||
|
||||
# Calculate offset
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
# Build WHERE clause
|
||||
where_clause = "WHERE workspace=$1"
|
||||
params = {"workspace": self.db.workspace}
|
||||
param_count = 1
|
||||
|
||||
if status_filter is not None:
|
||||
param_count += 1
|
||||
where_clause += f" AND status=${param_count}"
|
||||
params["status"] = status_filter.value
|
||||
|
||||
# Build ORDER BY clause
|
||||
order_clause = f"ORDER BY {sort_field} {sort_direction.upper()}"
|
||||
|
||||
# Query for total count
|
||||
count_sql = f"SELECT COUNT(*) as total FROM LIGHTRAG_DOC_STATUS {where_clause}"
|
||||
count_result = await self.db.query(count_sql, params)
|
||||
total_count = count_result["total"] if count_result else 0
|
||||
|
||||
# Query for paginated data
|
||||
data_sql = f"""
|
||||
SELECT * FROM LIGHTRAG_DOC_STATUS
|
||||
{where_clause}
|
||||
{order_clause}
|
||||
LIMIT ${param_count + 1} OFFSET ${param_count + 2}
|
||||
"""
|
||||
params["limit"] = page_size
|
||||
params["offset"] = offset
|
||||
|
||||
result = await self.db.query(data_sql, params, True)
|
||||
|
||||
# Convert to (doc_id, DocProcessingStatus) tuples
|
||||
documents = []
|
||||
for element in result:
|
||||
doc_id = element["id"]
|
||||
|
||||
# Parse chunks_list JSON string back to list
|
||||
chunks_list = element.get("chunks_list", [])
|
||||
if isinstance(chunks_list, str):
|
||||
try:
|
||||
chunks_list = json.loads(chunks_list)
|
||||
except json.JSONDecodeError:
|
||||
chunks_list = []
|
||||
|
||||
# Parse metadata JSON string back to dict
|
||||
metadata = element.get("metadata", {})
|
||||
if isinstance(metadata, str):
|
||||
try:
|
||||
metadata = json.loads(metadata)
|
||||
except json.JSONDecodeError:
|
||||
metadata = {}
|
||||
|
||||
# Convert datetime objects to ISO format strings with timezone info
|
||||
created_at = self._format_datetime_with_timezone(element["created_at"])
|
||||
updated_at = self._format_datetime_with_timezone(element["updated_at"])
|
||||
|
||||
doc_status = DocProcessingStatus(
|
||||
content_summary=element["content_summary"],
|
||||
content_length=element["content_length"],
|
||||
status=element["status"],
|
||||
|
@ -1771,9 +2160,39 @@ class PGDocStatusStorage(DocStatusStorage):
|
|||
chunks_count=element["chunks_count"],
|
||||
file_path=element["file_path"],
|
||||
chunks_list=chunks_list,
|
||||
track_id=element.get("track_id"),
|
||||
metadata=metadata,
|
||||
error_msg=element.get("error_msg"),
|
||||
)
|
||||
documents.append((doc_id, doc_status))
|
||||
|
||||
return docs_by_status
|
||||
return documents, total_count
|
||||
|
||||
async def get_all_status_counts(self) -> dict[str, int]:
|
||||
"""Get counts of documents in each status for all documents
|
||||
|
||||
Returns:
|
||||
Dictionary mapping status names to counts, including 'all' field
|
||||
"""
|
||||
sql = """
|
||||
SELECT status, COUNT(*) as count
|
||||
FROM LIGHTRAG_DOC_STATUS
|
||||
WHERE workspace=$1
|
||||
GROUP BY status
|
||||
"""
|
||||
params = {"workspace": self.db.workspace}
|
||||
result = await self.db.query(sql, params, True)
|
||||
|
||||
counts = {}
|
||||
total_count = 0
|
||||
for row in result:
|
||||
counts[row["status"]] = row["count"]
|
||||
total_count += row["count"]
|
||||
|
||||
# Add 'all' field with total count
|
||||
counts["all"] = total_count
|
||||
|
||||
return counts
|
||||
|
||||
async def index_done_callback(self) -> None:
|
||||
# PG handles persistence automatically
|
||||
|
@ -1843,18 +2262,20 @@ class PGDocStatusStorage(DocStatusStorage):
|
|||
logger.warning(f"Unable to parse datetime string: {dt_str}")
|
||||
return None
|
||||
|
||||
# Modified SQL to include created_at, updated_at, and chunks_list in both INSERT and UPDATE operations
|
||||
# Modified SQL to include created_at, updated_at, chunks_list, track_id, metadata, and error_msg in both INSERT and UPDATE operations
|
||||
# All fields are updated from the input data in both INSERT and UPDATE cases
|
||||
sql = """insert into LIGHTRAG_DOC_STATUS(workspace,id,content,content_summary,content_length,chunks_count,status,file_path,chunks_list,created_at,updated_at)
|
||||
values($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
|
||||
sql = """insert into LIGHTRAG_DOC_STATUS(workspace,id,content_summary,content_length,chunks_count,status,file_path,chunks_list,track_id,metadata,error_msg,created_at,updated_at)
|
||||
values($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
|
||||
on conflict(id,workspace) do update set
|
||||
content = EXCLUDED.content,
|
||||
content_summary = EXCLUDED.content_summary,
|
||||
content_length = EXCLUDED.content_length,
|
||||
chunks_count = EXCLUDED.chunks_count,
|
||||
status = EXCLUDED.status,
|
||||
file_path = EXCLUDED.file_path,
|
||||
chunks_list = EXCLUDED.chunks_list,
|
||||
track_id = EXCLUDED.track_id,
|
||||
metadata = EXCLUDED.metadata,
|
||||
error_msg = EXCLUDED.error_msg,
|
||||
created_at = EXCLUDED.created_at,
|
||||
updated_at = EXCLUDED.updated_at"""
|
||||
for k, v in data.items():
|
||||
|
@ -1862,19 +2283,23 @@ class PGDocStatusStorage(DocStatusStorage):
|
|||
created_at = parse_datetime(v.get("created_at"))
|
||||
updated_at = parse_datetime(v.get("updated_at"))
|
||||
|
||||
# chunks_count and chunks_list are optional
|
||||
# chunks_count, chunks_list, track_id, metadata, and error_msg are optional
|
||||
await self.db.execute(
|
||||
sql,
|
||||
{
|
||||
"workspace": self.db.workspace,
|
||||
"id": k,
|
||||
"content": v["content"],
|
||||
"content_summary": v["content_summary"],
|
||||
"content_length": v["content_length"],
|
||||
"chunks_count": v["chunks_count"] if "chunks_count" in v else -1,
|
||||
"status": v["status"],
|
||||
"file_path": v["file_path"],
|
||||
"chunks_list": json.dumps(v.get("chunks_list", [])),
|
||||
"track_id": v.get("track_id"), # Add track_id support
|
||||
"metadata": json.dumps(
|
||||
v.get("metadata", {})
|
||||
), # Add metadata support
|
||||
"error_msg": v.get("error_msg"), # Add error_msg support
|
||||
"created_at": created_at, # Use the converted datetime object
|
||||
"updated_at": updated_at, # Use the converted datetime object
|
||||
},
|
||||
|
@ -2864,7 +3289,7 @@ class PGGraphStorage(BaseGraphStorage):
|
|||
query = f"""
|
||||
SELECT * FROM cypher('{self.graph_name}', $$
|
||||
UNWIND {chunk_ids_str} AS chunk_id
|
||||
MATCH (a:base)-[r]-(b:base)
|
||||
MATCH ()-[r]-()
|
||||
WHERE r.source_id IS NOT NULL AND chunk_id IN split(r.source_id, '{GRAPH_FIELD_SEP}')
|
||||
RETURN DISTINCT r, startNode(r) AS source, endNode(r) AS target
|
||||
$$) AS (edge agtype, source agtype, target agtype);
|
||||
|
@ -3368,13 +3793,15 @@ TABLES = {
|
|||
"ddl": """CREATE TABLE LIGHTRAG_DOC_STATUS (
|
||||
workspace varchar(255) NOT NULL,
|
||||
id varchar(255) NOT NULL,
|
||||
content TEXT NULL,
|
||||
content_summary varchar(255) NULL,
|
||||
content_length int4 NULL,
|
||||
chunks_count int4 NULL,
|
||||
status varchar(64) NULL,
|
||||
file_path TEXT NULL,
|
||||
chunks_list JSONB NULL DEFAULT '[]'::jsonb,
|
||||
track_id varchar(255) NULL,
|
||||
metadata JSONB NULL DEFAULT '{}'::jsonb,
|
||||
error_msg TEXT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT LIGHTRAG_DOC_STATUS_PK PRIMARY KEY (workspace, id)
|
||||
|
|
|
@ -786,15 +786,16 @@ class RedisDocStatusStorage(DocStatusStorage):
|
|||
|
||||
# Make a copy of the data to avoid modifying the original
|
||||
data = doc_data.copy()
|
||||
# If content is missing, use content_summary as content
|
||||
if (
|
||||
"content" not in data
|
||||
and "content_summary" in data
|
||||
):
|
||||
data["content"] = data["content_summary"]
|
||||
# Remove deprecated content field if it exists
|
||||
data.pop("content", None)
|
||||
# If file_path is not in data, use document id as file path
|
||||
if "file_path" not in data:
|
||||
data["file_path"] = "no-file-path"
|
||||
# Ensure new fields exist with default values
|
||||
if "metadata" not in data:
|
||||
data["metadata"] = {}
|
||||
if "error_msg" not in data:
|
||||
data["error_msg"] = None
|
||||
|
||||
result[doc_id] = DocProcessingStatus(**data)
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
|
@ -810,6 +811,62 @@ class RedisDocStatusStorage(DocStatusStorage):
|
|||
|
||||
return result
|
||||
|
||||
async def get_docs_by_track_id(
|
||||
self, track_id: str
|
||||
) -> dict[str, DocProcessingStatus]:
|
||||
"""Get all documents with a specific track_id"""
|
||||
result = {}
|
||||
async with self._get_redis_connection() as redis:
|
||||
try:
|
||||
# Use SCAN to iterate through all keys in the namespace
|
||||
cursor = 0
|
||||
while True:
|
||||
cursor, keys = await redis.scan(
|
||||
cursor, match=f"{self.namespace}:*", count=1000
|
||||
)
|
||||
if keys:
|
||||
# Get all values in batch
|
||||
pipe = redis.pipeline()
|
||||
for key in keys:
|
||||
pipe.get(key)
|
||||
values = await pipe.execute()
|
||||
|
||||
# Filter by track_id and create DocProcessingStatus objects
|
||||
for key, value in zip(keys, values):
|
||||
if value:
|
||||
try:
|
||||
doc_data = json.loads(value)
|
||||
if doc_data.get("track_id") == track_id:
|
||||
# Extract document ID from key
|
||||
doc_id = key.split(":", 1)[1]
|
||||
|
||||
# Make a copy of the data to avoid modifying the original
|
||||
data = doc_data.copy()
|
||||
# Remove deprecated content field if it exists
|
||||
data.pop("content", None)
|
||||
# If file_path is not in data, use document id as file path
|
||||
if "file_path" not in data:
|
||||
data["file_path"] = "no-file-path"
|
||||
# Ensure new fields exist with default values
|
||||
if "metadata" not in data:
|
||||
data["metadata"] = {}
|
||||
if "error_msg" not in data:
|
||||
data["error_msg"] = None
|
||||
|
||||
result[doc_id] = DocProcessingStatus(**data)
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
logger.error(
|
||||
f"Error processing document {key}: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
if cursor == 0:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting docs by track_id: {e}")
|
||||
|
||||
return result
|
||||
|
||||
async def index_done_callback(self) -> None:
|
||||
"""Redis handles persistence automatically"""
|
||||
pass
|
||||
|
@ -862,6 +919,138 @@ class RedisDocStatusStorage(DocStatusStorage):
|
|||
f"Deleted {deleted_count} of {len(doc_ids)} doc status entries from {self.namespace}"
|
||||
)
|
||||
|
||||
async def get_docs_paginated(
|
||||
self,
|
||||
status_filter: DocStatus | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
sort_field: str = "updated_at",
|
||||
sort_direction: str = "desc",
|
||||
) -> tuple[list[tuple[str, DocProcessingStatus]], int]:
|
||||
"""Get documents with pagination support
|
||||
|
||||
Args:
|
||||
status_filter: Filter by document status, None for all statuses
|
||||
page: Page number (1-based)
|
||||
page_size: Number of documents per page (10-200)
|
||||
sort_field: Field to sort by ('created_at', 'updated_at', 'id')
|
||||
sort_direction: Sort direction ('asc' or 'desc')
|
||||
|
||||
Returns:
|
||||
Tuple of (list of (doc_id, DocProcessingStatus) tuples, total_count)
|
||||
"""
|
||||
# Validate parameters
|
||||
if page < 1:
|
||||
page = 1
|
||||
if page_size < 10:
|
||||
page_size = 10
|
||||
elif page_size > 200:
|
||||
page_size = 200
|
||||
|
||||
if sort_field not in ["created_at", "updated_at", "id", "file_path"]:
|
||||
sort_field = "updated_at"
|
||||
|
||||
if sort_direction.lower() not in ["asc", "desc"]:
|
||||
sort_direction = "desc"
|
||||
|
||||
# For Redis, we need to load all data and sort/filter in memory
|
||||
all_docs = []
|
||||
total_count = 0
|
||||
|
||||
async with self._get_redis_connection() as redis:
|
||||
try:
|
||||
# Use SCAN to iterate through all keys in the namespace
|
||||
cursor = 0
|
||||
while True:
|
||||
cursor, keys = await redis.scan(
|
||||
cursor, match=f"{self.namespace}:*", count=1000
|
||||
)
|
||||
if keys:
|
||||
# Get all values in batch
|
||||
pipe = redis.pipeline()
|
||||
for key in keys:
|
||||
pipe.get(key)
|
||||
values = await pipe.execute()
|
||||
|
||||
# Process documents
|
||||
for key, value in zip(keys, values):
|
||||
if value:
|
||||
try:
|
||||
doc_data = json.loads(value)
|
||||
|
||||
# Apply status filter
|
||||
if (
|
||||
status_filter is not None
|
||||
and doc_data.get("status")
|
||||
!= status_filter.value
|
||||
):
|
||||
continue
|
||||
|
||||
# Extract document ID from key
|
||||
doc_id = key.split(":", 1)[1]
|
||||
|
||||
# Prepare document data
|
||||
data = doc_data.copy()
|
||||
data.pop("content", None)
|
||||
if "file_path" not in data:
|
||||
data["file_path"] = "no-file-path"
|
||||
if "metadata" not in data:
|
||||
data["metadata"] = {}
|
||||
if "error_msg" not in data:
|
||||
data["error_msg"] = None
|
||||
|
||||
# Calculate sort key for sorting (but don't add to data)
|
||||
if sort_field == "id":
|
||||
sort_key = doc_id
|
||||
else:
|
||||
sort_key = data.get(sort_field, "")
|
||||
|
||||
doc_status = DocProcessingStatus(**data)
|
||||
all_docs.append((doc_id, doc_status, sort_key))
|
||||
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
logger.error(
|
||||
f"Error processing document {key}: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
if cursor == 0:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting paginated docs: {e}")
|
||||
return [], 0
|
||||
|
||||
# Sort documents using the separate sort key
|
||||
reverse_sort = sort_direction.lower() == "desc"
|
||||
all_docs.sort(key=lambda x: x[2], reverse=reverse_sort)
|
||||
|
||||
# Remove sort key from tuples and keep only (doc_id, doc_status)
|
||||
all_docs = [(doc_id, doc_status) for doc_id, doc_status, _ in all_docs]
|
||||
|
||||
total_count = len(all_docs)
|
||||
|
||||
# Apply pagination
|
||||
start_idx = (page - 1) * page_size
|
||||
end_idx = start_idx + page_size
|
||||
paginated_docs = all_docs[start_idx:end_idx]
|
||||
|
||||
return paginated_docs, total_count
|
||||
|
||||
async def get_all_status_counts(self) -> dict[str, int]:
|
||||
"""Get counts of documents in each status for all documents
|
||||
|
||||
Returns:
|
||||
Dictionary mapping status names to counts, including 'all' field
|
||||
"""
|
||||
counts = await self.get_status_counts()
|
||||
|
||||
# Add 'all' field with total count
|
||||
total_count = sum(counts.values())
|
||||
counts["all"] = total_count
|
||||
|
||||
return counts
|
||||
|
||||
async def drop(self) -> dict[str, str]:
|
||||
"""Drop all document status data from storage and clean up resources"""
|
||||
try:
|
||||
|
|
|
@ -31,6 +31,11 @@ from lightrag.constants import (
|
|||
DEFAULT_MAX_TOTAL_TOKENS,
|
||||
DEFAULT_COSINE_THRESHOLD,
|
||||
DEFAULT_RELATED_CHUNK_NUMBER,
|
||||
DEFAULT_MIN_RERANK_SCORE,
|
||||
DEFAULT_SUMMARY_MAX_TOKENS,
|
||||
DEFAULT_MAX_ASYNC,
|
||||
DEFAULT_MAX_PARALLEL_INSERT,
|
||||
DEFAULT_MAX_GRAPH_NODES,
|
||||
)
|
||||
from lightrag.utils import get_env_value
|
||||
|
||||
|
@ -39,6 +44,7 @@ from lightrag.kg import (
|
|||
verify_storage_implementation,
|
||||
)
|
||||
|
||||
|
||||
from lightrag.kg.shared_storage import (
|
||||
get_namespace_data,
|
||||
get_pipeline_status_lock,
|
||||
|
@ -56,6 +62,7 @@ from .base import (
|
|||
StorageNameSpace,
|
||||
StoragesStatus,
|
||||
DeletionResult,
|
||||
OllamaServerInfos,
|
||||
)
|
||||
from .namespace import NameSpace
|
||||
from .operate import (
|
||||
|
@ -74,12 +81,12 @@ from .utils import (
|
|||
EmbeddingFunc,
|
||||
always_get_an_event_loop,
|
||||
compute_mdhash_id,
|
||||
convert_response_to_json,
|
||||
lazy_external_import,
|
||||
priority_limit_async_func_call,
|
||||
get_content_summary,
|
||||
clean_text,
|
||||
check_storage_env_vars,
|
||||
generate_track_id,
|
||||
logger,
|
||||
)
|
||||
from .types import KnowledgeGraph
|
||||
|
@ -269,10 +276,14 @@ class LightRAG:
|
|||
llm_model_name: str = field(default="gpt-4o-mini")
|
||||
"""Name of the LLM model used for generating responses."""
|
||||
|
||||
llm_model_max_token_size: int = field(default=int(os.getenv("MAX_TOKENS", 32000)))
|
||||
summary_max_tokens: int = field(
|
||||
default=int(os.getenv("MAX_TOKENS", DEFAULT_SUMMARY_MAX_TOKENS))
|
||||
)
|
||||
"""Maximum number of tokens allowed per LLM response."""
|
||||
|
||||
llm_model_max_async: int = field(default=int(os.getenv("MAX_ASYNC", 4)))
|
||||
llm_model_max_async: int = field(
|
||||
default=int(os.getenv("MAX_ASYNC", DEFAULT_MAX_ASYNC))
|
||||
)
|
||||
"""Maximum number of concurrent LLM calls."""
|
||||
|
||||
llm_model_kwargs: dict[str, Any] = field(default_factory=dict)
|
||||
|
@ -284,6 +295,11 @@ class LightRAG:
|
|||
rerank_model_func: Callable[..., object] | None = field(default=None)
|
||||
"""Function for reranking retrieved documents. All rerank configurations (model name, API keys, top_k, etc.) should be included in this function. Optional."""
|
||||
|
||||
min_rerank_score: float = field(
|
||||
default=get_env_value("MIN_RERANK_SCORE", DEFAULT_MIN_RERANK_SCORE, float)
|
||||
)
|
||||
"""Minimum rerank score threshold for filtering chunks after reranking."""
|
||||
|
||||
# Storage
|
||||
# ---
|
||||
|
||||
|
@ -299,10 +315,14 @@ class LightRAG:
|
|||
# Extensions
|
||||
# ---
|
||||
|
||||
max_parallel_insert: int = field(default=int(os.getenv("MAX_PARALLEL_INSERT", 2)))
|
||||
max_parallel_insert: int = field(
|
||||
default=int(os.getenv("MAX_PARALLEL_INSERT", DEFAULT_MAX_PARALLEL_INSERT))
|
||||
)
|
||||
"""Maximum number of parallel insert operations."""
|
||||
|
||||
max_graph_nodes: int = field(default=get_env_value("MAX_GRAPH_NODES", 1000, int))
|
||||
max_graph_nodes: int = field(
|
||||
default=get_env_value("MAX_GRAPH_NODES", DEFAULT_MAX_GRAPH_NODES, int)
|
||||
)
|
||||
"""Maximum number of graph nodes to return in knowledge graph queries."""
|
||||
|
||||
addon_params: dict[str, Any] = field(
|
||||
|
@ -320,19 +340,13 @@ class LightRAG:
|
|||
# Storages Management
|
||||
# ---
|
||||
|
||||
convert_response_to_json_func: Callable[[str], dict[str, Any]] = field(
|
||||
default_factory=lambda: convert_response_to_json
|
||||
)
|
||||
"""
|
||||
Custom function for converting LLM responses to JSON format.
|
||||
|
||||
The default function is :func:`.utils.convert_response_to_json`.
|
||||
"""
|
||||
|
||||
cosine_better_than_threshold: float = field(
|
||||
default=float(os.getenv("COSINE_THRESHOLD", 0.2))
|
||||
)
|
||||
|
||||
ollama_server_infos: Optional[OllamaServerInfos] = field(default=None)
|
||||
"""Configuration for Ollama server information."""
|
||||
|
||||
_storages_status: StoragesStatus = field(default=StoragesStatus.NOT_CREATED)
|
||||
|
||||
def __post_init__(self):
|
||||
|
@ -394,6 +408,10 @@ class LightRAG:
|
|||
else:
|
||||
self.tokenizer = TiktokenTokenizer()
|
||||
|
||||
# Initialize ollama_server_infos if not provided
|
||||
if self.ollama_server_infos is None:
|
||||
self.ollama_server_infos = OllamaServerInfos()
|
||||
|
||||
# Fix global_config now
|
||||
global_config = asdict(self)
|
||||
|
||||
|
@ -548,7 +566,7 @@ class LightRAG:
|
|||
await asyncio.gather(*tasks)
|
||||
|
||||
self._storages_status = StoragesStatus.INITIALIZED
|
||||
logger.debug("Initialized Storages")
|
||||
logger.debug("All storage types initialized")
|
||||
|
||||
async def finalize_storages(self):
|
||||
"""Asynchronously finalize the storages"""
|
||||
|
@ -605,9 +623,28 @@ class LightRAG:
|
|||
)
|
||||
|
||||
def _get_storage_class(self, storage_name: str) -> Callable[..., Any]:
|
||||
import_path = STORAGES[storage_name]
|
||||
storage_class = lazy_external_import(import_path, storage_name)
|
||||
return storage_class
|
||||
# Direct imports for default storage implementations
|
||||
if storage_name == "JsonKVStorage":
|
||||
from lightrag.kg.json_kv_impl import JsonKVStorage
|
||||
|
||||
return JsonKVStorage
|
||||
elif storage_name == "NanoVectorDBStorage":
|
||||
from lightrag.kg.nano_vector_db_impl import NanoVectorDBStorage
|
||||
|
||||
return NanoVectorDBStorage
|
||||
elif storage_name == "NetworkXStorage":
|
||||
from lightrag.kg.networkx_impl import NetworkXStorage
|
||||
|
||||
return NetworkXStorage
|
||||
elif storage_name == "JsonDocStatusStorage":
|
||||
from lightrag.kg.json_doc_status_impl import JsonDocStatusStorage
|
||||
|
||||
return JsonDocStatusStorage
|
||||
else:
|
||||
# Fallback to dynamic import for other storage implementations
|
||||
import_path = STORAGES[storage_name]
|
||||
storage_class = lazy_external_import(import_path, storage_name)
|
||||
return storage_class
|
||||
|
||||
def insert(
|
||||
self,
|
||||
|
@ -616,7 +653,8 @@ class LightRAG:
|
|||
split_by_character_only: bool = False,
|
||||
ids: str | list[str] | None = None,
|
||||
file_paths: str | list[str] | None = None,
|
||||
) -> None:
|
||||
track_id: str | None = None,
|
||||
) -> str:
|
||||
"""Sync Insert documents with checkpoint support
|
||||
|
||||
Args:
|
||||
|
@ -627,11 +665,20 @@ class LightRAG:
|
|||
split_by_character is None, this parameter is ignored.
|
||||
ids: single string of the document ID or list of unique document IDs, if not provided, MD5 hash IDs will be generated
|
||||
file_paths: single string of the file path or list of file paths, used for citation
|
||||
track_id: tracking ID for monitoring processing status, if not provided, will be generated
|
||||
|
||||
Returns:
|
||||
str: tracking ID for monitoring processing status
|
||||
"""
|
||||
loop = always_get_an_event_loop()
|
||||
loop.run_until_complete(
|
||||
return loop.run_until_complete(
|
||||
self.ainsert(
|
||||
input, split_by_character, split_by_character_only, ids, file_paths
|
||||
input,
|
||||
split_by_character,
|
||||
split_by_character_only,
|
||||
ids,
|
||||
file_paths,
|
||||
track_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -642,7 +689,8 @@ class LightRAG:
|
|||
split_by_character_only: bool = False,
|
||||
ids: str | list[str] | None = None,
|
||||
file_paths: str | list[str] | None = None,
|
||||
) -> None:
|
||||
track_id: str | None = None,
|
||||
) -> str:
|
||||
"""Async Insert documents with checkpoint support
|
||||
|
||||
Args:
|
||||
|
@ -653,12 +701,22 @@ class LightRAG:
|
|||
split_by_character is None, this parameter is ignored.
|
||||
ids: list of unique document IDs, if not provided, MD5 hash IDs will be generated
|
||||
file_paths: list of file paths corresponding to each document, used for citation
|
||||
track_id: tracking ID for monitoring processing status, if not provided, will be generated
|
||||
|
||||
Returns:
|
||||
str: tracking ID for monitoring processing status
|
||||
"""
|
||||
await self.apipeline_enqueue_documents(input, ids, file_paths)
|
||||
# Generate track_id if not provided
|
||||
if track_id is None:
|
||||
track_id = generate_track_id("insert")
|
||||
|
||||
await self.apipeline_enqueue_documents(input, ids, file_paths, track_id)
|
||||
await self.apipeline_process_enqueue_documents(
|
||||
split_by_character, split_by_character_only
|
||||
)
|
||||
|
||||
return track_id
|
||||
|
||||
# TODO: deprecated, use insert instead
|
||||
def insert_custom_chunks(
|
||||
self,
|
||||
|
@ -736,7 +794,8 @@ class LightRAG:
|
|||
input: str | list[str],
|
||||
ids: list[str] | None = None,
|
||||
file_paths: str | list[str] | None = None,
|
||||
) -> None:
|
||||
track_id: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Pipeline for Processing Documents
|
||||
|
||||
|
@ -750,7 +809,14 @@ class LightRAG:
|
|||
input: Single document string or list of document strings
|
||||
ids: list of unique document IDs, if not provided, MD5 hash IDs will be generated
|
||||
file_paths: list of file paths corresponding to each document, used for citation
|
||||
track_id: tracking ID for monitoring processing status, if not provided, will be generated with "enqueue" prefix
|
||||
|
||||
Returns:
|
||||
str: tracking ID for monitoring processing status
|
||||
"""
|
||||
# Generate track_id if not provided
|
||||
if track_id is None or track_id.strip() == "":
|
||||
track_id = generate_track_id("enqueue")
|
||||
if isinstance(input, str):
|
||||
input = [input]
|
||||
if isinstance(ids, str):
|
||||
|
@ -820,11 +886,10 @@ class LightRAG:
|
|||
for content, (id_, file_path) in unique_contents.items()
|
||||
}
|
||||
|
||||
# 3. Generate document initial status
|
||||
# 3. Generate document initial status (without content)
|
||||
new_docs: dict[str, Any] = {
|
||||
id_: {
|
||||
"status": DocStatus.PENDING,
|
||||
"content": content_data["content"],
|
||||
"content_summary": get_content_summary(content_data["content"]),
|
||||
"content_length": len(content_data["content"]),
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
|
@ -832,6 +897,7 @@ class LightRAG:
|
|||
"file_path": content_data[
|
||||
"file_path"
|
||||
], # Store file path in document status
|
||||
"track_id": track_id, # Store track_id in document status
|
||||
}
|
||||
for id_, content_data in contents.items()
|
||||
}
|
||||
|
@ -864,10 +930,20 @@ class LightRAG:
|
|||
logger.info("No new unique documents were found.")
|
||||
return
|
||||
|
||||
# 5. Store status document
|
||||
# 5. Store document content in full_docs and status in doc_status
|
||||
# Store full document content separately
|
||||
full_docs_data = {
|
||||
doc_id: {"content": contents[doc_id]["content"]}
|
||||
for doc_id in new_docs.keys()
|
||||
}
|
||||
await self.full_docs.upsert(full_docs_data)
|
||||
|
||||
# Store document status (without content)
|
||||
await self.doc_status.upsert(new_docs)
|
||||
logger.info(f"Stored {len(new_docs)} new unique documents")
|
||||
|
||||
return track_id
|
||||
|
||||
async def apipeline_process_enqueue_documents(
|
||||
self,
|
||||
split_by_character: str | None = None,
|
||||
|
@ -1006,6 +1082,14 @@ class LightRAG:
|
|||
pipeline_status["latest_message"] = log_message
|
||||
pipeline_status["history_messages"].append(log_message)
|
||||
|
||||
# Get document content from full_docs
|
||||
content_data = await self.full_docs.get_by_id(doc_id)
|
||||
if not content_data:
|
||||
raise Exception(
|
||||
f"Document content not found in full_docs for doc_id: {doc_id}"
|
||||
)
|
||||
content = content_data["content"]
|
||||
|
||||
# Generate chunks from document
|
||||
chunks: dict[str, Any] = {
|
||||
compute_mdhash_id(dp["content"], prefix="chunk-"): {
|
||||
|
@ -1016,7 +1100,7 @@ class LightRAG:
|
|||
}
|
||||
for dp in self.chunking_func(
|
||||
self.tokenizer,
|
||||
status_doc.content,
|
||||
content,
|
||||
split_by_character,
|
||||
split_by_character_only,
|
||||
self.chunk_overlap_token_size,
|
||||
|
@ -1027,6 +1111,9 @@ class LightRAG:
|
|||
if not chunks:
|
||||
logger.warning("No document chunks to process")
|
||||
|
||||
# Record processing start time
|
||||
processing_start_time = int(time.time())
|
||||
|
||||
# Process document in two stages
|
||||
# Stage 1: Process text chunks and docs (parallel execution)
|
||||
doc_status_task = asyncio.create_task(
|
||||
|
@ -1038,7 +1125,6 @@ class LightRAG:
|
|||
"chunks_list": list(
|
||||
chunks.keys()
|
||||
), # Save chunks list
|
||||
"content": status_doc.content,
|
||||
"content_summary": status_doc.content_summary,
|
||||
"content_length": status_doc.content_length,
|
||||
"created_at": status_doc.created_at,
|
||||
|
@ -1046,6 +1132,10 @@ class LightRAG:
|
|||
timezone.utc
|
||||
).isoformat(),
|
||||
"file_path": file_path,
|
||||
"track_id": status_doc.track_id, # Preserve existing track_id
|
||||
"metadata": {
|
||||
"processing_start_time": processing_start_time
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -1053,11 +1143,6 @@ class LightRAG:
|
|||
chunks_vdb_task = asyncio.create_task(
|
||||
self.chunks_vdb.upsert(chunks)
|
||||
)
|
||||
full_docs_task = asyncio.create_task(
|
||||
self.full_docs.upsert(
|
||||
{doc_id: {"content": status_doc.content}}
|
||||
)
|
||||
)
|
||||
text_chunks_task = asyncio.create_task(
|
||||
self.text_chunks.upsert(chunks)
|
||||
)
|
||||
|
@ -1066,7 +1151,6 @@ class LightRAG:
|
|||
first_stage_tasks = [
|
||||
doc_status_task,
|
||||
chunks_vdb_task,
|
||||
full_docs_task,
|
||||
text_chunks_task,
|
||||
]
|
||||
entity_relation_task = None
|
||||
|
@ -1109,13 +1193,15 @@ class LightRAG:
|
|||
if self.llm_response_cache:
|
||||
await self.llm_response_cache.index_done_callback()
|
||||
|
||||
# Record processing end time for failed case
|
||||
processing_end_time = int(time.time())
|
||||
|
||||
# Update document status to failed
|
||||
await self.doc_status.upsert(
|
||||
{
|
||||
doc_id: {
|
||||
"status": DocStatus.FAILED,
|
||||
"error": str(e),
|
||||
"content": status_doc.content,
|
||||
"error_msg": str(e),
|
||||
"content_summary": status_doc.content_summary,
|
||||
"content_length": status_doc.content_length,
|
||||
"created_at": status_doc.created_at,
|
||||
|
@ -1123,6 +1209,11 @@ class LightRAG:
|
|||
timezone.utc
|
||||
).isoformat(),
|
||||
"file_path": file_path,
|
||||
"track_id": status_doc.track_id, # Preserve existing track_id
|
||||
"metadata": {
|
||||
"processing_start_time": processing_start_time,
|
||||
"processing_end_time": processing_end_time,
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -1146,6 +1237,9 @@ class LightRAG:
|
|||
file_path=file_path,
|
||||
)
|
||||
|
||||
# Record processing end time
|
||||
processing_end_time = int(time.time())
|
||||
|
||||
await self.doc_status.upsert(
|
||||
{
|
||||
doc_id: {
|
||||
|
@ -1154,7 +1248,6 @@ class LightRAG:
|
|||
"chunks_list": list(
|
||||
chunks.keys()
|
||||
), # 保留 chunks_list
|
||||
"content": status_doc.content,
|
||||
"content_summary": status_doc.content_summary,
|
||||
"content_length": status_doc.content_length,
|
||||
"created_at": status_doc.created_at,
|
||||
|
@ -1162,6 +1255,11 @@ class LightRAG:
|
|||
timezone.utc
|
||||
).isoformat(),
|
||||
"file_path": file_path,
|
||||
"track_id": status_doc.track_id, # Preserve existing track_id
|
||||
"metadata": {
|
||||
"processing_start_time": processing_start_time,
|
||||
"processing_end_time": processing_end_time,
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -1195,18 +1293,25 @@ class LightRAG:
|
|||
if self.llm_response_cache:
|
||||
await self.llm_response_cache.index_done_callback()
|
||||
|
||||
# Record processing end time for failed case
|
||||
processing_end_time = int(time.time())
|
||||
|
||||
# Update document status to failed
|
||||
await self.doc_status.upsert(
|
||||
{
|
||||
doc_id: {
|
||||
"status": DocStatus.FAILED,
|
||||
"error": str(e),
|
||||
"content": status_doc.content,
|
||||
"error_msg": str(e),
|
||||
"content_summary": status_doc.content_summary,
|
||||
"content_length": status_doc.content_length,
|
||||
"created_at": status_doc.created_at,
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
"file_path": file_path,
|
||||
"track_id": status_doc.track_id, # Preserve existing track_id
|
||||
"metadata": {
|
||||
"processing_start_time": processing_start_time,
|
||||
"processing_end_time": processing_end_time,
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -2151,6 +2256,19 @@ class LightRAG:
|
|||
"""
|
||||
return await self.doc_status.get_status_counts()
|
||||
|
||||
async def aget_docs_by_track_id(
|
||||
self, track_id: str
|
||||
) -> dict[str, DocProcessingStatus]:
|
||||
"""Get documents by track_id
|
||||
|
||||
Args:
|
||||
track_id: The tracking ID to search for
|
||||
|
||||
Returns:
|
||||
Dict with document id as keys and document status as values
|
||||
"""
|
||||
return await self.doc_status.get_docs_by_track_id(track_id)
|
||||
|
||||
async def get_entity_info(
|
||||
self, entity_name: str, include_vector_data: bool = False
|
||||
) -> dict[str, str | None | dict[str, str]]:
|
||||
|
|
|
@ -58,7 +58,6 @@ rag = LightRAG(
|
|||
llm_model_func=llm_model_func,
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=1536,
|
||||
max_token_size=8192,
|
||||
func=lambda texts: llama_index_embed(
|
||||
texts,
|
||||
embed_model=OpenAIEmbedding(
|
||||
|
@ -114,7 +113,6 @@ rag = LightRAG(
|
|||
llm_model_func=llm_model_func,
|
||||
embedding_func=EmbeddingFunc(
|
||||
embedding_dim=1536,
|
||||
max_token_size=8192,
|
||||
func=lambda texts: llama_index_embed(
|
||||
texts,
|
||||
embed_model=LiteLLMEmbedding(
|
||||
|
@ -143,7 +141,6 @@ LITELLM_KEY=your-litellm-key
|
|||
# Model Configuration
|
||||
LLM_MODEL=gpt-4
|
||||
EMBEDDING_MODEL=text-embedding-3-large
|
||||
EMBEDDING_MAX_TOKEN_SIZE=8192
|
||||
```
|
||||
|
||||
### Key Differences
|
||||
|
|
|
@ -23,7 +23,6 @@ from tenacity import (
|
|||
|
||||
from lightrag.utils import (
|
||||
wrap_embedding_func_with_attrs,
|
||||
locate_json_string_body_from_string,
|
||||
safe_unicode_decode,
|
||||
)
|
||||
|
||||
|
@ -108,7 +107,7 @@ async def azure_openai_complete_if_cache(
|
|||
async def azure_openai_complete(
|
||||
prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs
|
||||
) -> str:
|
||||
keyword_extraction = kwargs.pop("keyword_extraction", None)
|
||||
kwargs.pop("keyword_extraction", None)
|
||||
result = await azure_openai_complete_if_cache(
|
||||
os.getenv("LLM_MODEL", "gpt-4o-mini"),
|
||||
prompt,
|
||||
|
@ -116,12 +115,10 @@ async def azure_openai_complete(
|
|||
history_messages=history_messages,
|
||||
**kwargs,
|
||||
)
|
||||
if keyword_extraction: # TODO: use JSON API
|
||||
return locate_json_string_body_from_string(result)
|
||||
return result
|
||||
|
||||
|
||||
@wrap_embedding_func_with_attrs(embedding_dim=1536, max_token_size=8191)
|
||||
@wrap_embedding_func_with_attrs(embedding_dim=1536)
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=4, max=10),
|
||||
|
|
|
@ -15,10 +15,6 @@ from tenacity import (
|
|||
retry_if_exception_type,
|
||||
)
|
||||
|
||||
from lightrag.utils import (
|
||||
locate_json_string_body_from_string,
|
||||
)
|
||||
|
||||
|
||||
class BedrockError(Exception):
|
||||
"""Generic error for issues related to Amazon Bedrock"""
|
||||
|
@ -96,7 +92,7 @@ async def bedrock_complete_if_cache(
|
|||
async def bedrock_complete(
|
||||
prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs
|
||||
) -> str:
|
||||
keyword_extraction = kwargs.pop("keyword_extraction", None)
|
||||
kwargs.pop("keyword_extraction", None)
|
||||
model_name = kwargs["hashing_kv"].global_config["llm_model_name"]
|
||||
result = await bedrock_complete_if_cache(
|
||||
model_name,
|
||||
|
@ -105,12 +101,10 @@ async def bedrock_complete(
|
|||
history_messages=history_messages,
|
||||
**kwargs,
|
||||
)
|
||||
if keyword_extraction: # TODO: use JSON API
|
||||
return locate_json_string_body_from_string(result)
|
||||
return result
|
||||
|
||||
|
||||
# @wrap_embedding_func_with_attrs(embedding_dim=1024, max_token_size=8192)
|
||||
# @wrap_embedding_func_with_attrs(embedding_dim=1024)
|
||||
# @retry(
|
||||
# stop=stop_after_attempt(3),
|
||||
# wait=wait_exponential(multiplier=1, min=4, max=10),
|
||||
|
|
|
@ -0,0 +1,446 @@
|
|||
"""
|
||||
Module that implements containers for specific LLM bindings.
|
||||
|
||||
This module provides container implementations for various Large Language Model
|
||||
bindings and integrations.
|
||||
"""
|
||||
|
||||
from argparse import ArgumentParser, Namespace
|
||||
import argparse
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from lightrag.utils import get_env_value
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# BindingOptions Base Class
|
||||
# =============================================================================
|
||||
#
|
||||
# The BindingOptions class serves as the foundation for all LLM provider bindings
|
||||
# in LightRAG. It provides a standardized framework for:
|
||||
#
|
||||
# 1. Configuration Management:
|
||||
# - Defines how each LLM provider's configuration parameters are structured
|
||||
# - Handles default values and type information for each parameter
|
||||
# - Maps configuration options to command-line arguments and environment variables
|
||||
#
|
||||
# 2. Environment Integration:
|
||||
# - Automatically generates environment variable names from binding parameters
|
||||
# - Provides methods to create sample .env files for easy configuration
|
||||
# - Supports configuration via environment variables with fallback to defaults
|
||||
#
|
||||
# 3. Command-Line Interface:
|
||||
# - Dynamically generates command-line arguments for all registered bindings
|
||||
# - Maintains consistent naming conventions across different LLM providers
|
||||
# - Provides help text and type validation for each configuration option
|
||||
#
|
||||
# 4. Extensibility:
|
||||
# - Uses class introspection to automatically discover all binding subclasses
|
||||
# - Requires minimal boilerplate code when adding new LLM provider bindings
|
||||
# - Maintains separation of concerns between different provider configurations
|
||||
#
|
||||
# This design pattern ensures that adding support for a new LLM provider requires
|
||||
# only defining the provider-specific parameters and help text, while the base
|
||||
# class handles all the common functionality for argument parsing, environment
|
||||
# variable handling, and configuration management.
|
||||
#
|
||||
# Instances of a derived class of BindingOptions can be used to store multiple
|
||||
# runtime configurations of options for a single LLM provider. using the
|
||||
# asdict() method to convert the options to a dictionary.
|
||||
#
|
||||
# =============================================================================
|
||||
@dataclass
|
||||
class BindingOptions:
|
||||
"""Base class for binding options."""
|
||||
|
||||
# mandatory name of binding
|
||||
_binding_name: ClassVar[str]
|
||||
|
||||
# optional help message for each option
|
||||
_help: ClassVar[dict[str, str]]
|
||||
|
||||
@staticmethod
|
||||
def _all_class_vars(klass: type, include_inherited=True) -> dict[str, Any]:
|
||||
"""Print class variables, optionally including inherited ones"""
|
||||
if include_inherited:
|
||||
# Get all class variables from MRO
|
||||
vars_dict = {}
|
||||
for base in reversed(klass.__mro__[:-1]): # Exclude 'object'
|
||||
vars_dict.update(
|
||||
{
|
||||
k: v
|
||||
for k, v in base.__dict__.items()
|
||||
if (
|
||||
not k.startswith("_")
|
||||
and not callable(v)
|
||||
and not isinstance(v, classmethod)
|
||||
)
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Only direct class variables
|
||||
vars_dict = {
|
||||
k: v
|
||||
for k, v in klass.__dict__.items()
|
||||
if (
|
||||
not k.startswith("_")
|
||||
and not callable(v)
|
||||
and not isinstance(v, classmethod)
|
||||
)
|
||||
}
|
||||
|
||||
return vars_dict
|
||||
|
||||
@classmethod
|
||||
def add_args(cls, parser: ArgumentParser):
|
||||
group = parser.add_argument_group(f"{cls._binding_name} binding options")
|
||||
for arg_item in cls.args_env_name_type_value():
|
||||
group.add_argument(
|
||||
f"--{arg_item['argname']}",
|
||||
type=arg_item["type"],
|
||||
default=get_env_value(f"{arg_item['env_name']}", argparse.SUPPRESS),
|
||||
help=arg_item["help"],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def args_env_name_type_value(cls):
|
||||
args_prefix = f"{cls._binding_name}".replace("_", "-")
|
||||
env_var_prefix = f"{cls._binding_name}_".upper()
|
||||
class_vars = {
|
||||
key: value
|
||||
for key, value in cls._all_class_vars(cls).items()
|
||||
if not callable(value) and not key.startswith("_")
|
||||
}
|
||||
help = cls._help
|
||||
|
||||
for class_var in class_vars:
|
||||
argdef = {
|
||||
"argname": f"{args_prefix}-{class_var}",
|
||||
"env_name": f"{env_var_prefix}{class_var.upper()}",
|
||||
"type": type(class_vars[class_var]),
|
||||
"default": class_vars[class_var],
|
||||
"help": f"{cls._binding_name} -- " + help.get(class_var, ""),
|
||||
}
|
||||
|
||||
yield argdef
|
||||
|
||||
@classmethod
|
||||
def generate_dot_env_sample(cls):
|
||||
from io import StringIO
|
||||
|
||||
sample_top = (
|
||||
"#" * 80
|
||||
+ "\n"
|
||||
+ (
|
||||
"# Autogenerated .env entries list for LightRAG binding options\n"
|
||||
"#\n"
|
||||
"# To generate run:\n"
|
||||
"# $ python -m lightrag.llm.binding_options\n"
|
||||
)
|
||||
+ "#" * 80
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
sample_bottom = (
|
||||
("#\n# End of .env entries for LightRAG binding options\n")
|
||||
+ "#" * 80
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
sample_stream = StringIO()
|
||||
sample_stream.write(sample_top)
|
||||
for klass in cls.__subclasses__():
|
||||
for arg_item in klass.args_env_name_type_value():
|
||||
if arg_item["help"]:
|
||||
sample_stream.write(f"# {arg_item['help']}\n")
|
||||
sample_stream.write(
|
||||
f"# {arg_item['env_name']}={arg_item['default']}\n\n"
|
||||
)
|
||||
|
||||
sample_stream.write(sample_bottom)
|
||||
return sample_stream.getvalue()
|
||||
|
||||
@classmethod
|
||||
def options_dict(cls, args: Namespace) -> dict[str, Any]:
|
||||
"""
|
||||
Extract options dictionary for a specific binding from parsed arguments.
|
||||
|
||||
This method filters the parsed command-line arguments to return only those
|
||||
that belong to the specific binding class. It removes the binding prefix
|
||||
from argument names to create a clean options dictionary.
|
||||
|
||||
Args:
|
||||
args (Namespace): Parsed command-line arguments containing all binding options
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Dictionary mapping option names (without prefix) to their values
|
||||
|
||||
Example:
|
||||
If args contains {'ollama_num_ctx': 512, 'other_option': 'value'}
|
||||
and this is called on OllamaOptions, it returns {'num_ctx': 512}
|
||||
"""
|
||||
prefix = cls._binding_name + "_"
|
||||
skipchars = len(prefix)
|
||||
options = {
|
||||
key[skipchars:]: value
|
||||
for key, value in vars(args).items()
|
||||
if key.startswith(prefix)
|
||||
}
|
||||
|
||||
return options
|
||||
|
||||
def asdict(self) -> dict[str, Any]:
|
||||
"""
|
||||
Convert an instance of binding options to a dictionary.
|
||||
|
||||
This method uses dataclasses.asdict() to convert the dataclass instance
|
||||
into a dictionary representation, including all its fields and values.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Dictionary representation of the binding options instance
|
||||
"""
|
||||
return asdict(self)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Binding Options for Different LLM Providers
|
||||
# =============================================================================
|
||||
#
|
||||
# This section contains dataclass definitions for various LLM provider options.
|
||||
# Each binding option class inherits from BindingOptions and defines:
|
||||
# - _binding_name: Unique identifier for the binding
|
||||
# - Configuration parameters with default values
|
||||
# - _help: Dictionary mapping parameter names to help descriptions
|
||||
#
|
||||
# To add a new binding:
|
||||
# 1. Create a new dataclass inheriting from BindingOptions
|
||||
# 2. Set the _binding_name class variable
|
||||
# 3. Define configuration parameters as class attributes
|
||||
# 4. Add corresponding help strings in the _help dictionary
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Binding Options for Ollama
|
||||
# =============================================================================
|
||||
#
|
||||
# Ollama binding options provide configuration for the Ollama local LLM server.
|
||||
# These options control model behavior, sampling parameters, hardware utilization,
|
||||
# and performance settings. The parameters are based on Ollama's API specification
|
||||
# and provide fine-grained control over model inference and generation.
|
||||
#
|
||||
# The _OllamaOptionsMixin defines the complete set of available options, while
|
||||
# OllamaEmbeddingOptions and OllamaLLMOptions provide specialized configurations
|
||||
# for embedding and language model tasks respectively.
|
||||
# =============================================================================
|
||||
@dataclass
|
||||
class _OllamaOptionsMixin:
|
||||
"""Options for Ollama bindings."""
|
||||
|
||||
# Core context and generation parameters
|
||||
num_ctx: int = 32768 # Context window size (number of tokens)
|
||||
num_predict: int = 128 # Maximum number of tokens to predict
|
||||
num_keep: int = 0 # Number of tokens to keep from the initial prompt
|
||||
seed: int = -1 # Random seed for generation (-1 for random)
|
||||
|
||||
# Sampling parameters
|
||||
temperature: float = 0.8 # Controls randomness (0.0-2.0)
|
||||
top_k: int = 40 # Top-k sampling parameter
|
||||
top_p: float = 0.9 # Top-p (nucleus) sampling parameter
|
||||
tfs_z: float = 1.0 # Tail free sampling parameter
|
||||
typical_p: float = 1.0 # Typical probability mass
|
||||
min_p: float = 0.0 # Minimum probability threshold
|
||||
|
||||
# Repetition control
|
||||
repeat_last_n: int = 64 # Number of tokens to consider for repetition penalty
|
||||
repeat_penalty: float = 1.1 # Penalty for repetition
|
||||
presence_penalty: float = 0.0 # Penalty for token presence
|
||||
frequency_penalty: float = 0.0 # Penalty for token frequency
|
||||
|
||||
# Mirostat sampling
|
||||
mirostat: int = (
|
||||
# Mirostat sampling algorithm (0=disabled, 1=Mirostat 1.0, 2=Mirostat 2.0)
|
||||
0
|
||||
)
|
||||
mirostat_tau: float = 5.0 # Mirostat target entropy
|
||||
mirostat_eta: float = 0.1 # Mirostat learning rate
|
||||
|
||||
# Hardware and performance parameters
|
||||
numa: bool = False # Enable NUMA optimization
|
||||
num_batch: int = 512 # Batch size for processing
|
||||
num_gpu: int = -1 # Number of GPUs to use (-1 for auto)
|
||||
main_gpu: int = 0 # Main GPU index
|
||||
low_vram: bool = False # Optimize for low VRAM
|
||||
num_thread: int = 0 # Number of CPU threads (0 for auto)
|
||||
|
||||
# Memory and model parameters
|
||||
f16_kv: bool = True # Use half-precision for key/value cache
|
||||
logits_all: bool = False # Return logits for all tokens
|
||||
vocab_only: bool = False # Only load vocabulary
|
||||
use_mmap: bool = True # Use memory mapping for model files
|
||||
use_mlock: bool = False # Lock model in memory
|
||||
embedding_only: bool = False # Only use for embeddings
|
||||
|
||||
# Output control
|
||||
penalize_newline: bool = True # Penalize newline tokens
|
||||
stop: str = "" # Stop sequences (comma-separated)
|
||||
|
||||
# optional help strings
|
||||
_help: ClassVar[dict[str, str]] = {
|
||||
"num_ctx": "Context window size (number of tokens)",
|
||||
"num_predict": "Maximum number of tokens to predict",
|
||||
"num_keep": "Number of tokens to keep from the initial prompt",
|
||||
"seed": "Random seed for generation (-1 for random)",
|
||||
"temperature": "Controls randomness (0.0-2.0, higher = more creative)",
|
||||
"top_k": "Top-k sampling parameter (0 = disabled)",
|
||||
"top_p": "Top-p (nucleus) sampling parameter (0.0-1.0)",
|
||||
"tfs_z": "Tail free sampling parameter (1.0 = disabled)",
|
||||
"typical_p": "Typical probability mass (1.0 = disabled)",
|
||||
"min_p": "Minimum probability threshold (0.0 = disabled)",
|
||||
"repeat_last_n": "Number of tokens to consider for repetition penalty",
|
||||
"repeat_penalty": "Penalty for repetition (1.0 = no penalty)",
|
||||
"presence_penalty": "Penalty for token presence (-2.0 to 2.0)",
|
||||
"frequency_penalty": "Penalty for token frequency (-2.0 to 2.0)",
|
||||
"mirostat": "Mirostat sampling algorithm (0=disabled, 1=Mirostat 1.0, 2=Mirostat 2.0)",
|
||||
"mirostat_tau": "Mirostat target entropy",
|
||||
"mirostat_eta": "Mirostat learning rate",
|
||||
"numa": "Enable NUMA optimization",
|
||||
"num_batch": "Batch size for processing",
|
||||
"num_gpu": "Number of GPUs to use (-1 for auto)",
|
||||
"main_gpu": "Main GPU index",
|
||||
"low_vram": "Optimize for low VRAM",
|
||||
"num_thread": "Number of CPU threads (0 for auto)",
|
||||
"f16_kv": "Use half-precision for key/value cache",
|
||||
"logits_all": "Return logits for all tokens",
|
||||
"vocab_only": "Only load vocabulary",
|
||||
"use_mmap": "Use memory mapping for model files",
|
||||
"use_mlock": "Lock model in memory",
|
||||
"embedding_only": "Only use for embeddings",
|
||||
"penalize_newline": "Penalize newline tokens",
|
||||
"stop": "Stop sequences (comma-separated string)",
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Ollama Binding Options - Specialized Configurations
|
||||
# =============================================================================
|
||||
#
|
||||
# This section defines specialized binding option classes for different Ollama
|
||||
# use cases. Both classes inherit from OllamaOptionsMixin to share the complete
|
||||
# set of Ollama configuration parameters, while providing distinct binding names
|
||||
# for command-line argument generation and environment variable handling.
|
||||
#
|
||||
# OllamaEmbeddingOptions: Specialized for embedding tasks
|
||||
# OllamaLLMOptions: Specialized for language model/chat tasks
|
||||
#
|
||||
# Each class maintains its own binding name prefix, allowing users to configure
|
||||
# embedding and LLM options independently when both are used in the same application.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class OllamaEmbeddingOptions(_OllamaOptionsMixin, BindingOptions):
|
||||
"""Options for Ollama embeddings with specialized configuration for embedding tasks."""
|
||||
|
||||
# mandatory name of binding
|
||||
_binding_name: ClassVar[str] = "ollama_embedding"
|
||||
|
||||
|
||||
@dataclass
|
||||
class OllamaLLMOptions(_OllamaOptionsMixin, BindingOptions):
|
||||
"""Options for Ollama LLM with specialized configuration for LLM tasks."""
|
||||
|
||||
# mandatory name of binding
|
||||
_binding_name: ClassVar[str] = "ollama_llm"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Additional LLM Provider Bindings
|
||||
# =============================================================================
|
||||
#
|
||||
# This section is where you can add binding options for other LLM providers.
|
||||
# Each new binding should follow the same pattern as the Ollama bindings above:
|
||||
#
|
||||
# 1. Create a dataclass that inherits from BindingOptions
|
||||
# 2. Set a unique _binding_name class variable (e.g., "openai", "anthropic")
|
||||
# 3. Define configuration parameters as class attributes with default values
|
||||
# 4. Add a _help class variable with descriptions for each parameter
|
||||
#
|
||||
# Example template for a new provider:
|
||||
#
|
||||
# @dataclass
|
||||
# class NewProviderOptions(BindingOptions):
|
||||
# """Options for NewProvider LLM binding."""
|
||||
#
|
||||
# _binding_name: ClassVar[str] = "newprovider"
|
||||
#
|
||||
# # Configuration parameters
|
||||
# api_key: str = ""
|
||||
# max_tokens: int = 1000
|
||||
# model: str = "default-model"
|
||||
#
|
||||
# # Help descriptions
|
||||
# _help: ClassVar[dict[str, str]] = {
|
||||
# "api_key": "API key for authentication",
|
||||
# "max_tokens": "Maximum tokens to generate",
|
||||
# "model": "Model name to use",
|
||||
# }
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
# TODO: Add binding options for additional LLM providers here
|
||||
# Common providers to consider: OpenAI, Anthropic, Cohere, Hugging Face, etc.
|
||||
|
||||
# =============================================================================
|
||||
# Main Section - For Testing and Sample Generation
|
||||
# =============================================================================
|
||||
#
|
||||
# When run as a script, this module:
|
||||
# 1. Generates and prints a sample .env file with all binding options
|
||||
# 2. If "test" argument is provided, demonstrates argument parsing with Ollama binding
|
||||
#
|
||||
# Usage:
|
||||
# python -m lightrag.llm.binding_options # Generate .env sample
|
||||
# python -m lightrag.llm.binding_options test # Test argument parsing
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import dotenv
|
||||
from io import StringIO
|
||||
|
||||
print(BindingOptions.generate_dot_env_sample())
|
||||
|
||||
env_strstream = StringIO(
|
||||
("OLLAMA_LLM_TEMPERATURE=0.1\nOLLAMA_EMBEDDING_TEMPERATURE=0.2\n")
|
||||
)
|
||||
|
||||
# Load environment variables from .env file
|
||||
dotenv.load_dotenv(stream=env_strstream)
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "test":
|
||||
parser = ArgumentParser(description="Test Ollama binding")
|
||||
OllamaEmbeddingOptions.add_args(parser)
|
||||
OllamaLLMOptions.add_args(parser)
|
||||
args = parser.parse_args(
|
||||
[
|
||||
"--ollama-embedding-num_ctx",
|
||||
"1024",
|
||||
"--ollama-llm-num_ctx",
|
||||
"2048",
|
||||
]
|
||||
)
|
||||
print(args)
|
||||
|
||||
# test LLM options
|
||||
ollama_options = OllamaLLMOptions.options_dict(args)
|
||||
print(ollama_options)
|
||||
print(OllamaLLMOptions(num_ctx=30000).asdict())
|
||||
|
||||
# test embedding options
|
||||
embedding_options = OllamaEmbeddingOptions.options_dict(args)
|
||||
print(embedding_options)
|
||||
print(OllamaEmbeddingOptions(**embedding_options).asdict())
|
|
@ -24,9 +24,6 @@ from lightrag.exceptions import (
|
|||
RateLimitError,
|
||||
APITimeoutError,
|
||||
)
|
||||
from lightrag.utils import (
|
||||
locate_json_string_body_from_string,
|
||||
)
|
||||
import torch
|
||||
import numpy as np
|
||||
|
||||
|
@ -119,7 +116,7 @@ async def hf_model_if_cache(
|
|||
async def hf_model_complete(
|
||||
prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs
|
||||
) -> str:
|
||||
keyword_extraction = kwargs.pop("keyword_extraction", None)
|
||||
kwargs.pop("keyword_extraction", None)
|
||||
model_name = kwargs["hashing_kv"].global_config["llm_model_name"]
|
||||
result = await hf_model_if_cache(
|
||||
model_name,
|
||||
|
@ -128,8 +125,6 @@ async def hf_model_complete(
|
|||
history_messages=history_messages,
|
||||
**kwargs,
|
||||
)
|
||||
if keyword_extraction: # TODO: use JSON API
|
||||
return locate_json_string_body_from_string(result)
|
||||
return result
|
||||
|
||||
|
||||
|
|
|
@ -2,45 +2,117 @@ import os
|
|||
import pipmaster as pm # Pipmaster for dynamic library install
|
||||
|
||||
# install specific modules
|
||||
if not pm.is_installed("lmdeploy"):
|
||||
pm.install("lmdeploy")
|
||||
if not pm.is_installed("aiohttp"):
|
||||
pm.install("aiohttp")
|
||||
if not pm.is_installed("tenacity"):
|
||||
pm.install("tenacity")
|
||||
|
||||
|
||||
import numpy as np
|
||||
import aiohttp
|
||||
from tenacity import (
|
||||
retry,
|
||||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
retry_if_exception_type,
|
||||
)
|
||||
from lightrag.utils import wrap_embedding_func_with_attrs, logger
|
||||
|
||||
|
||||
async def fetch_data(url, headers, data):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, headers=headers, json=data) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
logger.error(f"Jina API error {response.status}: {error_text}")
|
||||
raise aiohttp.ClientResponseError(
|
||||
request_info=response.request_info,
|
||||
history=response.history,
|
||||
status=response.status,
|
||||
message=f"Jina API error: {error_text}",
|
||||
)
|
||||
response_json = await response.json()
|
||||
data_list = response_json.get("data", [])
|
||||
return data_list
|
||||
|
||||
|
||||
@wrap_embedding_func_with_attrs(embedding_dim=2048)
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=4, max=60),
|
||||
retry=(
|
||||
retry_if_exception_type(aiohttp.ClientError)
|
||||
| retry_if_exception_type(aiohttp.ClientResponseError)
|
||||
),
|
||||
)
|
||||
async def jina_embed(
|
||||
texts: list[str],
|
||||
dimensions: int = 1024,
|
||||
dimensions: int = 2048,
|
||||
late_chunking: bool = False,
|
||||
base_url: str = None,
|
||||
api_key: str = None,
|
||||
) -> np.ndarray:
|
||||
"""Generate embeddings for a list of texts using Jina AI's API.
|
||||
|
||||
Args:
|
||||
texts: List of texts to embed.
|
||||
dimensions: The embedding dimensions (default: 2048 for jina-embeddings-v4).
|
||||
late_chunking: Whether to use late chunking.
|
||||
base_url: Optional base URL for the Jina API.
|
||||
api_key: Optional Jina API key. If None, uses the JINA_API_KEY environment variable.
|
||||
|
||||
Returns:
|
||||
A numpy array of embeddings, one per input text.
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientError: If there is a connection error with the Jina API.
|
||||
aiohttp.ClientResponseError: If the Jina API returns an error response.
|
||||
"""
|
||||
if api_key:
|
||||
os.environ["JINA_API_KEY"] = api_key
|
||||
url = "https://api.jina.ai/v1/embeddings" if not base_url else base_url
|
||||
|
||||
if "JINA_API_KEY" not in os.environ:
|
||||
raise ValueError("JINA_API_KEY environment variable is required")
|
||||
|
||||
url = base_url or "https://api.jina.ai/v1/embeddings"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {os.environ['JINA_API_KEY']}",
|
||||
}
|
||||
data = {
|
||||
"model": "jina-embeddings-v3",
|
||||
"normalized": True,
|
||||
"embedding_type": "float",
|
||||
"dimensions": f"{dimensions}",
|
||||
"late_chunking": late_chunking,
|
||||
"model": "jina-embeddings-v4",
|
||||
"task": "text-matching",
|
||||
"dimensions": dimensions,
|
||||
"input": texts,
|
||||
}
|
||||
data_list = await fetch_data(url, headers, data)
|
||||
return np.array([dp["embedding"] for dp in data_list])
|
||||
|
||||
# Only add optional parameters if they have non-default values
|
||||
if late_chunking:
|
||||
data["late_chunking"] = late_chunking
|
||||
|
||||
logger.debug(
|
||||
f"Jina embedding request: {len(texts)} texts, dimensions: {dimensions}"
|
||||
)
|
||||
|
||||
try:
|
||||
data_list = await fetch_data(url, headers, data)
|
||||
|
||||
if not data_list:
|
||||
logger.error("Jina API returned empty data list")
|
||||
raise ValueError("Jina API returned empty data list")
|
||||
|
||||
if len(data_list) != len(texts):
|
||||
logger.error(
|
||||
f"Jina API returned {len(data_list)} embeddings for {len(texts)} texts"
|
||||
)
|
||||
raise ValueError(
|
||||
f"Jina API returned {len(data_list)} embeddings for {len(texts)} texts"
|
||||
)
|
||||
|
||||
embeddings = np.array([dp["embedding"] for dp in data_list])
|
||||
logger.debug(f"Jina embeddings generated: shape {embeddings.shape}")
|
||||
|
||||
return embeddings
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Jina embedding error: {e}")
|
||||
raise
|
||||
|
|
|
@ -21,7 +21,6 @@ from tenacity import (
|
|||
)
|
||||
from lightrag.utils import (
|
||||
wrap_embedding_func_with_attrs,
|
||||
locate_json_string_body_from_string,
|
||||
)
|
||||
from lightrag.exceptions import (
|
||||
APIConnectionError,
|
||||
|
@ -157,7 +156,7 @@ async def llama_index_complete(
|
|||
if history_messages is None:
|
||||
history_messages = []
|
||||
|
||||
keyword_extraction = kwargs.pop("keyword_extraction", None)
|
||||
kwargs.pop("keyword_extraction", None)
|
||||
result = await llama_index_complete_if_cache(
|
||||
kwargs.get("llm_instance"),
|
||||
prompt,
|
||||
|
@ -165,12 +164,10 @@ async def llama_index_complete(
|
|||
history_messages=history_messages,
|
||||
**kwargs,
|
||||
)
|
||||
if keyword_extraction:
|
||||
return locate_json_string_body_from_string(result)
|
||||
return result
|
||||
|
||||
|
||||
@wrap_embedding_func_with_attrs(embedding_dim=1536, max_token_size=8192)
|
||||
@wrap_embedding_func_with_attrs(embedding_dim=1536)
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=4, max=60),
|
||||
|
|
|
@ -59,7 +59,7 @@ async def lollms_model_if_cache(
|
|||
"personality": kwargs.get("personality", -1),
|
||||
"n_predict": kwargs.get("n_predict", None),
|
||||
"stream": stream,
|
||||
"temperature": kwargs.get("temperature", 0.1),
|
||||
"temperature": kwargs.get("temperature", 0.8),
|
||||
"top_k": kwargs.get("top_k", 50),
|
||||
"top_p": kwargs.get("top_p", 0.95),
|
||||
"repeat_penalty": kwargs.get("repeat_penalty", 0.8),
|
||||
|
|
|
@ -33,7 +33,7 @@ from lightrag.utils import (
|
|||
import numpy as np
|
||||
|
||||
|
||||
@wrap_embedding_func_with_attrs(embedding_dim=2048, max_token_size=512)
|
||||
@wrap_embedding_func_with_attrs(embedding_dim=2048)
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=4, max=60),
|
||||
|
|
|
@ -50,7 +50,7 @@ async def _ollama_model_if_cache(
|
|||
kwargs.pop("max_tokens", None)
|
||||
# kwargs.pop("response_format", None) # allow json
|
||||
host = kwargs.pop("host", None)
|
||||
timeout = kwargs.pop("timeout", None) or 600 # Default timeout 600s
|
||||
timeout = kwargs.pop("timeout", None)
|
||||
kwargs.pop("hashing_kv", None)
|
||||
api_key = kwargs.pop("api_key", None)
|
||||
headers = {
|
||||
|
@ -146,12 +146,14 @@ async def ollama_embed(texts: list[str], embed_model, **kwargs) -> np.ndarray:
|
|||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
host = kwargs.pop("host", None)
|
||||
timeout = kwargs.pop("timeout", None) or 300 # Default time out 300s
|
||||
timeout = kwargs.pop("timeout", None)
|
||||
|
||||
ollama_client = ollama.AsyncClient(host=host, timeout=timeout, headers=headers)
|
||||
|
||||
try:
|
||||
data = await ollama_client.embed(model=embed_model, input=texts)
|
||||
options = kwargs.pop("options", {})
|
||||
data = await ollama_client.embed(
|
||||
model=embed_model, input=texts, options=options
|
||||
)
|
||||
return np.array(data["embeddings"])
|
||||
except Exception as e:
|
||||
logger.error(f"Error in ollama_embed: {str(e)}")
|
||||
|
|
|
@ -27,7 +27,6 @@ from tenacity import (
|
|||
)
|
||||
from lightrag.utils import (
|
||||
wrap_embedding_func_with_attrs,
|
||||
locate_json_string_body_from_string,
|
||||
safe_unicode_decode,
|
||||
logger,
|
||||
)
|
||||
|
@ -418,7 +417,7 @@ async def nvidia_openai_complete(
|
|||
) -> str:
|
||||
if history_messages is None:
|
||||
history_messages = []
|
||||
keyword_extraction = kwargs.pop("keyword_extraction", None)
|
||||
kwargs.pop("keyword_extraction", None)
|
||||
result = await openai_complete_if_cache(
|
||||
"nvidia/llama-3.1-nemotron-70b-instruct", # context length 128k
|
||||
prompt,
|
||||
|
@ -427,12 +426,10 @@ async def nvidia_openai_complete(
|
|||
base_url="https://integrate.api.nvidia.com/v1",
|
||||
**kwargs,
|
||||
)
|
||||
if keyword_extraction: # TODO: use JSON API
|
||||
return locate_json_string_body_from_string(result)
|
||||
return result
|
||||
|
||||
|
||||
@wrap_embedding_func_with_attrs(embedding_dim=1536, max_token_size=8192)
|
||||
@wrap_embedding_func_with_attrs(embedding_dim=1536)
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=4, max=60),
|
||||
|
|
|
@ -40,7 +40,7 @@ async def siliconcloud_embedding(
|
|||
texts: list[str],
|
||||
model: str = "netease-youdao/bce-embedding-base_v1",
|
||||
base_url: str = "https://api.siliconflow.cn/v1/embeddings",
|
||||
max_token_size: int = 512,
|
||||
max_token_size: int = 8192,
|
||||
api_key: str = None,
|
||||
) -> np.ndarray:
|
||||
if api_key and not api_key.startswith("Bearer "):
|
||||
|
|
|
@ -167,7 +167,7 @@ async def zhipu_complete(
|
|||
)
|
||||
|
||||
|
||||
@wrap_embedding_func_with_attrs(embedding_dim=1024, max_token_size=8192)
|
||||
@wrap_embedding_func_with_attrs(embedding_dim=1024)
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=4, max=60),
|
||||
|
|
1076
lightrag/operate.py
1076
lightrag/operate.py
File diff suppressed because it is too large
Load Diff
|
@ -19,9 +19,9 @@ Use {language} as output language.
|
|||
|
||||
---Steps---
|
||||
1. Identify all entities. For each identified entity, extract the following information:
|
||||
- entity_name: Name of the entity, use same language as input text. If English, capitalized the name.
|
||||
- entity_name: Name of the entity, use same language as input text. If English, capitalized the name
|
||||
- entity_type: One of the following types: [{entity_types}]
|
||||
- entity_description: Comprehensive description of the entity's attributes and activities
|
||||
- entity_description: Provide a comprehensive description of the entity's attributes and activities *based solely on the information present in the input text*. **Do not infer or hallucinate information not explicitly stated.** If the text provides insufficient information to create a comprehensive description, state "Description not available in text."
|
||||
Format each entity as ("entity"{tuple_delimiter}<entity_name>{tuple_delimiter}<entity_type>{tuple_delimiter}<entity_description>)
|
||||
|
||||
2. From the entities identified in step 1, identify all pairs of (source_entity, target_entity) that are *clearly related* to each other.
|
||||
|
@ -151,14 +151,14 @@ Output:
|
|||
"""
|
||||
|
||||
PROMPTS["entity_continue_extraction"] = """
|
||||
MANY entities and relationships were missed in the last extraction.
|
||||
MANY entities and relationships were missed in the last extraction. Please find only the missing entities and relationships from previous text.
|
||||
|
||||
---Remember Steps---
|
||||
|
||||
1. Identify all entities. For each identified entity, extract the following information:
|
||||
- entity_name: Name of the entity, use same language as input text. If English, capitalized the name.
|
||||
- entity_name: Name of the entity, use same language as input text. If English, capitalized the name
|
||||
- entity_type: One of the following types: [{entity_types}]
|
||||
- entity_description: Comprehensive description of the entity's attributes and activities
|
||||
- entity_description: Provide a comprehensive description of the entity's attributes and activities *based solely on the information present in the input text*. **Do not infer or hallucinate information not explicitly stated.** If the text provides insufficient information to create a comprehensive description, state "Description not available in text."
|
||||
Format each entity as ("entity"{tuple_delimiter}<entity_name>{tuple_delimiter}<entity_type>{tuple_delimiter}<entity_description>)
|
||||
|
||||
2. From the entities identified in step 1, identify all pairs of (source_entity, target_entity) that are *clearly related* to each other.
|
||||
|
@ -179,7 +179,7 @@ Format the content-level key words as ("content_keywords"{tuple_delimiter}<high_
|
|||
|
||||
---Output---
|
||||
|
||||
Add them below using the same format:\n
|
||||
Add new entities and relations below using the same format, and do not include entities and relations that have been previously extracted. :\n
|
||||
""".strip()
|
||||
|
||||
PROMPTS["entity_if_loop_extraction"] = """
|
||||
|
@ -226,7 +226,7 @@ When handling relationships with timestamps:
|
|||
- List up to 5 most important reference sources at the end under "References" section. Clearly indicating whether each source is from Knowledge Graph (KG) or Document Chunks (DC), and include the file path if available, in the following format: [KG/DC] file_path
|
||||
- If you don't know the answer, just say so.
|
||||
- Do not make anything up. Do not include information not provided by the Knowledge Base.
|
||||
- Addtional user prompt: {user_prompt}
|
||||
- Additional user prompt: {user_prompt}
|
||||
|
||||
Response:"""
|
||||
|
||||
|
@ -251,7 +251,7 @@ Given the query and conversation history, list both high-level and low-level key
|
|||
######################
|
||||
{examples}
|
||||
|
||||
#############################
|
||||
######################
|
||||
---Real Data---
|
||||
######################
|
||||
Conversation History:
|
||||
|
@ -259,42 +259,45 @@ Conversation History:
|
|||
|
||||
Current Query: {query}
|
||||
######################
|
||||
The `Output` should be human text, not unicode characters. Keep the same language as `Query`.
|
||||
Output:
|
||||
The `Output` should be in JSON format, with no other text before and after the JSON. Use the same language as `Current Query`.
|
||||
|
||||
Output:
|
||||
"""
|
||||
|
||||
PROMPTS["keywords_extraction_examples"] = [
|
||||
"""Example 1:
|
||||
|
||||
Query: "How does international trade influence global economic stability?"
|
||||
################
|
||||
|
||||
Output:
|
||||
{
|
||||
"high_level_keywords": ["International trade", "Global economic stability", "Economic impact"],
|
||||
"low_level_keywords": ["Trade agreements", "Tariffs", "Currency exchange", "Imports", "Exports"]
|
||||
}
|
||||
#############################""",
|
||||
|
||||
""",
|
||||
"""Example 2:
|
||||
|
||||
Query: "What are the environmental consequences of deforestation on biodiversity?"
|
||||
################
|
||||
|
||||
Output:
|
||||
{
|
||||
"high_level_keywords": ["Environmental consequences", "Deforestation", "Biodiversity loss"],
|
||||
"low_level_keywords": ["Species extinction", "Habitat destruction", "Carbon emissions", "Rainforest", "Ecosystem"]
|
||||
}
|
||||
#############################""",
|
||||
|
||||
""",
|
||||
"""Example 3:
|
||||
|
||||
Query: "What is the role of education in reducing poverty?"
|
||||
################
|
||||
|
||||
Output:
|
||||
{
|
||||
"high_level_keywords": ["Education", "Poverty reduction", "Socioeconomic development"],
|
||||
"low_level_keywords": ["School access", "Literacy rates", "Job training", "Income inequality"]
|
||||
}
|
||||
#############################""",
|
||||
|
||||
""",
|
||||
]
|
||||
|
||||
PROMPTS["naive_rag_response"] = """---Role---
|
||||
|
|
|
@ -9,7 +9,9 @@ import logging
|
|||
import logging.handlers
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from hashlib import md5
|
||||
from typing import Any, Protocol, Callable, TYPE_CHECKING, List
|
||||
|
@ -19,6 +21,9 @@ from lightrag.constants import (
|
|||
DEFAULT_LOG_MAX_BYTES,
|
||||
DEFAULT_LOG_BACKUP_COUNT,
|
||||
DEFAULT_LOG_FILENAME,
|
||||
GRAPH_FIELD_SEP,
|
||||
DEFAULT_MAX_TOTAL_TOKENS,
|
||||
DEFAULT_MAX_FILE_PATH_LENGTH,
|
||||
)
|
||||
|
||||
|
||||
|
@ -55,7 +60,7 @@ def get_env_value(
|
|||
|
||||
# Use TYPE_CHECKING to avoid circular imports
|
||||
if TYPE_CHECKING:
|
||||
from lightrag.base import BaseKVStorage
|
||||
from lightrag.base import BaseKVStorage, QueryParam
|
||||
|
||||
# use the .env that is inside the current folder
|
||||
# allows to use different .env file for each lightrag instance
|
||||
|
@ -85,8 +90,10 @@ def verbose_debug(msg: str, *args, **kwargs):
|
|||
formatted_msg = msg
|
||||
# Then truncate the formatted message
|
||||
truncated_msg = (
|
||||
formatted_msg[:100] + "..." if len(formatted_msg) > 100 else formatted_msg
|
||||
formatted_msg[:150] + "..." if len(formatted_msg) > 150 else formatted_msg
|
||||
)
|
||||
# Remove consecutive newlines
|
||||
truncated_msg = re.sub(r"\n+", "\n", truncated_msg)
|
||||
logger.debug(truncated_msg, **kwargs)
|
||||
|
||||
|
||||
|
@ -116,6 +123,7 @@ class LightragPathFilter(logging.Filter):
|
|||
# Define paths to be filtered
|
||||
self.filtered_paths = [
|
||||
"/documents",
|
||||
"/documents/paginated",
|
||||
"/health",
|
||||
"/webui/",
|
||||
"/documents/pipeline_status",
|
||||
|
@ -138,6 +146,7 @@ class LightragPathFilter(logging.Filter):
|
|||
# Filter out successful GET requests to filtered paths
|
||||
if (
|
||||
method == "GET"
|
||||
or method == "POST"
|
||||
and (status == 200 or status == 304)
|
||||
and path in self.filtered_paths
|
||||
):
|
||||
|
@ -232,51 +241,13 @@ class UnlimitedSemaphore:
|
|||
@dataclass
|
||||
class EmbeddingFunc:
|
||||
embedding_dim: int
|
||||
max_token_size: int
|
||||
func: callable
|
||||
# concurrent_limit: int = 16
|
||||
max_token_size: int | None = None # deprecated keep it for compatible only
|
||||
|
||||
async def __call__(self, *args, **kwargs) -> np.ndarray:
|
||||
return await self.func(*args, **kwargs)
|
||||
|
||||
|
||||
def locate_json_string_body_from_string(content: str) -> str | None:
|
||||
"""Locate the JSON string body from a string"""
|
||||
try:
|
||||
maybe_json_str = re.search(r"{.*}", content, re.DOTALL)
|
||||
if maybe_json_str is not None:
|
||||
maybe_json_str = maybe_json_str.group(0)
|
||||
maybe_json_str = maybe_json_str.replace("\\n", "")
|
||||
maybe_json_str = maybe_json_str.replace("\n", "")
|
||||
maybe_json_str = maybe_json_str.replace("'", '"')
|
||||
# json.loads(maybe_json_str) # don't check here, cannot validate schema after all
|
||||
return maybe_json_str
|
||||
except Exception:
|
||||
pass
|
||||
# try:
|
||||
# content = (
|
||||
# content.replace(kw_prompt[:-1], "")
|
||||
# .replace("user", "")
|
||||
# .replace("model", "")
|
||||
# .strip()
|
||||
# )
|
||||
# maybe_json_str = "{" + content.split("{")[1].split("}")[0] + "}"
|
||||
# json.loads(maybe_json_str)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def convert_response_to_json(response: str) -> dict[str, Any]:
|
||||
json_str = locate_json_string_body_from_string(response)
|
||||
assert json_str is not None, f"Unable to parse JSON from response: {response}"
|
||||
try:
|
||||
data = json.loads(json_str)
|
||||
return data
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse JSON: {json_str}")
|
||||
raise e from None
|
||||
|
||||
|
||||
def compute_args_hash(*args: Any) -> str:
|
||||
"""Compute a hash for the given arguments.
|
||||
Args:
|
||||
|
@ -777,39 +748,6 @@ def truncate_list_by_token_size(
|
|||
return list_data
|
||||
|
||||
|
||||
def process_combine_contexts(*context_lists):
|
||||
"""
|
||||
Combine multiple context lists and remove duplicate content
|
||||
|
||||
Args:
|
||||
*context_lists: Any number of context lists
|
||||
|
||||
Returns:
|
||||
Combined context list with duplicates removed
|
||||
"""
|
||||
seen_content = {}
|
||||
combined_data = []
|
||||
|
||||
# Iterate through all input context lists
|
||||
for context_list in context_lists:
|
||||
if not context_list: # Skip empty lists
|
||||
continue
|
||||
for item in context_list:
|
||||
content_dict = {
|
||||
k: v for k, v in item.items() if k != "id" and k != "created_at"
|
||||
}
|
||||
content_key = tuple(sorted(content_dict.items()))
|
||||
if content_key not in seen_content:
|
||||
seen_content[content_key] = item
|
||||
combined_data.append(item)
|
||||
|
||||
# Reassign IDs
|
||||
for i, item in enumerate(combined_data):
|
||||
item["id"] = str(i + 1)
|
||||
|
||||
return combined_data
|
||||
|
||||
|
||||
def cosine_similarity(v1, v2):
|
||||
"""Calculate cosine similarity between two vectors"""
|
||||
dot_product = np.dot(v1, v2)
|
||||
|
@ -1673,6 +1611,86 @@ def check_storage_env_vars(storage_name: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
def linear_gradient_weighted_polling(
|
||||
entities_or_relations: list[dict],
|
||||
max_related_chunks: int,
|
||||
min_related_chunks: int = 1,
|
||||
) -> list[str]:
|
||||
"""
|
||||
Linear gradient weighted polling algorithm for text chunk selection.
|
||||
|
||||
This algorithm ensures that entities/relations with higher importance get more text chunks,
|
||||
forming a linear decreasing allocation pattern.
|
||||
|
||||
Args:
|
||||
entities_or_relations: List of entities or relations sorted by importance (high to low)
|
||||
max_related_chunks: Expected number of text chunks for the highest importance entity/relation
|
||||
min_related_chunks: Expected number of text chunks for the lowest importance entity/relation
|
||||
|
||||
Returns:
|
||||
List of selected text chunk IDs
|
||||
"""
|
||||
if not entities_or_relations:
|
||||
return []
|
||||
|
||||
n = len(entities_or_relations)
|
||||
if n == 1:
|
||||
# Only one entity/relation, return its first max_related_chunks text chunks
|
||||
entity_chunks = entities_or_relations[0].get("sorted_chunks", [])
|
||||
return entity_chunks[:max_related_chunks]
|
||||
|
||||
# Calculate expected text chunk count for each position (linear decrease)
|
||||
expected_counts = []
|
||||
for i in range(n):
|
||||
# Linear interpolation: from max_related_chunks to min_related_chunks
|
||||
ratio = i / (n - 1) if n > 1 else 0
|
||||
expected = max_related_chunks - ratio * (
|
||||
max_related_chunks - min_related_chunks
|
||||
)
|
||||
expected_counts.append(int(round(expected)))
|
||||
|
||||
# First round allocation: allocate by expected values
|
||||
selected_chunks = []
|
||||
used_counts = [] # Track number of chunks used by each entity
|
||||
total_remaining = 0 # Accumulate remaining quotas
|
||||
|
||||
for i, entity_rel in enumerate(entities_or_relations):
|
||||
entity_chunks = entity_rel.get("sorted_chunks", [])
|
||||
expected = expected_counts[i]
|
||||
|
||||
# Actual allocatable count
|
||||
actual = min(expected, len(entity_chunks))
|
||||
selected_chunks.extend(entity_chunks[:actual])
|
||||
used_counts.append(actual)
|
||||
|
||||
# Accumulate remaining quota
|
||||
remaining = expected - actual
|
||||
if remaining > 0:
|
||||
total_remaining += remaining
|
||||
|
||||
# Second round allocation: multi-round scanning to allocate remaining quotas
|
||||
for _ in range(total_remaining):
|
||||
allocated = False
|
||||
|
||||
# Scan entities one by one, allocate one chunk when finding unused chunks
|
||||
for i, entity_rel in enumerate(entities_or_relations):
|
||||
entity_chunks = entity_rel.get("sorted_chunks", [])
|
||||
|
||||
# Check if there are still unused chunks
|
||||
if used_counts[i] < len(entity_chunks):
|
||||
# Allocate one chunk
|
||||
selected_chunks.append(entity_chunks[used_counts[i]])
|
||||
used_counts[i] += 1
|
||||
allocated = True
|
||||
break
|
||||
|
||||
# If no chunks were allocated in this round, all entities are exhausted
|
||||
if not allocated:
|
||||
break
|
||||
|
||||
return selected_chunks
|
||||
|
||||
|
||||
class TokenTracker:
|
||||
"""Track token usage for LLM calls."""
|
||||
|
||||
|
@ -1728,3 +1746,217 @@ class TokenTracker:
|
|||
f"Completion tokens: {usage['completion_tokens']}, "
|
||||
f"Total tokens: {usage['total_tokens']}"
|
||||
)
|
||||
|
||||
|
||||
async def apply_rerank_if_enabled(
|
||||
query: str,
|
||||
retrieved_docs: list[dict],
|
||||
global_config: dict,
|
||||
enable_rerank: bool = True,
|
||||
top_n: int = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Apply reranking to retrieved documents if rerank is enabled.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
retrieved_docs: List of retrieved documents
|
||||
global_config: Global configuration containing rerank settings
|
||||
enable_rerank: Whether to enable reranking from query parameter
|
||||
top_n: Number of top documents to return after reranking
|
||||
|
||||
Returns:
|
||||
Reranked documents if rerank is enabled, otherwise original documents
|
||||
"""
|
||||
if not enable_rerank or not retrieved_docs:
|
||||
return retrieved_docs
|
||||
|
||||
rerank_func = global_config.get("rerank_model_func")
|
||||
if not rerank_func:
|
||||
logger.warning(
|
||||
"Rerank is enabled but no rerank model is configured. Please set up a rerank model or set enable_rerank=False in query parameters."
|
||||
)
|
||||
return retrieved_docs
|
||||
|
||||
try:
|
||||
# Apply reranking - let rerank_model_func handle top_k internally
|
||||
reranked_docs = await rerank_func(
|
||||
query=query,
|
||||
documents=retrieved_docs,
|
||||
top_n=top_n,
|
||||
)
|
||||
if reranked_docs and len(reranked_docs) > 0:
|
||||
if len(reranked_docs) > top_n:
|
||||
reranked_docs = reranked_docs[:top_n]
|
||||
logger.info(f"Successfully reranked: {len(retrieved_docs)} chunks")
|
||||
return reranked_docs
|
||||
else:
|
||||
logger.warning("Rerank returned empty results, using original chunks")
|
||||
return retrieved_docs
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during reranking: {e}, using original chunks")
|
||||
return retrieved_docs
|
||||
|
||||
|
||||
async def process_chunks_unified(
|
||||
query: str,
|
||||
unique_chunks: list[dict],
|
||||
query_param: "QueryParam",
|
||||
global_config: dict,
|
||||
source_type: str = "mixed",
|
||||
chunk_token_limit: int = None, # Add parameter for dynamic token limit
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Unified processing for text chunks: deduplication, chunk_top_k limiting, reranking, and token truncation.
|
||||
|
||||
Args:
|
||||
query: Search query for reranking
|
||||
chunks: List of text chunks to process
|
||||
query_param: Query parameters containing configuration
|
||||
global_config: Global configuration dictionary
|
||||
source_type: Source type for logging ("vector", "entity", "relationship", "mixed")
|
||||
chunk_token_limit: Dynamic token limit for chunks (if None, uses default)
|
||||
|
||||
Returns:
|
||||
Processed and filtered list of text chunks
|
||||
"""
|
||||
if not unique_chunks:
|
||||
return []
|
||||
|
||||
origin_count = len(unique_chunks)
|
||||
|
||||
# 1. Apply reranking if enabled and query is provided
|
||||
if query_param.enable_rerank and query and unique_chunks:
|
||||
rerank_top_k = query_param.chunk_top_k or len(unique_chunks)
|
||||
unique_chunks = await apply_rerank_if_enabled(
|
||||
query=query,
|
||||
retrieved_docs=unique_chunks,
|
||||
global_config=global_config,
|
||||
enable_rerank=query_param.enable_rerank,
|
||||
top_n=rerank_top_k,
|
||||
)
|
||||
|
||||
# 2. Filter by minimum rerank score if reranking is enabled
|
||||
if query_param.enable_rerank and unique_chunks:
|
||||
min_rerank_score = global_config.get("min_rerank_score", 0.5)
|
||||
if min_rerank_score > 0.0:
|
||||
original_count = len(unique_chunks)
|
||||
|
||||
# Filter chunks with score below threshold
|
||||
filtered_chunks = []
|
||||
for chunk in unique_chunks:
|
||||
rerank_score = chunk.get(
|
||||
"rerank_score", 1.0
|
||||
) # Default to 1.0 if no score
|
||||
if rerank_score >= min_rerank_score:
|
||||
filtered_chunks.append(chunk)
|
||||
|
||||
unique_chunks = filtered_chunks
|
||||
filtered_count = original_count - len(unique_chunks)
|
||||
|
||||
if filtered_count > 0:
|
||||
logger.info(
|
||||
f"Rerank filtering: {len(unique_chunks)} chunks remained (min rerank score: {min_rerank_score})"
|
||||
)
|
||||
if not unique_chunks:
|
||||
return []
|
||||
|
||||
# 3. Apply chunk_top_k limiting if specified
|
||||
if query_param.chunk_top_k is not None and query_param.chunk_top_k > 0:
|
||||
if len(unique_chunks) > query_param.chunk_top_k:
|
||||
unique_chunks = unique_chunks[: query_param.chunk_top_k]
|
||||
logger.debug(
|
||||
f"Kept chunk_top-k: {len(unique_chunks)} chunks (deduplicated original: {origin_count})"
|
||||
)
|
||||
|
||||
# 4. Token-based final truncation
|
||||
tokenizer = global_config.get("tokenizer")
|
||||
if tokenizer and unique_chunks:
|
||||
# Set default chunk_token_limit if not provided
|
||||
if chunk_token_limit is None:
|
||||
# Get default from query_param or global_config
|
||||
chunk_token_limit = getattr(
|
||||
query_param,
|
||||
"max_total_tokens",
|
||||
global_config.get("MAX_TOTAL_TOKENS", DEFAULT_MAX_TOTAL_TOKENS),
|
||||
)
|
||||
|
||||
original_count = len(unique_chunks)
|
||||
unique_chunks = truncate_list_by_token_size(
|
||||
unique_chunks,
|
||||
key=lambda x: x.get("content", ""),
|
||||
max_token_size=chunk_token_limit,
|
||||
tokenizer=tokenizer,
|
||||
)
|
||||
logger.debug(
|
||||
f"Token truncation: {len(unique_chunks)} chunks from {original_count} "
|
||||
f"(chunk available tokens: {chunk_token_limit}, source: {source_type})"
|
||||
)
|
||||
|
||||
return unique_chunks
|
||||
|
||||
|
||||
def build_file_path(already_file_paths, data_list, target):
|
||||
"""Build file path string with length limit and deduplication
|
||||
|
||||
Args:
|
||||
already_file_paths: List of existing file paths
|
||||
data_list: List of data items containing file_path
|
||||
target: Target name for logging warnings
|
||||
|
||||
Returns:
|
||||
str: Combined file paths separated by GRAPH_FIELD_SEP
|
||||
"""
|
||||
# set: deduplication
|
||||
file_paths_set = {fp for fp in already_file_paths if fp}
|
||||
|
||||
# string: filter empty value and keep file order in already_file_paths
|
||||
file_paths = GRAPH_FIELD_SEP.join(fp for fp in already_file_paths if fp)
|
||||
# ignored file_paths
|
||||
file_paths_ignore = ""
|
||||
# add file_paths
|
||||
for dp in data_list:
|
||||
cur_file_path = dp.get("file_path")
|
||||
# empty
|
||||
if not cur_file_path:
|
||||
continue
|
||||
|
||||
# skip duplicate item
|
||||
if cur_file_path in file_paths_set:
|
||||
continue
|
||||
# add
|
||||
file_paths_set.add(cur_file_path)
|
||||
|
||||
# check the length
|
||||
if (
|
||||
len(file_paths) + len(GRAPH_FIELD_SEP + cur_file_path)
|
||||
< DEFAULT_MAX_FILE_PATH_LENGTH
|
||||
):
|
||||
# append
|
||||
file_paths += (
|
||||
GRAPH_FIELD_SEP + cur_file_path if file_paths else cur_file_path
|
||||
)
|
||||
else:
|
||||
# ignore
|
||||
file_paths_ignore += GRAPH_FIELD_SEP + cur_file_path
|
||||
|
||||
if file_paths_ignore:
|
||||
logger.warning(
|
||||
f"Length of file_path exceeds {target}, ignoring new file: {file_paths_ignore}"
|
||||
)
|
||||
return file_paths
|
||||
|
||||
|
||||
def generate_track_id(prefix: str = "upload") -> str:
|
||||
"""Generate a unique tracking ID with timestamp and UUID
|
||||
|
||||
Args:
|
||||
prefix: Prefix for the track ID (e.g., 'upload', 'insert')
|
||||
|
||||
Returns:
|
||||
str: Unique tracking ID in format: {prefix}_{timestamp}_{uuid}
|
||||
"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
unique_id = str(uuid.uuid4())[:8] # Use first 8 characters of UUID
|
||||
return f"{prefix}_{timestamp}_{unique_id}"
|
||||
|
|
|
@ -3,7 +3,7 @@ import ThemeProvider from '@/components/ThemeProvider'
|
|||
import TabVisibilityProvider from '@/contexts/TabVisibilityProvider'
|
||||
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
||||
import StatusIndicator from '@/components/status/StatusIndicator'
|
||||
import { healthCheckInterval, SiteInfo, webuiPrefix } from '@/lib/constants'
|
||||
import { SiteInfo, webuiPrefix } from '@/lib/constants'
|
||||
import { useBackendState, useAuthStore } from '@/stores/state'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { getAuthStatus } from '@/api/lightrag'
|
||||
|
@ -56,9 +56,6 @@ function App() {
|
|||
|
||||
// Health check - can be disabled
|
||||
useEffect(() => {
|
||||
// Only execute if health check is enabled and ApiKeyAlert is closed
|
||||
if (!enableHealthCheck || apiKeyAlertOpen) return;
|
||||
|
||||
// Health check function
|
||||
const performHealthCheck = async () => {
|
||||
try {
|
||||
|
@ -71,17 +68,27 @@ function App() {
|
|||
}
|
||||
};
|
||||
|
||||
// On first mount or when enableHealthCheck becomes true and apiKeyAlertOpen is false,
|
||||
// perform an immediate health check
|
||||
if (!healthCheckInitializedRef.current) {
|
||||
healthCheckInitializedRef.current = true;
|
||||
// Immediate health check on first load
|
||||
performHealthCheck();
|
||||
// Set health check function in the store
|
||||
useBackendState.getState().setHealthCheckFunction(performHealthCheck);
|
||||
|
||||
if (!enableHealthCheck || apiKeyAlertOpen) {
|
||||
useBackendState.getState().clearHealthCheckTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set interval for periodic execution
|
||||
const interval = setInterval(performHealthCheck, healthCheckInterval * 1000);
|
||||
return () => clearInterval(interval);
|
||||
// On first mount or when enableHealthCheck becomes true and apiKeyAlertOpen is false,
|
||||
// perform an immediate health check and start the timer
|
||||
if (!healthCheckInitializedRef.current) {
|
||||
healthCheckInitializedRef.current = true;
|
||||
}
|
||||
|
||||
// Start/reset the health check timer using the store
|
||||
useBackendState.getState().resetHealthCheckTimer();
|
||||
|
||||
// Component unmount cleanup
|
||||
return () => {
|
||||
useBackendState.getState().clearHealthCheckTimer();
|
||||
};
|
||||
}, [enableHealthCheck, apiKeyAlertOpen]);
|
||||
|
||||
// Version check - independent and executed only once
|
||||
|
|
|
@ -45,6 +45,15 @@ export type LightragStatus = {
|
|||
enable_rerank?: boolean
|
||||
rerank_model?: string | null
|
||||
rerank_binding_host?: string | null
|
||||
summary_language: string
|
||||
force_llm_summary_on_merge: boolean
|
||||
max_parallel_insert: number
|
||||
max_async: number
|
||||
embedding_func_max_async: number
|
||||
embedding_batch_num: number
|
||||
cosine_threshold: number
|
||||
min_rerank_score: number
|
||||
related_chunk_number: number
|
||||
}
|
||||
update_status?: Record<string, any>
|
||||
core_version?: string
|
||||
|
@ -134,6 +143,13 @@ export type QueryResponse = {
|
|||
export type DocActionResponse = {
|
||||
status: 'success' | 'partial_success' | 'failure' | 'duplicated'
|
||||
message: string
|
||||
track_id?: string
|
||||
}
|
||||
|
||||
export type ScanResponse = {
|
||||
status: 'scanning_started'
|
||||
message: string
|
||||
track_id: string
|
||||
}
|
||||
|
||||
export type DeleteDocResponse = {
|
||||
|
@ -151,8 +167,9 @@ export type DocStatusResponse = {
|
|||
status: DocStatus
|
||||
created_at: string
|
||||
updated_at: string
|
||||
track_id?: string
|
||||
chunks_count?: number
|
||||
error?: string
|
||||
error_msg?: string
|
||||
metadata?: Record<string, any>
|
||||
file_path: string
|
||||
}
|
||||
|
@ -161,6 +178,40 @@ export type DocsStatusesResponse = {
|
|||
statuses: Record<DocStatus, DocStatusResponse[]>
|
||||
}
|
||||
|
||||
export type TrackStatusResponse = {
|
||||
track_id: string
|
||||
documents: DocStatusResponse[]
|
||||
total_count: number
|
||||
status_summary: Record<string, number>
|
||||
}
|
||||
|
||||
export type DocumentsRequest = {
|
||||
status_filter?: DocStatus | null
|
||||
page: number
|
||||
page_size: number
|
||||
sort_field: 'created_at' | 'updated_at' | 'id' | 'file_path'
|
||||
sort_direction: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
export type PaginationInfo = {
|
||||
page: number
|
||||
page_size: number
|
||||
total_count: number
|
||||
total_pages: number
|
||||
has_next: boolean
|
||||
has_prev: boolean
|
||||
}
|
||||
|
||||
export type PaginatedDocsResponse = {
|
||||
documents: DocStatusResponse[]
|
||||
pagination: PaginationInfo
|
||||
status_counts: Record<string, number>
|
||||
}
|
||||
|
||||
export type StatusCountsResponse = {
|
||||
status_counts: Record<string, number>
|
||||
}
|
||||
|
||||
export type AuthStatusResponse = {
|
||||
auth_configured: boolean
|
||||
access_token?: string
|
||||
|
@ -284,7 +335,7 @@ export const getDocuments = async (): Promise<DocsStatusesResponse> => {
|
|||
return response.data
|
||||
}
|
||||
|
||||
export const scanNewDocuments = async (): Promise<{ status: string }> => {
|
||||
export const scanNewDocuments = async (): Promise<ScanResponse> => {
|
||||
const response = await axiosInstance.post('/documents/scan')
|
||||
return response.data
|
||||
}
|
||||
|
@ -680,3 +731,32 @@ export const checkEntityNameExists = async (entityName: string): Promise<boolean
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the processing status of documents by tracking ID
|
||||
* @param trackId The tracking ID returned from upload, text, or texts endpoints
|
||||
* @returns Promise with the track status response containing documents and summary
|
||||
*/
|
||||
export const getTrackStatus = async (trackId: string): Promise<TrackStatusResponse> => {
|
||||
const response = await axiosInstance.get(`/documents/track_status/${encodeURIComponent(trackId)}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documents with pagination support
|
||||
* @param request The pagination request parameters
|
||||
* @returns Promise with paginated documents response
|
||||
*/
|
||||
export const getDocumentsPaginated = async (request: DocumentsRequest): Promise<PaginatedDocsResponse> => {
|
||||
const response = await axiosInstance.post('/documents/paginated', request)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get counts of documents by status
|
||||
* @returns Promise with status counts response
|
||||
*/
|
||||
export const getDocumentStatusCounts = async (): Promise<StatusCountsResponse> => {
|
||||
const response = await axiosInstance.get('/documents/status_counts')
|
||||
return response.data
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { QueryMode, QueryRequest } from '@/api/lightrag'
|
||||
// Removed unused import for Text component
|
||||
import Checkbox from '@/components/ui/Checkbox'
|
||||
|
@ -15,6 +15,7 @@ import {
|
|||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RotateCcw } from 'lucide-react'
|
||||
|
||||
export default function QuerySettings() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -24,6 +25,42 @@ export default function QuerySettings() {
|
|||
useSettingsStore.getState().updateQuerySettings({ [key]: value })
|
||||
}, [])
|
||||
|
||||
// Default values for reset functionality
|
||||
const defaultValues = useMemo(() => ({
|
||||
mode: 'mix' as QueryMode,
|
||||
response_type: 'Multiple Paragraphs',
|
||||
top_k: 40,
|
||||
chunk_top_k: 10,
|
||||
max_entity_tokens: 10000,
|
||||
max_relation_tokens: 10000,
|
||||
max_total_tokens: 30000
|
||||
}), [])
|
||||
|
||||
const handleReset = useCallback((key: keyof typeof defaultValues) => {
|
||||
handleChange(key, defaultValues[key])
|
||||
}, [handleChange, defaultValues])
|
||||
|
||||
// Reset button component
|
||||
const ResetButton = ({ onClick, title }: { onClick: () => void; title: string }) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="mr-1 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
title={title}
|
||||
>
|
||||
<RotateCcw className="h-3 w-3 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
<p>{title}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
return (
|
||||
<Card className="flex shrink-0 flex-col min-w-[220px]">
|
||||
<CardHeader className="px-4 pt-4 pb-2">
|
||||
|
@ -32,7 +69,7 @@ export default function QuerySettings() {
|
|||
</CardHeader>
|
||||
<CardContent className="m-0 flex grow flex-col p-0 text-xs">
|
||||
<div className="relative size-full">
|
||||
<div className="absolute inset-0 flex flex-col gap-2 overflow-auto px-2">
|
||||
<div className="absolute inset-0 flex flex-col gap-2 overflow-auto px-2 pr-3">
|
||||
{/* Query Mode */}
|
||||
<>
|
||||
<TooltipProvider>
|
||||
|
@ -47,27 +84,33 @@ export default function QuerySettings() {
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Select
|
||||
value={querySettings.mode}
|
||||
onValueChange={(v) => handleChange('mode', v as QueryMode)}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="query_mode_select"
|
||||
className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0"
|
||||
<div className="flex items-center gap-1">
|
||||
<Select
|
||||
value={querySettings.mode}
|
||||
onValueChange={(v) => handleChange('mode', v as QueryMode)}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="naive">{t('retrievePanel.querySettings.queryModeOptions.naive')}</SelectItem>
|
||||
<SelectItem value="local">{t('retrievePanel.querySettings.queryModeOptions.local')}</SelectItem>
|
||||
<SelectItem value="global">{t('retrievePanel.querySettings.queryModeOptions.global')}</SelectItem>
|
||||
<SelectItem value="hybrid">{t('retrievePanel.querySettings.queryModeOptions.hybrid')}</SelectItem>
|
||||
<SelectItem value="mix">{t('retrievePanel.querySettings.queryModeOptions.mix')}</SelectItem>
|
||||
<SelectItem value="bypass">{t('retrievePanel.querySettings.queryModeOptions.bypass')}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<SelectTrigger
|
||||
id="query_mode_select"
|
||||
className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0 flex-1 text-left [&>span]:break-all [&>span]:line-clamp-1"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="naive">{t('retrievePanel.querySettings.queryModeOptions.naive')}</SelectItem>
|
||||
<SelectItem value="local">{t('retrievePanel.querySettings.queryModeOptions.local')}</SelectItem>
|
||||
<SelectItem value="global">{t('retrievePanel.querySettings.queryModeOptions.global')}</SelectItem>
|
||||
<SelectItem value="hybrid">{t('retrievePanel.querySettings.queryModeOptions.hybrid')}</SelectItem>
|
||||
<SelectItem value="mix">{t('retrievePanel.querySettings.queryModeOptions.mix')}</SelectItem>
|
||||
<SelectItem value="bypass">{t('retrievePanel.querySettings.queryModeOptions.bypass')}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<ResetButton
|
||||
onClick={() => handleReset('mode')}
|
||||
title="Reset to default (Mix)"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{/* Response Format */}
|
||||
|
@ -84,24 +127,30 @@ export default function QuerySettings() {
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Select
|
||||
value={querySettings.response_type}
|
||||
onValueChange={(v) => handleChange('response_type', v)}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="response_format_select"
|
||||
className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0"
|
||||
<div className="flex items-center gap-1">
|
||||
<Select
|
||||
value={querySettings.response_type}
|
||||
onValueChange={(v) => handleChange('response_type', v)}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="Multiple Paragraphs">{t('retrievePanel.querySettings.responseFormatOptions.multipleParagraphs')}</SelectItem>
|
||||
<SelectItem value="Single Paragraph">{t('retrievePanel.querySettings.responseFormatOptions.singleParagraph')}</SelectItem>
|
||||
<SelectItem value="Bullet Points">{t('retrievePanel.querySettings.responseFormatOptions.bulletPoints')}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<SelectTrigger
|
||||
id="response_format_select"
|
||||
className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0 flex-1 text-left [&>span]:break-all [&>span]:line-clamp-1"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="Multiple Paragraphs">{t('retrievePanel.querySettings.responseFormatOptions.multipleParagraphs')}</SelectItem>
|
||||
<SelectItem value="Single Paragraph">{t('retrievePanel.querySettings.responseFormatOptions.singleParagraph')}</SelectItem>
|
||||
<SelectItem value="Bullet Points">{t('retrievePanel.querySettings.responseFormatOptions.bulletPoints')}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<ResetButton
|
||||
onClick={() => handleReset('response_type')}
|
||||
title="Reset to default (Multiple Paragraphs)"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{/* Top K */}
|
||||
|
@ -118,7 +167,7 @@ export default function QuerySettings() {
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
id="top_k"
|
||||
type="number"
|
||||
|
@ -135,6 +184,11 @@ export default function QuerySettings() {
|
|||
}}
|
||||
min={1}
|
||||
placeholder={t('retrievePanel.querySettings.topKPlaceholder')}
|
||||
className="h-9 flex-1 pr-2 [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [-moz-appearance:textfield]"
|
||||
/>
|
||||
<ResetButton
|
||||
onClick={() => handleReset('top_k')}
|
||||
title="Reset to default"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
@ -153,7 +207,7 @@ export default function QuerySettings() {
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
id="chunk_top_k"
|
||||
type="number"
|
||||
|
@ -170,6 +224,11 @@ export default function QuerySettings() {
|
|||
}}
|
||||
min={1}
|
||||
placeholder={t('retrievePanel.querySettings.chunkTopKPlaceholder')}
|
||||
className="h-9 flex-1 pr-2 [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [-moz-appearance:textfield]"
|
||||
/>
|
||||
<ResetButton
|
||||
onClick={() => handleReset('chunk_top_k')}
|
||||
title="Reset to default"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
@ -188,7 +247,7 @@ export default function QuerySettings() {
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
id="max_entity_tokens"
|
||||
type="number"
|
||||
|
@ -205,6 +264,11 @@ export default function QuerySettings() {
|
|||
}}
|
||||
min={1}
|
||||
placeholder={t('retrievePanel.querySettings.maxEntityTokensPlaceholder')}
|
||||
className="h-9 flex-1 pr-2 [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [-moz-appearance:textfield]"
|
||||
/>
|
||||
<ResetButton
|
||||
onClick={() => handleReset('max_entity_tokens')}
|
||||
title="Reset to default"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
@ -223,7 +287,7 @@ export default function QuerySettings() {
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
id="max_relation_tokens"
|
||||
type="number"
|
||||
|
@ -240,6 +304,11 @@ export default function QuerySettings() {
|
|||
}}
|
||||
min={1}
|
||||
placeholder={t('retrievePanel.querySettings.maxRelationTokensPlaceholder')}
|
||||
className="h-9 flex-1 pr-2 [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [-moz-appearance:textfield]"
|
||||
/>
|
||||
<ResetButton
|
||||
onClick={() => handleReset('max_relation_tokens')}
|
||||
title="Reset to default"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
@ -258,7 +327,7 @@ export default function QuerySettings() {
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
id="max_total_tokens"
|
||||
type="number"
|
||||
|
@ -270,50 +339,20 @@ export default function QuerySettings() {
|
|||
onBlur={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === '' || isNaN(parseInt(value))) {
|
||||
handleChange('max_total_tokens', 1000)
|
||||
handleChange('max_total_tokens', 32000)
|
||||
}
|
||||
}}
|
||||
min={1}
|
||||
placeholder={t('retrievePanel.querySettings.maxTotalTokensPlaceholder')}
|
||||
className="h-9 flex-1 pr-2 [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [-moz-appearance:textfield]"
|
||||
/>
|
||||
<ResetButton
|
||||
onClick={() => handleReset('max_total_tokens')}
|
||||
title="Reset to default"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{/* History Turns */}
|
||||
<>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<label htmlFor="history_turns" className="ml-1 cursor-help">
|
||||
{t('retrievePanel.querySettings.historyTurns')}
|
||||
</label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
<p>{t('retrievePanel.querySettings.historyTurnsTooltip')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div>
|
||||
<Input
|
||||
id="history_turns"
|
||||
type="number"
|
||||
value={querySettings.history_turns ?? ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
handleChange('history_turns', value === '' ? '' : parseInt(value) || 0)
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === '' || isNaN(parseInt(value))) {
|
||||
handleChange('history_turns', 0)
|
||||
}
|
||||
}}
|
||||
min={0}
|
||||
placeholder={t('retrievePanel.querySettings.historyTurnsPlaceholder')}
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{/* User Prompt */}
|
||||
<>
|
||||
|
|
|
@ -10,38 +10,38 @@ const StatusCard = ({ status }: { status: LightragStatus | null }) => {
|
|||
return (
|
||||
<div className="min-w-[300px] space-y-2 text-xs">
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-medium">{t('graphPanel.statusCard.storageInfo')}</h4>
|
||||
<h4 className="font-medium">{t('graphPanel.statusCard.serverInfo')}</h4>
|
||||
<div className="text-foreground grid grid-cols-[160px_1fr] gap-1">
|
||||
<span>{t('graphPanel.statusCard.workingDirectory')}:</span>
|
||||
<span className="truncate">{status.working_directory}</span>
|
||||
<span>{t('graphPanel.statusCard.inputDirectory')}:</span>
|
||||
<span className="truncate">{status.input_directory}</span>
|
||||
<span>{t('graphPanel.statusCard.summarySettings')}:</span>
|
||||
<span>{status.configuration.summary_language} / LLM summary on {status.configuration.force_llm_summary_on_merge.toString()} fragments</span>
|
||||
<span>{t('graphPanel.statusCard.threshold')}:</span>
|
||||
<span>cosine {status.configuration.cosine_threshold} / rerank_score {status.configuration.min_rerank_score} / max_related {status.configuration.related_chunk_number}</span>
|
||||
<span>{t('graphPanel.statusCard.maxParallelInsert')}:</span>
|
||||
<span>{status.configuration.max_parallel_insert}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-medium">{t('graphPanel.statusCard.llmConfig')}</h4>
|
||||
<div className="text-foreground grid grid-cols-[160px_1fr] gap-1">
|
||||
<span>{t('graphPanel.statusCard.llmBinding')}:</span>
|
||||
<span>{status.configuration.llm_binding}</span>
|
||||
<span>{t('graphPanel.statusCard.llmBindingHost')}:</span>
|
||||
<span>{status.configuration.llm_binding_host}</span>
|
||||
<span>{t('graphPanel.statusCard.llmModel')}:</span>
|
||||
<span>{status.configuration.llm_model}</span>
|
||||
<span>{t('graphPanel.statusCard.maxTokens')}:</span>
|
||||
<span>{status.configuration.max_tokens}</span>
|
||||
<span>{status.configuration.llm_binding}: {status.configuration.llm_model} (#{status.configuration.max_async} Async)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-medium">{t('graphPanel.statusCard.embeddingConfig')}</h4>
|
||||
<div className="text-foreground grid grid-cols-[160px_1fr] gap-1">
|
||||
<span>{t('graphPanel.statusCard.embeddingBinding')}:</span>
|
||||
<span>{status.configuration.embedding_binding}</span>
|
||||
<span>{t('graphPanel.statusCard.embeddingBindingHost')}:</span>
|
||||
<span>{status.configuration.embedding_binding_host}</span>
|
||||
<span>{t('graphPanel.statusCard.embeddingModel')}:</span>
|
||||
<span>{status.configuration.embedding_model}</span>
|
||||
<span>{status.configuration.embedding_binding}: {status.configuration.embedding_model} (#{status.configuration.embedding_func_max_async} Async * {status.configuration.embedding_batch_num} batches)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from './Button'
|
||||
import Input from './Input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './Select'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon } from 'lucide-react'
|
||||
|
||||
export type PaginationControlsProps = {
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
pageSize: number
|
||||
totalCount: number
|
||||
onPageChange: (page: number) => void
|
||||
onPageSizeChange: (pageSize: number) => void
|
||||
isLoading?: boolean
|
||||
compact?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [
|
||||
{ value: 10, label: '10' },
|
||||
{ value: 20, label: '20' },
|
||||
{ value: 50, label: '50' },
|
||||
{ value: 100, label: '100' },
|
||||
{ value: 200, label: '200' }
|
||||
]
|
||||
|
||||
export default function PaginationControls({
|
||||
currentPage,
|
||||
totalPages,
|
||||
pageSize,
|
||||
totalCount,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
isLoading = false,
|
||||
compact = false,
|
||||
className
|
||||
}: PaginationControlsProps) {
|
||||
const { t } = useTranslation()
|
||||
const [inputPage, setInputPage] = useState(currentPage.toString())
|
||||
|
||||
// Update input when currentPage changes
|
||||
useEffect(() => {
|
||||
setInputPage(currentPage.toString())
|
||||
}, [currentPage])
|
||||
|
||||
// Handle page input change with debouncing
|
||||
const handlePageInputChange = useCallback((value: string) => {
|
||||
setInputPage(value)
|
||||
}, [])
|
||||
|
||||
// Handle page input submit
|
||||
const handlePageInputSubmit = useCallback(() => {
|
||||
const pageNum = parseInt(inputPage, 10)
|
||||
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) {
|
||||
onPageChange(pageNum)
|
||||
} else {
|
||||
// Reset to current page if invalid
|
||||
setInputPage(currentPage.toString())
|
||||
}
|
||||
}, [inputPage, totalPages, onPageChange, currentPage])
|
||||
|
||||
// Handle page input key press
|
||||
const handlePageInputKeyPress = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handlePageInputSubmit()
|
||||
}
|
||||
}, [handlePageInputSubmit])
|
||||
|
||||
// Handle page size change
|
||||
const handlePageSizeChange = useCallback((value: string) => {
|
||||
const newPageSize = parseInt(value, 10)
|
||||
if (!isNaN(newPageSize)) {
|
||||
onPageSizeChange(newPageSize)
|
||||
}
|
||||
}, [onPageSizeChange])
|
||||
|
||||
// Navigation handlers
|
||||
const goToFirstPage = useCallback(() => {
|
||||
if (currentPage > 1 && !isLoading) {
|
||||
onPageChange(1)
|
||||
}
|
||||
}, [currentPage, onPageChange, isLoading])
|
||||
|
||||
const goToPrevPage = useCallback(() => {
|
||||
if (currentPage > 1 && !isLoading) {
|
||||
onPageChange(currentPage - 1)
|
||||
}
|
||||
}, [currentPage, onPageChange, isLoading])
|
||||
|
||||
const goToNextPage = useCallback(() => {
|
||||
if (currentPage < totalPages && !isLoading) {
|
||||
onPageChange(currentPage + 1)
|
||||
}
|
||||
}, [currentPage, totalPages, onPageChange, isLoading])
|
||||
|
||||
const goToLastPage = useCallback(() => {
|
||||
if (currentPage < totalPages && !isLoading) {
|
||||
onPageChange(totalPages)
|
||||
}
|
||||
}, [currentPage, totalPages, onPageChange, isLoading])
|
||||
|
||||
if (totalPages <= 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className={cn('flex items-center gap-2', className)}>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={goToPrevPage}
|
||||
disabled={currentPage <= 1 || isLoading}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
type="text"
|
||||
value={inputPage}
|
||||
onChange={(e) => handlePageInputChange(e.target.value)}
|
||||
onBlur={handlePageInputSubmit}
|
||||
onKeyPress={handlePageInputKeyPress}
|
||||
disabled={isLoading}
|
||||
className="h-8 w-12 text-center text-sm"
|
||||
/>
|
||||
<span className="text-sm text-gray-500">/ {totalPages}</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={goToNextPage}
|
||||
disabled={currentPage >= totalPages || isLoading}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
value={pageSize.toString()}
|
||||
onValueChange={handlePageSizeChange}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-16">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PAGE_SIZE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center justify-between gap-4', className)}>
|
||||
<div className="text-sm text-gray-500">
|
||||
{t('pagination.showing', {
|
||||
start: Math.min((currentPage - 1) * pageSize + 1, totalCount),
|
||||
end: Math.min(currentPage * pageSize, totalCount),
|
||||
total: totalCount
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={goToFirstPage}
|
||||
disabled={currentPage <= 1 || isLoading}
|
||||
className="h-8 w-8 p-0"
|
||||
tooltip={t('pagination.firstPage')}
|
||||
>
|
||||
<ChevronsLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={goToPrevPage}
|
||||
disabled={currentPage <= 1 || isLoading}
|
||||
className="h-8 w-8 p-0"
|
||||
tooltip={t('pagination.prevPage')}
|
||||
>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm">{t('pagination.page')}</span>
|
||||
<Input
|
||||
type="text"
|
||||
value={inputPage}
|
||||
onChange={(e) => handlePageInputChange(e.target.value)}
|
||||
onBlur={handlePageInputSubmit}
|
||||
onKeyPress={handlePageInputKeyPress}
|
||||
disabled={isLoading}
|
||||
className="h-8 w-16 text-center text-sm"
|
||||
/>
|
||||
<span className="text-sm">/ {totalPages}</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={goToNextPage}
|
||||
disabled={currentPage >= totalPages || isLoading}
|
||||
className="h-8 w-8 p-0"
|
||||
tooltip={t('pagination.nextPage')}
|
||||
>
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={goToLastPage}
|
||||
disabled={currentPage >= totalPages || isLoading}
|
||||
className="h-8 w-8 p-0"
|
||||
tooltip={t('pagination.lastPage')}
|
||||
>
|
||||
<ChevronsRightIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">{t('pagination.pageSize')}</span>
|
||||
<Select
|
||||
value={pageSize.toString()}
|
||||
onValueChange={handlePageSizeChange}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-16">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PAGE_SIZE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -18,13 +18,22 @@ import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog'
|
|||
import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog'
|
||||
import DeleteDocumentsDialog from '@/components/documents/DeleteDocumentsDialog'
|
||||
import DeselectDocumentsDialog from '@/components/documents/DeselectDocumentsDialog'
|
||||
import PaginationControls from '@/components/ui/PaginationControls'
|
||||
|
||||
import { getDocuments, scanNewDocuments, DocsStatusesResponse, DocStatus, DocStatusResponse } from '@/api/lightrag'
|
||||
import {
|
||||
scanNewDocuments,
|
||||
getDocumentsPaginated,
|
||||
DocsStatusesResponse,
|
||||
DocStatus,
|
||||
DocStatusResponse,
|
||||
DocumentsRequest,
|
||||
PaginationInfo
|
||||
} from '@/api/lightrag'
|
||||
import { errorMessage } from '@/lib/utils'
|
||||
import { toast } from 'sonner'
|
||||
import { useBackendState } from '@/stores/state'
|
||||
|
||||
import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon, FilterIcon } from 'lucide-react'
|
||||
import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon, RotateCcwIcon } from 'lucide-react'
|
||||
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
|
||||
|
||||
type StatusFilter = DocStatus | 'all';
|
||||
|
@ -136,7 +145,7 @@ const pulseStyle = `
|
|||
`;
|
||||
|
||||
// Type definitions for sort field and direction
|
||||
type SortField = 'created_at' | 'updated_at' | 'id';
|
||||
type SortField = 'created_at' | 'updated_at' | 'id' | 'file_path';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export default function DocumentManager() {
|
||||
|
@ -164,10 +173,28 @@ export default function DocumentManager() {
|
|||
const { t, i18n } = useTranslation()
|
||||
const health = useBackendState.use.health()
|
||||
const pipelineBusy = useBackendState.use.pipelineBusy()
|
||||
|
||||
// Legacy state for backward compatibility
|
||||
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
||||
|
||||
const currentTab = useSettingsStore.use.currentTab()
|
||||
const showFileName = useSettingsStore.use.showFileName()
|
||||
const setShowFileName = useSettingsStore.use.setShowFileName()
|
||||
const documentsPageSize = useSettingsStore.use.documentsPageSize()
|
||||
const setDocumentsPageSize = useSettingsStore.use.setDocumentsPageSize()
|
||||
|
||||
// New pagination state
|
||||
const [, setCurrentPageDocs] = useState<DocStatusResponse[]>([])
|
||||
const [pagination, setPagination] = useState<PaginationInfo>({
|
||||
page: 1,
|
||||
page_size: documentsPageSize,
|
||||
total_count: 0,
|
||||
total_pages: 0,
|
||||
has_next: false,
|
||||
has_prev: false
|
||||
})
|
||||
const [statusCounts, setStatusCounts] = useState<Record<string, number>>({ all: 0 })
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
|
||||
// Sort state
|
||||
const [sortField, setSortField] = useState<SortField>('updated_at')
|
||||
|
@ -176,6 +203,15 @@ export default function DocumentManager() {
|
|||
// State for document status filter
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
|
||||
|
||||
// State to store page number for each status filter
|
||||
const [pageByStatus, setPageByStatus] = useState<Record<StatusFilter, number>>({
|
||||
all: 1,
|
||||
processed: 1,
|
||||
processing: 1,
|
||||
pending: 1,
|
||||
failed: 1,
|
||||
});
|
||||
|
||||
// State for document selection
|
||||
const [selectedDocIds, setSelectedDocIds] = useState<string[]>([])
|
||||
const isSelectionMode = selectedDocIds.length > 0
|
||||
|
@ -198,15 +234,30 @@ export default function DocumentManager() {
|
|||
|
||||
// Handle sort column click
|
||||
const handleSort = (field: SortField) => {
|
||||
if (sortField === field) {
|
||||
// Toggle sort direction if clicking the same field
|
||||
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc')
|
||||
} else {
|
||||
// Set new sort field with default desc direction
|
||||
setSortField(field)
|
||||
setSortDirection('desc')
|
||||
let actualField = field;
|
||||
|
||||
// When clicking the first column, determine the actual sort field based on showFileName
|
||||
if (field === 'id') {
|
||||
actualField = showFileName ? 'file_path' : 'id';
|
||||
}
|
||||
}
|
||||
|
||||
const newDirection = (sortField === actualField && sortDirection === 'desc') ? 'asc' : 'desc';
|
||||
|
||||
setSortField(actualField);
|
||||
setSortDirection(newDirection);
|
||||
|
||||
// Reset page to 1 when sorting changes
|
||||
setPagination(prev => ({ ...prev, page: 1 }));
|
||||
|
||||
// Reset all status filters' page memory since sorting affects all
|
||||
setPageByStatus({
|
||||
all: 1,
|
||||
processed: 1,
|
||||
processing: 1,
|
||||
pending: 1,
|
||||
failed: 1,
|
||||
});
|
||||
};
|
||||
|
||||
// Sort documents based on current sort field and direction
|
||||
const sortDocuments = useCallback((documents: DocStatusResponse[]) => {
|
||||
|
@ -373,74 +424,84 @@ export default function DocumentManager() {
|
|||
};
|
||||
}, [docs]);
|
||||
|
||||
const fetchDocuments = useCallback(async () => {
|
||||
// New paginated data fetching function
|
||||
const fetchPaginatedDocuments = useCallback(async (
|
||||
page: number,
|
||||
pageSize: number,
|
||||
statusFilter: StatusFilter
|
||||
) => {
|
||||
try {
|
||||
// Check if component is still mounted before starting the request
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
const docs = await getDocuments();
|
||||
setIsRefreshing(true);
|
||||
|
||||
// Prepare request parameters
|
||||
const request: DocumentsRequest = {
|
||||
status_filter: statusFilter === 'all' ? null : statusFilter,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
sort_field: sortField,
|
||||
sort_direction: sortDirection
|
||||
};
|
||||
|
||||
const response = await getDocumentsPaginated(request);
|
||||
|
||||
// Check again if component is still mounted after the request completes
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// Only update state if component is still mounted
|
||||
if (isMountedRef.current) {
|
||||
// Update docs state
|
||||
if (docs && docs.statuses) {
|
||||
const numDocuments = Object.values(docs.statuses).reduce(
|
||||
(acc, status) => acc + status.length,
|
||||
0
|
||||
)
|
||||
if (numDocuments > 0) {
|
||||
setDocs(docs)
|
||||
} else {
|
||||
setDocs(null)
|
||||
}
|
||||
} else {
|
||||
setDocs(null)
|
||||
// Update pagination state
|
||||
setPagination(response.pagination);
|
||||
setCurrentPageDocs(response.documents);
|
||||
setStatusCounts(response.status_counts);
|
||||
|
||||
// Update legacy docs state for backward compatibility
|
||||
const legacyDocs: DocsStatusesResponse = {
|
||||
statuses: {
|
||||
processed: response.documents.filter(doc => doc.status === 'processed'),
|
||||
processing: response.documents.filter(doc => doc.status === 'processing'),
|
||||
pending: response.documents.filter(doc => doc.status === 'pending'),
|
||||
failed: response.documents.filter(doc => doc.status === 'failed')
|
||||
}
|
||||
};
|
||||
|
||||
if (response.pagination.total_count > 0) {
|
||||
setDocs(legacyDocs);
|
||||
} else {
|
||||
setDocs(null);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
// Only show error if component is still mounted
|
||||
if (isMountedRef.current) {
|
||||
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }))
|
||||
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }));
|
||||
}
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}
|
||||
}, [setDocs, t])
|
||||
}, [sortField, sortDirection, t]);
|
||||
|
||||
// Fetch documents when the tab becomes visible
|
||||
useEffect(() => {
|
||||
if (currentTab === 'documents') {
|
||||
fetchDocuments()
|
||||
// Legacy fetchDocuments function for backward compatibility
|
||||
const fetchDocuments = useCallback(async () => {
|
||||
await fetchPaginatedDocuments(pagination.page, pagination.page_size, statusFilter);
|
||||
}, [fetchPaginatedDocuments, pagination.page, pagination.page_size, statusFilter]);
|
||||
|
||||
// Add refs to track previous pipelineBusy state and current interval
|
||||
const prevPipelineBusyRef = useRef<boolean | undefined>(undefined);
|
||||
const pollingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// Function to clear current polling interval
|
||||
const clearPollingInterval = useCallback(() => {
|
||||
if (pollingIntervalRef.current) {
|
||||
clearInterval(pollingIntervalRef.current);
|
||||
pollingIntervalRef.current = null;
|
||||
}
|
||||
}, [currentTab, fetchDocuments])
|
||||
}, []);
|
||||
|
||||
const scanDocuments = useCallback(async () => {
|
||||
try {
|
||||
// Check if component is still mounted before starting the request
|
||||
if (!isMountedRef.current) return;
|
||||
// Function to start polling with given interval
|
||||
const startPollingInterval = useCallback((intervalMs: number) => {
|
||||
clearPollingInterval();
|
||||
|
||||
const { status } = await scanNewDocuments();
|
||||
|
||||
// Check again if component is still mounted after the request completes
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
toast.message(status);
|
||||
} catch (err) {
|
||||
// Only show error if component is still mounted
|
||||
if (isMountedRef.current) {
|
||||
toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) }));
|
||||
}
|
||||
}
|
||||
}, [t])
|
||||
|
||||
// Set up polling when the documents tab is active and health is good
|
||||
useEffect(() => {
|
||||
if (currentTab !== 'documents' || !health) {
|
||||
return
|
||||
}
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
pollingIntervalRef.current = setInterval(async () => {
|
||||
try {
|
||||
// Only perform fetch if component is still mounted
|
||||
if (isMountedRef.current) {
|
||||
|
@ -452,12 +513,145 @@ export default function DocumentManager() {
|
|||
toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
|
||||
}
|
||||
}
|
||||
}, 5000)
|
||||
}, intervalMs);
|
||||
}, [fetchDocuments, t, clearPollingInterval]);
|
||||
|
||||
const scanDocuments = useCallback(async () => {
|
||||
try {
|
||||
// Check if component is still mounted before starting the request
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
const { status, message, track_id: _track_id } = await scanNewDocuments(); // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
|
||||
// Check again if component is still mounted after the request completes
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// Note: _track_id is available for future use (e.g., progress tracking)
|
||||
toast.message(message || status);
|
||||
|
||||
// Reset health check timer with 1 second delay to avoid race condition
|
||||
useBackendState.getState().resetHealthCheckTimerDelayed(1000);
|
||||
|
||||
// Schedule a health check 2 seconds after successful scan
|
||||
startPollingInterval(2000);
|
||||
} catch (err) {
|
||||
// Only show error if component is still mounted
|
||||
if (isMountedRef.current) {
|
||||
toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) }));
|
||||
}
|
||||
}
|
||||
}, [t, startPollingInterval])
|
||||
|
||||
// Handle page size change - update state and save to store
|
||||
const handlePageSizeChange = useCallback((newPageSize: number) => {
|
||||
if (newPageSize === pagination.page_size) return;
|
||||
|
||||
// Save the new page size to the store
|
||||
setDocumentsPageSize(newPageSize);
|
||||
|
||||
// Reset all status filters to page 1 when page size changes
|
||||
setPageByStatus({
|
||||
all: 1,
|
||||
processed: 1,
|
||||
processing: 1,
|
||||
pending: 1,
|
||||
failed: 1,
|
||||
});
|
||||
|
||||
setPagination(prev => ({ ...prev, page: 1, page_size: newPageSize }));
|
||||
}, [pagination.page_size, setDocumentsPageSize]);
|
||||
|
||||
// Handle manual refresh with pagination reset logic
|
||||
const handleManualRefresh = useCallback(async () => {
|
||||
try {
|
||||
setIsRefreshing(true);
|
||||
|
||||
// Fetch documents from the first page
|
||||
const request: DocumentsRequest = {
|
||||
status_filter: statusFilter === 'all' ? null : statusFilter,
|
||||
page: 1,
|
||||
page_size: pagination.page_size,
|
||||
sort_field: sortField,
|
||||
sort_direction: sortDirection
|
||||
};
|
||||
|
||||
const response = await getDocumentsPaginated(request);
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// Check if total count is less than current page size and page size is not already 10
|
||||
if (response.pagination.total_count < pagination.page_size && pagination.page_size !== 10) {
|
||||
// Reset page size to 10 which will trigger a new fetch
|
||||
handlePageSizeChange(10);
|
||||
} else {
|
||||
// Update pagination state
|
||||
setPagination(response.pagination);
|
||||
setCurrentPageDocs(response.documents);
|
||||
setStatusCounts(response.status_counts);
|
||||
|
||||
// Update legacy docs state for backward compatibility
|
||||
const legacyDocs: DocsStatusesResponse = {
|
||||
statuses: {
|
||||
processed: response.documents.filter(doc => doc.status === 'processed'),
|
||||
processing: response.documents.filter(doc => doc.status === 'processing'),
|
||||
pending: response.documents.filter(doc => doc.status === 'pending'),
|
||||
failed: response.documents.filter(doc => doc.status === 'failed')
|
||||
}
|
||||
};
|
||||
|
||||
if (response.pagination.total_count > 0) {
|
||||
setDocs(legacyDocs);
|
||||
} else {
|
||||
setDocs(null);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
if (isMountedRef.current) {
|
||||
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }));
|
||||
}
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}
|
||||
}, [statusFilter, pagination.page_size, sortField, sortDirection, handlePageSizeChange, t]);
|
||||
|
||||
// Monitor pipelineBusy changes and trigger immediate refresh with timer reset
|
||||
useEffect(() => {
|
||||
// Skip the first render when prevPipelineBusyRef is undefined
|
||||
if (prevPipelineBusyRef.current !== undefined && prevPipelineBusyRef.current !== pipelineBusy) {
|
||||
// pipelineBusy state has changed, trigger immediate refresh
|
||||
if (currentTab === 'documents' && health && isMountedRef.current) {
|
||||
handleManualRefresh();
|
||||
|
||||
// Reset polling timer after manual refresh
|
||||
const hasActiveDocuments = (statusCounts.processing || 0) > 0 || (statusCounts.pending || 0) > 0;
|
||||
const pollingInterval = hasActiveDocuments ? 5000 : 30000;
|
||||
startPollingInterval(pollingInterval);
|
||||
}
|
||||
}
|
||||
// Update the previous state
|
||||
prevPipelineBusyRef.current = pipelineBusy;
|
||||
}, [pipelineBusy, currentTab, health, handleManualRefresh, statusCounts.processing, statusCounts.pending, startPollingInterval]);
|
||||
|
||||
// Set up intelligent polling with dynamic interval based on document status
|
||||
useEffect(() => {
|
||||
if (currentTab !== 'documents' || !health) {
|
||||
clearPollingInterval();
|
||||
return
|
||||
}
|
||||
|
||||
// Determine polling interval based on document status
|
||||
const hasActiveDocuments = (statusCounts.processing || 0) > 0 || (statusCounts.pending || 0) > 0;
|
||||
const pollingInterval = hasActiveDocuments ? 5000 : 30000; // 5s if active, 30s if idle
|
||||
|
||||
startPollingInterval(pollingInterval);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
clearPollingInterval();
|
||||
}
|
||||
}, [health, fetchDocuments, t, currentTab])
|
||||
}, [health, t, currentTab, statusCounts, startPollingInterval, clearPollingInterval])
|
||||
|
||||
// Monitor docs changes to check status counts and trigger health check if needed
|
||||
useEffect(() => {
|
||||
|
@ -485,16 +679,97 @@ export default function DocumentManager() {
|
|||
prevStatusCounts.current = newStatusCounts
|
||||
}, [docs]);
|
||||
|
||||
// Handle page change - only update state
|
||||
const handlePageChange = useCallback((newPage: number) => {
|
||||
if (newPage === pagination.page) return;
|
||||
|
||||
// Save the new page for current status filter
|
||||
setPageByStatus(prev => ({ ...prev, [statusFilter]: newPage }));
|
||||
setPagination(prev => ({ ...prev, page: newPage }));
|
||||
}, [pagination.page, statusFilter]);
|
||||
|
||||
// Handle status filter change - only update state
|
||||
const handleStatusFilterChange = useCallback((newStatusFilter: StatusFilter) => {
|
||||
if (newStatusFilter === statusFilter) return;
|
||||
|
||||
// Save current page for the current status filter
|
||||
setPageByStatus(prev => ({ ...prev, [statusFilter]: pagination.page }));
|
||||
|
||||
// Get the saved page for the new status filter
|
||||
const newPage = pageByStatus[newStatusFilter];
|
||||
|
||||
// Update status filter and restore the saved page
|
||||
setStatusFilter(newStatusFilter);
|
||||
setPagination(prev => ({ ...prev, page: newPage }));
|
||||
}, [statusFilter, pagination.page, pageByStatus]);
|
||||
|
||||
// Handle documents deleted callback
|
||||
const handleDocumentsDeleted = useCallback(async () => {
|
||||
setSelectedDocIds([])
|
||||
await fetchDocuments()
|
||||
}, [fetchDocuments])
|
||||
|
||||
// Add dependency on sort state to re-render when sort changes
|
||||
// Reset health check timer with 1 second delay to avoid race condition
|
||||
useBackendState.getState().resetHealthCheckTimerDelayed(1000)
|
||||
|
||||
// Schedule a health check 2 seconds after successful clear
|
||||
startPollingInterval(2000)
|
||||
}, [startPollingInterval])
|
||||
|
||||
// Handle documents cleared callback with proper interval reset
|
||||
const handleDocumentsCleared = useCallback(async () => {
|
||||
// Clear current polling interval
|
||||
clearPollingInterval();
|
||||
|
||||
// Reset status counts to ensure proper state
|
||||
setStatusCounts({
|
||||
all: 0,
|
||||
processed: 0,
|
||||
processing: 0,
|
||||
pending: 0,
|
||||
failed: 0
|
||||
});
|
||||
|
||||
// Perform one immediate refresh to confirm clear operation
|
||||
if (isMountedRef.current) {
|
||||
try {
|
||||
await fetchDocuments();
|
||||
} catch (err) {
|
||||
console.error('Error fetching documents after clear:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Set appropriate polling interval based on current state
|
||||
// Since documents are cleared, use idle interval (30 seconds)
|
||||
if (currentTab === 'documents' && health && isMountedRef.current) {
|
||||
startPollingInterval(30000); // 30 seconds for idle state
|
||||
}
|
||||
}, [clearPollingInterval, setStatusCounts, fetchDocuments, currentTab, health, startPollingInterval])
|
||||
|
||||
|
||||
// Handle showFileName change - switch sort field if currently sorting by first column
|
||||
useEffect(() => {
|
||||
// This effect ensures the component re-renders when sort state changes
|
||||
}, [sortField, sortDirection]);
|
||||
// Only switch if currently sorting by the first column (id or file_path)
|
||||
if (sortField === 'id' || sortField === 'file_path') {
|
||||
const newSortField = showFileName ? 'file_path' : 'id';
|
||||
if (sortField !== newSortField) {
|
||||
setSortField(newSortField);
|
||||
}
|
||||
}
|
||||
}, [showFileName, sortField]);
|
||||
|
||||
// Central effect to handle all data fetching
|
||||
useEffect(() => {
|
||||
if (currentTab === 'documents') {
|
||||
fetchPaginatedDocuments(pagination.page, pagination.page_size, statusFilter);
|
||||
}
|
||||
}, [
|
||||
currentTab,
|
||||
pagination.page,
|
||||
pagination.page_size,
|
||||
statusFilter,
|
||||
sortField,
|
||||
sortDirection,
|
||||
fetchPaginatedDocuments
|
||||
]);
|
||||
|
||||
return (
|
||||
<Card className="!rounded-none !overflow-hidden flex flex-col h-full min-h-0">
|
||||
|
@ -502,7 +777,7 @@ export default function DocumentManager() {
|
|||
<CardTitle className="text-lg">{t('documentPanel.documentManager.title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 flex flex-col min-h-0 overflow-auto">
|
||||
<div className="flex gap-2 mb-2">
|
||||
<div className="flex justify-between items-center gap-2 mb-2">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
@ -526,27 +801,43 @@ export default function DocumentManager() {
|
|||
<ActivityIcon /> {t('documentPanel.documentManager.pipelineStatusButton')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
{isSelectionMode && (
|
||||
<DeleteDocumentsDialog
|
||||
selectedDocIds={selectedDocIds}
|
||||
totalCompletedCount={documentCounts.processed || 0}
|
||||
onDocumentsDeleted={handleDocumentsDeleted}
|
||||
|
||||
{/* Pagination Controls in the middle */}
|
||||
{pagination.total_pages > 1 && (
|
||||
<PaginationControls
|
||||
currentPage={pagination.page}
|
||||
totalPages={pagination.total_pages}
|
||||
pageSize={pagination.page_size}
|
||||
totalCount={pagination.total_count}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
isLoading={isRefreshing}
|
||||
compact={true}
|
||||
/>
|
||||
)}
|
||||
{isSelectionMode ? (
|
||||
<DeselectDocumentsDialog
|
||||
selectedCount={selectedDocIds.length}
|
||||
onDeselect={handleDeselectAll}
|
||||
|
||||
<div className="flex gap-2">
|
||||
{isSelectionMode && (
|
||||
<DeleteDocumentsDialog
|
||||
selectedDocIds={selectedDocIds}
|
||||
totalCompletedCount={documentCounts.processed || 0}
|
||||
onDocumentsDeleted={handleDocumentsDeleted}
|
||||
/>
|
||||
)}
|
||||
{isSelectionMode ? (
|
||||
<DeselectDocumentsDialog
|
||||
selectedCount={selectedDocIds.length}
|
||||
onDeselect={handleDeselectAll}
|
||||
/>
|
||||
) : (
|
||||
<ClearDocumentsDialog onDocumentsCleared={handleDocumentsCleared} />
|
||||
)}
|
||||
<UploadDocumentsDialog onDocumentsUploaded={fetchDocuments} />
|
||||
<PipelineStatusDialog
|
||||
open={showPipelineStatus}
|
||||
onOpenChange={setShowPipelineStatus}
|
||||
/>
|
||||
) : (
|
||||
<ClearDocumentsDialog onDocumentsCleared={fetchDocuments} />
|
||||
)}
|
||||
<UploadDocumentsDialog onDocumentsUploaded={fetchDocuments} />
|
||||
<PipelineStatusDialog
|
||||
open={showPipelineStatus}
|
||||
onOpenChange={setShowPipelineStatus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="flex-1 flex flex-col border rounded-md min-h-0 mb-2">
|
||||
|
@ -554,63 +845,77 @@ export default function DocumentManager() {
|
|||
<div className="flex justify-between items-center">
|
||||
<CardTitle>{t('documentPanel.documentManager.uploadedTitle')}</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<FilterIcon className="h-4 w-4" />
|
||||
<div className="flex gap-1" dir={i18n.dir()}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={statusFilter === 'all' ? 'secondary' : 'outline'}
|
||||
onClick={() => setStatusFilter('all')}
|
||||
onClick={() => handleStatusFilterChange('all')}
|
||||
disabled={isRefreshing}
|
||||
className={cn(
|
||||
statusFilter === 'all' && 'bg-gray-100 dark:bg-gray-900 font-medium border border-gray-400 dark:border-gray-500 shadow-sm'
|
||||
)}
|
||||
>
|
||||
{t('documentPanel.documentManager.status.all')} ({documentCounts.all})
|
||||
{t('documentPanel.documentManager.status.all')} ({statusCounts.all || documentCounts.all})
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={statusFilter === 'processed' ? 'secondary' : 'outline'}
|
||||
onClick={() => setStatusFilter('processed')}
|
||||
onClick={() => handleStatusFilterChange('processed')}
|
||||
disabled={isRefreshing}
|
||||
className={cn(
|
||||
documentCounts.processed > 0 ? 'text-green-600' : 'text-gray-500',
|
||||
(statusCounts.PROCESSED || statusCounts.processed || documentCounts.processed) > 0 ? 'text-green-600' : 'text-gray-500',
|
||||
statusFilter === 'processed' && 'bg-green-100 dark:bg-green-900/30 font-medium border border-green-400 dark:border-green-600 shadow-sm'
|
||||
)}
|
||||
>
|
||||
{t('documentPanel.documentManager.status.completed')} ({documentCounts.processed || 0})
|
||||
{t('documentPanel.documentManager.status.completed')} ({statusCounts.PROCESSED || statusCounts.processed || 0})
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={statusFilter === 'processing' ? 'secondary' : 'outline'}
|
||||
onClick={() => setStatusFilter('processing')}
|
||||
onClick={() => handleStatusFilterChange('processing')}
|
||||
disabled={isRefreshing}
|
||||
className={cn(
|
||||
documentCounts.processing > 0 ? 'text-blue-600' : 'text-gray-500',
|
||||
(statusCounts.PROCESSING || statusCounts.processing || documentCounts.processing) > 0 ? 'text-blue-600' : 'text-gray-500',
|
||||
statusFilter === 'processing' && 'bg-blue-100 dark:bg-blue-900/30 font-medium border border-blue-400 dark:border-blue-600 shadow-sm'
|
||||
)}
|
||||
>
|
||||
{t('documentPanel.documentManager.status.processing')} ({documentCounts.processing || 0})
|
||||
{t('documentPanel.documentManager.status.processing')} ({statusCounts.PROCESSING || statusCounts.processing || 0})
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={statusFilter === 'pending' ? 'secondary' : 'outline'}
|
||||
onClick={() => setStatusFilter('pending')}
|
||||
onClick={() => handleStatusFilterChange('pending')}
|
||||
disabled={isRefreshing}
|
||||
className={cn(
|
||||
documentCounts.pending > 0 ? 'text-yellow-600' : 'text-gray-500',
|
||||
(statusCounts.PENDING || statusCounts.pending || documentCounts.pending) > 0 ? 'text-yellow-600' : 'text-gray-500',
|
||||
statusFilter === 'pending' && 'bg-yellow-100 dark:bg-yellow-900/30 font-medium border border-yellow-400 dark:border-yellow-600 shadow-sm'
|
||||
)}
|
||||
>
|
||||
{t('documentPanel.documentManager.status.pending')} ({documentCounts.pending || 0})
|
||||
{t('documentPanel.documentManager.status.pending')} ({statusCounts.PENDING || statusCounts.pending || 0})
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={statusFilter === 'failed' ? 'secondary' : 'outline'}
|
||||
onClick={() => setStatusFilter('failed')}
|
||||
onClick={() => handleStatusFilterChange('failed')}
|
||||
disabled={isRefreshing}
|
||||
className={cn(
|
||||
documentCounts.failed > 0 ? 'text-red-600' : 'text-gray-500',
|
||||
(statusCounts.FAILED || statusCounts.failed || documentCounts.failed) > 0 ? 'text-red-600' : 'text-gray-500',
|
||||
statusFilter === 'failed' && 'bg-red-100 dark:bg-red-900/30 font-medium border border-red-400 dark:border-red-600 shadow-sm'
|
||||
)}
|
||||
>
|
||||
{t('documentPanel.documentManager.status.failed')} ({documentCounts.failed || 0})
|
||||
{t('documentPanel.documentManager.status.failed')} ({statusCounts.FAILED || statusCounts.failed || 0})
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleManualRefresh}
|
||||
disabled={isRefreshing}
|
||||
side="bottom"
|
||||
tooltip={t('documentPanel.documentManager.refreshTooltip')}
|
||||
>
|
||||
<RotateCcwIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label
|
||||
|
@ -656,8 +961,11 @@ export default function DocumentManager() {
|
|||
className="cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-800 select-none"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{t('documentPanel.documentManager.columns.id')}
|
||||
{sortField === 'id' && (
|
||||
{showFileName
|
||||
? t('documentPanel.documentManager.columns.fileName')
|
||||
: t('documentPanel.documentManager.columns.id')
|
||||
}
|
||||
{((sortField === 'id' && !showFileName) || (sortField === 'file_path' && showFileName)) && (
|
||||
<span className="ml-1">
|
||||
{sortDirection === 'asc' ? <ArrowUpIcon size={14} /> : <ArrowDownIcon size={14} />}
|
||||
</span>
|
||||
|
@ -749,8 +1057,8 @@ export default function DocumentManager() {
|
|||
{doc.status === 'failed' && (
|
||||
<span className="text-red-600">{t('documentPanel.documentManager.status.failed')}</span>
|
||||
)}
|
||||
{doc.error && (
|
||||
<span className="ml-2 text-red-500" title={doc.error}>
|
||||
{doc.error_msg && (
|
||||
<span className="ml-2 text-red-500" title={doc.error_msg}>
|
||||
⚠️
|
||||
</span>
|
||||
)}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Settings as SigmaSettings } from 'sigma/settings'
|
|||
import { GraphSearchOption, OptionItem } from '@react-sigma/graph-search'
|
||||
import { EdgeArrowProgram, NodePointProgram, NodeCircleProgram } from 'sigma/rendering'
|
||||
import { NodeBorderProgram } from '@sigma/node-border'
|
||||
import EdgeCurveProgram, { EdgeCurvedArrowProgram } from '@sigma/edge-curve'
|
||||
import { EdgeCurvedArrowProgram, createEdgeCurveProgram } from '@sigma/edge-curve'
|
||||
|
||||
import FocusOnNode from '@/components/graph/FocusOnNode'
|
||||
import LayoutsControl from '@/components/graph/LayoutsControl'
|
||||
|
@ -36,7 +36,7 @@ const defaultSigmaSettings: Partial<SigmaSettings> = {
|
|||
edgeProgramClasses: {
|
||||
arrow: EdgeArrowProgram,
|
||||
curvedArrow: EdgeCurvedArrowProgram,
|
||||
curvedNoArrow: EdgeCurveProgram
|
||||
curvedNoArrow: createEdgeCurveProgram()
|
||||
},
|
||||
nodeProgramClasses: {
|
||||
default: NodeBorderProgram,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue