Simplified the api services issue #565
Browse files- README.md +54 -75
- lightrag/api/azure_openai_lightrag_server.py +0 -532
- lightrag/api/{lollms_lightrag_server.py → lightrag_server.py} +79 -15
- lightrag/api/ollama_lightrag_server.py +0 -491
- lightrag/api/openai_lightrag_server.py +0 -506
- setup.py +1 -4
README.md
CHANGED
@@ -912,12 +912,14 @@ pip install -e ".[api]"
|
|
912 |
|
913 |
### Prerequisites
|
914 |
|
915 |
-
Before running any of the servers, ensure you have the corresponding backend service running
|
|
|
|
|
916 |
|
917 |
#### For LoLLMs Server
|
918 |
- LoLLMs must be running and accessible
|
919 |
- Default connection: http://localhost:9600
|
920 |
-
- Configure using --
|
921 |
|
922 |
#### For Ollama Server
|
923 |
- Ollama must be running and accessible
|
@@ -953,15 +955,19 @@ The output of the last command will give you the endpoint and the key for the Op
|
|
953 |
|
954 |
Each server has its own specific configuration options:
|
955 |
|
956 |
-
####
|
957 |
|
958 |
| Parameter | Default | Description |
|
959 |
|-----------|---------|-------------|
|
960 |
| --host | 0.0.0.0 | RAG server host |
|
961 |
| --port | 9621 | RAG server port |
|
|
|
|
|
962 |
| --model | mistral-nemo:latest | LLM model name |
|
|
|
|
|
963 |
| --embedding-model | bge-m3:latest | Embedding model name |
|
964 |
-
| --
|
965 |
| --working-dir | ./rag_storage | Working directory for RAG |
|
966 |
| --max-async | 4 | Maximum async operations |
|
967 |
| --max-tokens | 32768 | Maximum token size |
|
@@ -971,95 +977,71 @@ Each server has its own specific configuration options:
|
|
971 |
| --log-level | INFO | Logging level |
|
972 |
| --key | none | Access Key to protect the lightrag service |
|
973 |
|
974 |
-
#### Ollama Server Options
|
975 |
|
976 |
-
| Parameter | Default | Description |
|
977 |
-
|-----------|---------|-------------|
|
978 |
-
| --host | 0.0.0.0 | RAG server host |
|
979 |
-
| --port | 9621 | RAG server port |
|
980 |
-
| --model | mistral-nemo:latest | LLM model name |
|
981 |
-
| --embedding-model | bge-m3:latest | Embedding model name |
|
982 |
-
| --ollama-host | http://localhost:11434 | Ollama backend URL |
|
983 |
-
| --working-dir | ./rag_storage | Working directory for RAG |
|
984 |
-
| --max-async | 4 | Maximum async operations |
|
985 |
-
| --max-tokens | 32768 | Maximum token size |
|
986 |
-
| --embedding-dim | 1024 | Embedding dimensions |
|
987 |
-
| --max-embed-tokens | 8192 | Maximum embedding token size |
|
988 |
-
| --input-file | ./book.txt | Initial input file |
|
989 |
-
| --log-level | INFO | Logging level |
|
990 |
-
| --key | none | Access Key to protect the lightrag service |
|
991 |
|
992 |
-
|
|
|
993 |
|
994 |
-
|
995 |
-
|-----------|---------|-------------|
|
996 |
-
| --host | 0.0.0.0 | RAG server host |
|
997 |
-
| --port | 9621 | RAG server port |
|
998 |
-
| --model | gpt-4 | OpenAI model name |
|
999 |
-
| --embedding-model | text-embedding-3-large | OpenAI embedding model |
|
1000 |
-
| --working-dir | ./rag_storage | Working directory for RAG |
|
1001 |
-
| --max-tokens | 32768 | Maximum token size |
|
1002 |
-
| --max-embed-tokens | 8192 | Maximum embedding token size |
|
1003 |
-
| --input-dir | ./inputs | Input directory for documents |
|
1004 |
-
| --log-level | INFO | Logging level |
|
1005 |
-
| --key | none | Access Key to protect the lightrag service |
|
1006 |
|
1007 |
-
|
1008 |
|
1009 |
-
|
1010 |
-
|
1011 |
-
|
1012 |
-
| --port | 9621 | Server port |
|
1013 |
-
| --model | gpt-4 | OpenAI model name |
|
1014 |
-
| --embedding-model | text-embedding-3-large | OpenAI embedding model |
|
1015 |
-
| --working-dir | ./rag_storage | Working directory for RAG |
|
1016 |
-
| --max-tokens | 32768 | Maximum token size |
|
1017 |
-
| --max-embed-tokens | 8192 | Maximum embedding token size |
|
1018 |
-
| --input-dir | ./inputs | Input directory for documents |
|
1019 |
-
| --enable-cache | True | Enable response cache |
|
1020 |
-
| --log-level | INFO | Logging level |
|
1021 |
-
| --key | none | Access Key to protect the lightrag service |
|
1022 |
|
|
|
|
|
1023 |
|
1024 |
-
|
1025 |
-
|
1026 |
|
1027 |
-
|
|
|
|
|
|
|
|
|
1028 |
|
1029 |
```bash
|
1030 |
-
#
|
1031 |
-
|
1032 |
|
1033 |
-
# Using specific models (ensure they are installed in your
|
1034 |
-
|
1035 |
|
1036 |
-
# Using
|
1037 |
-
|
1038 |
|
|
|
|
|
1039 |
```
|
1040 |
|
1041 |
-
|
|
|
1042 |
|
1043 |
```bash
|
1044 |
-
#
|
1045 |
-
|
1046 |
|
1047 |
-
# Using
|
1048 |
-
|
|
|
|
|
|
|
1049 |
```
|
1050 |
|
1051 |
-
####
|
1052 |
|
1053 |
```bash
|
1054 |
-
#
|
1055 |
-
|
1056 |
-
```
|
1057 |
-
#### Azure OpenAI RAG Server
|
1058 |
-
```bash
|
1059 |
-
# Using GPT-4 with text-embedding-3-large
|
1060 |
-
azure-openai-lightrag-server --model gpt-4o --port 8080 --working-dir ./custom_rag --embedding-model text-embedding-3-large
|
1061 |
-
```
|
1062 |
|
|
|
|
|
|
|
|
|
|
|
|
|
1063 |
|
1064 |
**Important Notes:**
|
1065 |
- For LoLLMs: Make sure the specified models are installed in your LoLLMs instance
|
@@ -1069,10 +1051,7 @@ azure-openai-lightrag-server --model gpt-4o --port 8080 --working-dir ./custom_r
|
|
1069 |
|
1070 |
For help on any server, use the --help flag:
|
1071 |
```bash
|
1072 |
-
|
1073 |
-
ollama-lightrag-server --help
|
1074 |
-
openai-lightrag-server --help
|
1075 |
-
azure-openai-lightrag-server --help
|
1076 |
```
|
1077 |
|
1078 |
Note: If you don't need the API functionality, you can install the base package without API support using:
|
@@ -1092,7 +1071,7 @@ Query the RAG system with options for different search modes.
|
|
1092 |
```bash
|
1093 |
curl -X POST "http://localhost:9621/query" \
|
1094 |
-H "Content-Type: application/json" \
|
1095 |
-
-d '{"query": "Your question here", "mode": "hybrid"}'
|
1096 |
```
|
1097 |
|
1098 |
#### POST /query/stream
|
|
|
912 |
|
913 |
### Prerequisites
|
914 |
|
915 |
+
Before running any of the servers, ensure you have the corresponding backend service running for both llm and embedding.
|
916 |
+
The new api allows you to mix different bindings for llm/embeddings.
|
917 |
+
For example, you have the possibility to use ollama for the embedding and openai for the llm.
|
918 |
|
919 |
#### For LoLLMs Server
|
920 |
- LoLLMs must be running and accessible
|
921 |
- Default connection: http://localhost:9600
|
922 |
+
- Configure using --llm-binding-host and/or --embedding-binding-host if running on a different host/port
|
923 |
|
924 |
#### For Ollama Server
|
925 |
- Ollama must be running and accessible
|
|
|
955 |
|
956 |
Each server has its own specific configuration options:
|
957 |
|
958 |
+
#### LightRag Server Options
|
959 |
|
960 |
| Parameter | Default | Description |
|
961 |
|-----------|---------|-------------|
|
962 |
| --host | 0.0.0.0 | RAG server host |
|
963 |
| --port | 9621 | RAG server port |
|
964 |
+
| --llm-binding | ollama | LLM binding to be used. Supported: lollms, ollama, openai (default: ollama) |
|
965 |
+
| --llm-binding-host | http://localhost:11434 if the binding is ollama, http://localhost:9600 if the binding is lollms, https://api.openai.com/v1 if the binding is openai | llm server host URL (default: http://localhost:11434 if the binding is ollama, http://localhost:9600 if the binding is lollms, https://api.openai.com/v1 if the binding is openai) |
|
966 |
| --model | mistral-nemo:latest | LLM model name |
|
967 |
+
| --embedding-binding | ollama | Embedding binding to be used. Supported: lollms, ollama, openai (default: ollama) |
|
968 |
+
| --embedding-binding-host | http://localhost:11434 if the binding is ollama, http://localhost:9600 if the binding is lollms, https://api.openai.com/v1 if the binding is openai | embedding server host URL (default: http://localhost:11434 if the binding is ollama, http://localhost:9600 if the binding is lollms, https://api.openai.com/v1 if the binding is openai) |
|
969 |
| --embedding-model | bge-m3:latest | Embedding model name |
|
970 |
+
| --embedding-binding-host | http://localhost:9600 | LoLLMS backend URL |
|
971 |
| --working-dir | ./rag_storage | Working directory for RAG |
|
972 |
| --max-async | 4 | Maximum async operations |
|
973 |
| --max-tokens | 32768 | Maximum token size |
|
|
|
977 |
| --log-level | INFO | Logging level |
|
978 |
| --key | none | Access Key to protect the lightrag service |
|
979 |
|
|
|
980 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
981 |
|
982 |
+
For protecting the server using an authentication key, you can also use an environment variable named `LIGHTRAG_API_KEY`.
|
983 |
+
### Example Usage
|
984 |
|
985 |
+
#### Running a Lightrag server with ollama default local server as llm and embedding backends
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
986 |
|
987 |
+
Ollama is the default backend for both llm and embedding, so by default you can run lightrag-server with no parameters and the default ones will be used. Make sure ollama is installed and is running and default models are already installed on ollama.
|
988 |
|
989 |
+
```bash
|
990 |
+
# Run lightrag with ollama, mistral-nemo:latest for llm, and bge-m3:latest for embedding
|
991 |
+
lightrag-server
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
992 |
|
993 |
+
# Using specific models (ensure they are installed in your ollama instance)
|
994 |
+
lightrag-server --llm-model adrienbrault/nous-hermes2theta-llama3-8b:f16 --embedding-model nomic-embed-text --embedding-dim 1024
|
995 |
|
996 |
+
# Using an authentication key
|
997 |
+
lightrag-server --key my-key
|
998 |
|
999 |
+
# Using lollms for llm and ollama for embedding
|
1000 |
+
lightrag-server --llm-binding lollms
|
1001 |
+
```
|
1002 |
+
|
1003 |
+
#### Running a Lightrag server with lollms default local server as llm and embedding backends
|
1004 |
|
1005 |
```bash
|
1006 |
+
# Run lightrag with lollms, mistral-nemo:latest for llm, and bge-m3:latest for embedding, use lollms for both llm and embedding
|
1007 |
+
lightrag-server --llm-binding lollms --embedding-binding lollms
|
1008 |
|
1009 |
+
# Using specific models (ensure they are installed in your ollama instance)
|
1010 |
+
lightrag-server --llm-binding lollms --llm-model adrienbrault/nous-hermes2theta-llama3-8b:f16 --embedding-binding lollms --embedding-model nomic-embed-text --embedding-dim 1024
|
1011 |
|
1012 |
+
# Using an authentication key
|
1013 |
+
lightrag-server --key my-key
|
1014 |
|
1015 |
+
# Using lollms for llm and openai for embedding
|
1016 |
+
lightrag-server --llm-binding lollms --embedding-binding openai --embedding-model text-embedding-3-small
|
1017 |
```
|
1018 |
|
1019 |
+
|
1020 |
+
#### Running a Lightrag server with openai server as llm and embedding backends
|
1021 |
|
1022 |
```bash
|
1023 |
+
# Run lightrag with lollms, GPT-4o-mini for llm, and text-embedding-3-small for embedding, use openai for both llm and embedding
|
1024 |
+
lightrag-server --llm-binding openai --llm-model GPT-4o-mini --embedding-binding openai --embedding-model text-embedding-3-small
|
1025 |
|
1026 |
+
# Using an authentication key
|
1027 |
+
lightrag-server --llm-binding openai --llm-model GPT-4o-mini --embedding-binding openai --embedding-model text-embedding-3-small --key my-key
|
1028 |
+
|
1029 |
+
# Using lollms for llm and openai for embedding
|
1030 |
+
lightrag-server --llm-binding lollms --embedding-binding openai --embedding-model text-embedding-3-small
|
1031 |
```
|
1032 |
|
1033 |
+
#### Running a Lightrag server with azure openai server as llm and embedding backends
|
1034 |
|
1035 |
```bash
|
1036 |
+
# Run lightrag with lollms, GPT-4o-mini for llm, and text-embedding-3-small for embedding, use openai for both llm and embedding
|
1037 |
+
lightrag-server --llm-binding azure_openai --llm-model GPT-4o-mini --embedding-binding openai --embedding-model text-embedding-3-small
|
|
|
|
|
|
|
|
|
|
|
|
|
1038 |
|
1039 |
+
# Using an authentication key
|
1040 |
+
lightrag-server --llm-binding azure_openai --llm-model GPT-4o-mini --embedding-binding azure_openai --embedding-model text-embedding-3-small --key my-key
|
1041 |
+
|
1042 |
+
# Using lollms for llm and azure_openai for embedding
|
1043 |
+
lightrag-server --llm-binding lollms --embedding-binding azure_openai --embedding-model text-embedding-3-small
|
1044 |
+
```
|
1045 |
|
1046 |
**Important Notes:**
|
1047 |
- For LoLLMs: Make sure the specified models are installed in your LoLLMs instance
|
|
|
1051 |
|
1052 |
For help on any server, use the --help flag:
|
1053 |
```bash
|
1054 |
+
lightrag-server --help
|
|
|
|
|
|
|
1055 |
```
|
1056 |
|
1057 |
Note: If you don't need the API functionality, you can install the base package without API support using:
|
|
|
1071 |
```bash
|
1072 |
curl -X POST "http://localhost:9621/query" \
|
1073 |
-H "Content-Type: application/json" \
|
1074 |
+
-d '{"query": "Your question here", "mode": "hybrid", ""}'
|
1075 |
```
|
1076 |
|
1077 |
#### POST /query/stream
|
lightrag/api/azure_openai_lightrag_server.py
DELETED
@@ -1,532 +0,0 @@
|
|
1 |
-
from fastapi import FastAPI, HTTPException, File, UploadFile, Form
|
2 |
-
from pydantic import BaseModel
|
3 |
-
import asyncio
|
4 |
-
import logging
|
5 |
-
import argparse
|
6 |
-
from lightrag import LightRAG, QueryParam
|
7 |
-
from lightrag.llm import (
|
8 |
-
azure_openai_complete_if_cache,
|
9 |
-
azure_openai_embedding,
|
10 |
-
)
|
11 |
-
from lightrag.utils import EmbeddingFunc
|
12 |
-
from typing import Optional, List
|
13 |
-
from enum import Enum
|
14 |
-
from pathlib import Path
|
15 |
-
import shutil
|
16 |
-
import aiofiles
|
17 |
-
from ascii_colors import trace_exception
|
18 |
-
import os
|
19 |
-
from dotenv import load_dotenv
|
20 |
-
import inspect
|
21 |
-
import json
|
22 |
-
from fastapi.responses import StreamingResponse
|
23 |
-
|
24 |
-
from fastapi import Depends, Security
|
25 |
-
from fastapi.security import APIKeyHeader
|
26 |
-
from fastapi.middleware.cors import CORSMiddleware
|
27 |
-
|
28 |
-
from starlette.status import HTTP_403_FORBIDDEN
|
29 |
-
|
30 |
-
load_dotenv()
|
31 |
-
|
32 |
-
AZURE_OPENAI_API_VERSION = os.getenv("AZURE_OPENAI_API_VERSION")
|
33 |
-
AZURE_OPENAI_DEPLOYMENT = os.getenv("AZURE_OPENAI_DEPLOYMENT")
|
34 |
-
AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY")
|
35 |
-
AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
|
36 |
-
|
37 |
-
AZURE_EMBEDDING_DEPLOYMENT = os.getenv("AZURE_EMBEDDING_DEPLOYMENT")
|
38 |
-
AZURE_EMBEDDING_API_VERSION = os.getenv("AZURE_EMBEDDING_API_VERSION")
|
39 |
-
|
40 |
-
|
41 |
-
def parse_args():
|
42 |
-
parser = argparse.ArgumentParser(
|
43 |
-
description="LightRAG FastAPI Server with OpenAI integration"
|
44 |
-
)
|
45 |
-
|
46 |
-
# Server configuration
|
47 |
-
parser.add_argument(
|
48 |
-
"--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)"
|
49 |
-
)
|
50 |
-
parser.add_argument(
|
51 |
-
"--port", type=int, default=9621, help="Server port (default: 9621)"
|
52 |
-
)
|
53 |
-
|
54 |
-
# Directory configuration
|
55 |
-
parser.add_argument(
|
56 |
-
"--working-dir",
|
57 |
-
default="./rag_storage",
|
58 |
-
help="Working directory for RAG storage (default: ./rag_storage)",
|
59 |
-
)
|
60 |
-
parser.add_argument(
|
61 |
-
"--input-dir",
|
62 |
-
default="./inputs",
|
63 |
-
help="Directory containing input documents (default: ./inputs)",
|
64 |
-
)
|
65 |
-
|
66 |
-
# Model configuration
|
67 |
-
parser.add_argument(
|
68 |
-
"--model", default="gpt-4o", help="OpenAI model name (default: gpt-4o)"
|
69 |
-
)
|
70 |
-
parser.add_argument(
|
71 |
-
"--embedding-model",
|
72 |
-
default="text-embedding-3-large",
|
73 |
-
help="OpenAI embedding model (default: text-embedding-3-large)",
|
74 |
-
)
|
75 |
-
|
76 |
-
# RAG configuration
|
77 |
-
parser.add_argument(
|
78 |
-
"--max-tokens",
|
79 |
-
type=int,
|
80 |
-
default=32768,
|
81 |
-
help="Maximum token size (default: 32768)",
|
82 |
-
)
|
83 |
-
parser.add_argument(
|
84 |
-
"--max-embed-tokens",
|
85 |
-
type=int,
|
86 |
-
default=8192,
|
87 |
-
help="Maximum embedding token size (default: 8192)",
|
88 |
-
)
|
89 |
-
parser.add_argument(
|
90 |
-
"--enable-cache",
|
91 |
-
default=True,
|
92 |
-
help="Enable response cache (default: True)",
|
93 |
-
)
|
94 |
-
# Logging configuration
|
95 |
-
parser.add_argument(
|
96 |
-
"--log-level",
|
97 |
-
default="INFO",
|
98 |
-
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
99 |
-
help="Logging level (default: INFO)",
|
100 |
-
)
|
101 |
-
|
102 |
-
parser.add_argument(
|
103 |
-
"--key",
|
104 |
-
type=str,
|
105 |
-
help="API key for authentication. This protects lightrag server against unauthorized access",
|
106 |
-
default=None,
|
107 |
-
)
|
108 |
-
|
109 |
-
return parser.parse_args()
|
110 |
-
|
111 |
-
|
112 |
-
class DocumentManager:
|
113 |
-
"""Handles document operations and tracking"""
|
114 |
-
|
115 |
-
def __init__(self, input_dir: str, supported_extensions: tuple = (".txt", ".md")):
|
116 |
-
self.input_dir = Path(input_dir)
|
117 |
-
self.supported_extensions = supported_extensions
|
118 |
-
self.indexed_files = set()
|
119 |
-
|
120 |
-
# Create input directory if it doesn't exist
|
121 |
-
self.input_dir.mkdir(parents=True, exist_ok=True)
|
122 |
-
|
123 |
-
def scan_directory(self) -> List[Path]:
|
124 |
-
"""Scan input directory for new files"""
|
125 |
-
new_files = []
|
126 |
-
for ext in self.supported_extensions:
|
127 |
-
for file_path in self.input_dir.rglob(f"*{ext}"):
|
128 |
-
if file_path not in self.indexed_files:
|
129 |
-
new_files.append(file_path)
|
130 |
-
return new_files
|
131 |
-
|
132 |
-
def mark_as_indexed(self, file_path: Path):
|
133 |
-
"""Mark a file as indexed"""
|
134 |
-
self.indexed_files.add(file_path)
|
135 |
-
|
136 |
-
def is_supported_file(self, filename: str) -> bool:
|
137 |
-
"""Check if file type is supported"""
|
138 |
-
return any(filename.lower().endswith(ext) for ext in self.supported_extensions)
|
139 |
-
|
140 |
-
|
141 |
-
# Pydantic models
|
142 |
-
class SearchMode(str, Enum):
|
143 |
-
naive = "naive"
|
144 |
-
local = "local"
|
145 |
-
global_ = "global"
|
146 |
-
hybrid = "hybrid"
|
147 |
-
|
148 |
-
|
149 |
-
class QueryRequest(BaseModel):
|
150 |
-
query: str
|
151 |
-
mode: SearchMode = SearchMode.hybrid
|
152 |
-
only_need_context: bool = False
|
153 |
-
# stream: bool = False
|
154 |
-
|
155 |
-
|
156 |
-
class QueryResponse(BaseModel):
|
157 |
-
response: str
|
158 |
-
|
159 |
-
|
160 |
-
class InsertTextRequest(BaseModel):
|
161 |
-
text: str
|
162 |
-
description: Optional[str] = None
|
163 |
-
|
164 |
-
|
165 |
-
class InsertResponse(BaseModel):
|
166 |
-
status: str
|
167 |
-
message: str
|
168 |
-
document_count: int
|
169 |
-
|
170 |
-
|
171 |
-
def get_api_key_dependency(api_key: Optional[str]):
|
172 |
-
if not api_key:
|
173 |
-
# If no API key is configured, return a dummy dependency that always succeeds
|
174 |
-
async def no_auth():
|
175 |
-
return None
|
176 |
-
|
177 |
-
return no_auth
|
178 |
-
|
179 |
-
# If API key is configured, use proper authentication
|
180 |
-
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
181 |
-
|
182 |
-
async def api_key_auth(api_key_header_value: str | None = Security(api_key_header)):
|
183 |
-
if not api_key_header_value:
|
184 |
-
raise HTTPException(
|
185 |
-
status_code=HTTP_403_FORBIDDEN, detail="API Key required"
|
186 |
-
)
|
187 |
-
if api_key_header_value != api_key:
|
188 |
-
raise HTTPException(
|
189 |
-
status_code=HTTP_403_FORBIDDEN, detail="Invalid API Key"
|
190 |
-
)
|
191 |
-
return api_key_header_value
|
192 |
-
|
193 |
-
return api_key_auth
|
194 |
-
|
195 |
-
|
196 |
-
async def get_embedding_dim(embedding_model: str) -> int:
|
197 |
-
"""Get embedding dimensions for the specified model"""
|
198 |
-
test_text = ["This is a test sentence."]
|
199 |
-
embedding = await azure_openai_embedding(test_text, model=embedding_model)
|
200 |
-
return embedding.shape[1]
|
201 |
-
|
202 |
-
|
203 |
-
def create_app(args):
|
204 |
-
# Setup logging
|
205 |
-
logging.basicConfig(
|
206 |
-
format="%(levelname)s:%(message)s", level=getattr(logging, args.log_level)
|
207 |
-
)
|
208 |
-
|
209 |
-
# Check if API key is provided either through env var or args
|
210 |
-
api_key = os.getenv("LIGHTRAG_API_KEY") or args.key
|
211 |
-
|
212 |
-
# Initialize FastAPI
|
213 |
-
app = FastAPI(
|
214 |
-
title="LightRAG API",
|
215 |
-
description="API for querying text using LightRAG with separate storage and input directories"
|
216 |
-
+ "(With authentication)"
|
217 |
-
if api_key
|
218 |
-
else "",
|
219 |
-
version="1.0.0",
|
220 |
-
openapi_tags=[{"name": "api"}],
|
221 |
-
)
|
222 |
-
|
223 |
-
# Add CORS middleware
|
224 |
-
app.add_middleware(
|
225 |
-
CORSMiddleware,
|
226 |
-
allow_origins=["*"],
|
227 |
-
allow_credentials=True,
|
228 |
-
allow_methods=["*"],
|
229 |
-
allow_headers=["*"],
|
230 |
-
)
|
231 |
-
|
232 |
-
# Create the optional API key dependency
|
233 |
-
optional_api_key = get_api_key_dependency(api_key)
|
234 |
-
|
235 |
-
# Create working directory if it doesn't exist
|
236 |
-
Path(args.working_dir).mkdir(parents=True, exist_ok=True)
|
237 |
-
|
238 |
-
# Initialize document manager
|
239 |
-
doc_manager = DocumentManager(args.input_dir)
|
240 |
-
|
241 |
-
# Get embedding dimensions
|
242 |
-
embedding_dim = asyncio.run(get_embedding_dim(args.embedding_model))
|
243 |
-
|
244 |
-
async def async_openai_complete(
|
245 |
-
prompt, system_prompt=None, history_messages=[], **kwargs
|
246 |
-
):
|
247 |
-
"""Async wrapper for OpenAI completion"""
|
248 |
-
kwargs.pop("keyword_extraction", None)
|
249 |
-
|
250 |
-
return await azure_openai_complete_if_cache(
|
251 |
-
args.model,
|
252 |
-
prompt,
|
253 |
-
system_prompt=system_prompt,
|
254 |
-
history_messages=history_messages,
|
255 |
-
base_url=AZURE_OPENAI_ENDPOINT,
|
256 |
-
api_key=AZURE_OPENAI_API_KEY,
|
257 |
-
api_version=AZURE_OPENAI_API_VERSION,
|
258 |
-
**kwargs,
|
259 |
-
)
|
260 |
-
|
261 |
-
# Initialize RAG with OpenAI configuration
|
262 |
-
rag = LightRAG(
|
263 |
-
enable_llm_cache=args.enable_cache,
|
264 |
-
working_dir=args.working_dir,
|
265 |
-
llm_model_func=async_openai_complete,
|
266 |
-
llm_model_name=args.model,
|
267 |
-
llm_model_max_token_size=args.max_tokens,
|
268 |
-
embedding_func=EmbeddingFunc(
|
269 |
-
embedding_dim=embedding_dim,
|
270 |
-
max_token_size=args.max_embed_tokens,
|
271 |
-
func=lambda texts: azure_openai_embedding(
|
272 |
-
texts, model=args.embedding_model
|
273 |
-
),
|
274 |
-
),
|
275 |
-
)
|
276 |
-
|
277 |
-
@app.on_event("startup")
|
278 |
-
async def startup_event():
|
279 |
-
"""Index all files in input directory during startup"""
|
280 |
-
try:
|
281 |
-
new_files = doc_manager.scan_directory()
|
282 |
-
for file_path in new_files:
|
283 |
-
try:
|
284 |
-
# Use async file reading
|
285 |
-
async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
|
286 |
-
content = await f.read()
|
287 |
-
# Use the async version of insert directly
|
288 |
-
await rag.ainsert(content)
|
289 |
-
doc_manager.mark_as_indexed(file_path)
|
290 |
-
logging.info(f"Indexed file: {file_path}")
|
291 |
-
except Exception as e:
|
292 |
-
trace_exception(e)
|
293 |
-
logging.error(f"Error indexing file {file_path}: {str(e)}")
|
294 |
-
|
295 |
-
logging.info(f"Indexed {len(new_files)} documents from {args.input_dir}")
|
296 |
-
|
297 |
-
except Exception as e:
|
298 |
-
logging.error(f"Error during startup indexing: {str(e)}")
|
299 |
-
|
300 |
-
@app.post("/documents/scan", dependencies=[Depends(optional_api_key)])
|
301 |
-
async def scan_for_new_documents():
|
302 |
-
"""Manually trigger scanning for new documents"""
|
303 |
-
try:
|
304 |
-
new_files = doc_manager.scan_directory()
|
305 |
-
indexed_count = 0
|
306 |
-
|
307 |
-
for file_path in new_files:
|
308 |
-
try:
|
309 |
-
with open(file_path, "r", encoding="utf-8") as f:
|
310 |
-
content = f.read()
|
311 |
-
await rag.ainsert(content)
|
312 |
-
doc_manager.mark_as_indexed(file_path)
|
313 |
-
indexed_count += 1
|
314 |
-
except Exception as e:
|
315 |
-
logging.error(f"Error indexing file {file_path}: {str(e)}")
|
316 |
-
|
317 |
-
return {
|
318 |
-
"status": "success",
|
319 |
-
"indexed_count": indexed_count,
|
320 |
-
"total_documents": len(doc_manager.indexed_files),
|
321 |
-
}
|
322 |
-
except Exception as e:
|
323 |
-
raise HTTPException(status_code=500, detail=str(e))
|
324 |
-
|
325 |
-
@app.post("/resetcache", dependencies=[Depends(optional_api_key)])
|
326 |
-
async def reset_cache():
|
327 |
-
"""Manually reset cache"""
|
328 |
-
try:
|
329 |
-
cachefile = args.working_dir + "/kv_store_llm_response_cache.json"
|
330 |
-
if os.path.exists(cachefile):
|
331 |
-
with open(cachefile, "w") as f:
|
332 |
-
f.write("{}")
|
333 |
-
return {"status": "success"}
|
334 |
-
except Exception as e:
|
335 |
-
raise HTTPException(status_code=500, detail=str(e))
|
336 |
-
|
337 |
-
@app.post("/documents/upload", dependencies=[Depends(optional_api_key)])
|
338 |
-
async def upload_to_input_dir(file: UploadFile = File(...)):
|
339 |
-
"""Upload a file to the input directory"""
|
340 |
-
try:
|
341 |
-
if not doc_manager.is_supported_file(file.filename):
|
342 |
-
raise HTTPException(
|
343 |
-
status_code=400,
|
344 |
-
detail=f"Unsupported file type. Supported types: {doc_manager.supported_extensions}",
|
345 |
-
)
|
346 |
-
|
347 |
-
file_path = doc_manager.input_dir / file.filename
|
348 |
-
with open(file_path, "wb") as buffer:
|
349 |
-
shutil.copyfileobj(file.file, buffer)
|
350 |
-
|
351 |
-
# Immediately index the uploaded file
|
352 |
-
with open(file_path, "r", encoding="utf-8") as f:
|
353 |
-
content = f.read()
|
354 |
-
await rag.ainsert(content)
|
355 |
-
doc_manager.mark_as_indexed(file_path)
|
356 |
-
|
357 |
-
return {
|
358 |
-
"status": "success",
|
359 |
-
"message": f"File uploaded and indexed: {file.filename}",
|
360 |
-
"total_documents": len(doc_manager.indexed_files),
|
361 |
-
}
|
362 |
-
except Exception as e:
|
363 |
-
raise HTTPException(status_code=500, detail=str(e))
|
364 |
-
|
365 |
-
@app.post(
|
366 |
-
"/query", response_model=QueryResponse, dependencies=[Depends(optional_api_key)]
|
367 |
-
)
|
368 |
-
async def query_text(request: QueryRequest):
|
369 |
-
try:
|
370 |
-
response = await rag.aquery(
|
371 |
-
request.query,
|
372 |
-
param=QueryParam(
|
373 |
-
mode=request.mode,
|
374 |
-
stream=False,
|
375 |
-
only_need_context=request.only_need_context,
|
376 |
-
),
|
377 |
-
)
|
378 |
-
return QueryResponse(response=response)
|
379 |
-
except Exception as e:
|
380 |
-
raise HTTPException(status_code=500, detail=str(e))
|
381 |
-
|
382 |
-
@app.post("/query/stream", dependencies=[Depends(optional_api_key)])
|
383 |
-
async def query_text_stream(request: QueryRequest):
|
384 |
-
try:
|
385 |
-
response = await rag.aquery(
|
386 |
-
request.query,
|
387 |
-
param=QueryParam(
|
388 |
-
mode=request.mode,
|
389 |
-
stream=True,
|
390 |
-
only_need_context=request.only_need_context,
|
391 |
-
),
|
392 |
-
)
|
393 |
-
if inspect.isasyncgen(response):
|
394 |
-
|
395 |
-
async def stream_generator():
|
396 |
-
async for chunk in response:
|
397 |
-
yield json.dumps({"data": chunk}) + "\n"
|
398 |
-
|
399 |
-
return StreamingResponse(
|
400 |
-
stream_generator(), media_type="application/json"
|
401 |
-
)
|
402 |
-
else:
|
403 |
-
return QueryResponse(response=response)
|
404 |
-
|
405 |
-
except Exception as e:
|
406 |
-
raise HTTPException(status_code=500, detail=str(e))
|
407 |
-
|
408 |
-
@app.post(
|
409 |
-
"/documents/text",
|
410 |
-
response_model=InsertResponse,
|
411 |
-
dependencies=[Depends(optional_api_key)],
|
412 |
-
)
|
413 |
-
async def insert_text(request: InsertTextRequest):
|
414 |
-
try:
|
415 |
-
await rag.ainsert(request.text)
|
416 |
-
return InsertResponse(
|
417 |
-
status="success",
|
418 |
-
message="Text successfully inserted",
|
419 |
-
document_count=1,
|
420 |
-
)
|
421 |
-
except Exception as e:
|
422 |
-
raise HTTPException(status_code=500, detail=str(e))
|
423 |
-
|
424 |
-
@app.post(
|
425 |
-
"/documents/file",
|
426 |
-
response_model=InsertResponse,
|
427 |
-
dependencies=[Depends(optional_api_key)],
|
428 |
-
)
|
429 |
-
async def insert_file(file: UploadFile = File(...), description: str = Form(None)):
|
430 |
-
try:
|
431 |
-
content = await file.read()
|
432 |
-
|
433 |
-
if file.filename.endswith((".txt", ".md")):
|
434 |
-
text = content.decode("utf-8")
|
435 |
-
rag.insert(text)
|
436 |
-
else:
|
437 |
-
raise HTTPException(
|
438 |
-
status_code=400,
|
439 |
-
detail="Unsupported file type. Only .txt and .md files are supported",
|
440 |
-
)
|
441 |
-
|
442 |
-
return InsertResponse(
|
443 |
-
status="success",
|
444 |
-
message=f"File '{file.filename}' successfully inserted",
|
445 |
-
document_count=1,
|
446 |
-
)
|
447 |
-
except UnicodeDecodeError:
|
448 |
-
raise HTTPException(status_code=400, detail="File encoding not supported")
|
449 |
-
except Exception as e:
|
450 |
-
raise HTTPException(status_code=500, detail=str(e))
|
451 |
-
|
452 |
-
@app.post(
|
453 |
-
"/documents/batch",
|
454 |
-
response_model=InsertResponse,
|
455 |
-
dependencies=[Depends(optional_api_key)],
|
456 |
-
)
|
457 |
-
async def insert_batch(files: List[UploadFile] = File(...)):
|
458 |
-
try:
|
459 |
-
inserted_count = 0
|
460 |
-
failed_files = []
|
461 |
-
|
462 |
-
for file in files:
|
463 |
-
try:
|
464 |
-
content = await file.read()
|
465 |
-
if file.filename.endswith((".txt", ".md")):
|
466 |
-
text = content.decode("utf-8")
|
467 |
-
rag.insert(text)
|
468 |
-
inserted_count += 1
|
469 |
-
else:
|
470 |
-
failed_files.append(f"{file.filename} (unsupported type)")
|
471 |
-
except Exception as e:
|
472 |
-
failed_files.append(f"{file.filename} ({str(e)})")
|
473 |
-
|
474 |
-
status_message = f"Successfully inserted {inserted_count} documents"
|
475 |
-
if failed_files:
|
476 |
-
status_message += f". Failed files: {', '.join(failed_files)}"
|
477 |
-
|
478 |
-
return InsertResponse(
|
479 |
-
status="success" if inserted_count > 0 else "partial_success",
|
480 |
-
message=status_message,
|
481 |
-
document_count=len(files),
|
482 |
-
)
|
483 |
-
except Exception as e:
|
484 |
-
raise HTTPException(status_code=500, detail=str(e))
|
485 |
-
|
486 |
-
@app.delete(
|
487 |
-
"/documents",
|
488 |
-
response_model=InsertResponse,
|
489 |
-
dependencies=[Depends(optional_api_key)],
|
490 |
-
)
|
491 |
-
async def clear_documents():
|
492 |
-
try:
|
493 |
-
rag.text_chunks = []
|
494 |
-
rag.entities_vdb = None
|
495 |
-
rag.relationships_vdb = None
|
496 |
-
return InsertResponse(
|
497 |
-
status="success",
|
498 |
-
message="All documents cleared successfully",
|
499 |
-
document_count=0,
|
500 |
-
)
|
501 |
-
except Exception as e:
|
502 |
-
raise HTTPException(status_code=500, detail=str(e))
|
503 |
-
|
504 |
-
@app.get("/health", dependencies=[Depends(optional_api_key)])
|
505 |
-
async def get_status():
|
506 |
-
"""Get current system status"""
|
507 |
-
return {
|
508 |
-
"status": "healthy",
|
509 |
-
"working_directory": str(args.working_dir),
|
510 |
-
"input_directory": str(args.input_dir),
|
511 |
-
"indexed_files": len(doc_manager.indexed_files),
|
512 |
-
"configuration": {
|
513 |
-
"model": args.model,
|
514 |
-
"embedding_model": args.embedding_model,
|
515 |
-
"max_tokens": args.max_tokens,
|
516 |
-
"embedding_dim": embedding_dim,
|
517 |
-
},
|
518 |
-
}
|
519 |
-
|
520 |
-
return app
|
521 |
-
|
522 |
-
|
523 |
-
def main():
|
524 |
-
args = parse_args()
|
525 |
-
import uvicorn
|
526 |
-
|
527 |
-
app = create_app(args)
|
528 |
-
uvicorn.run(app, host=args.host, port=args.port)
|
529 |
-
|
530 |
-
|
531 |
-
if __name__ == "__main__":
|
532 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lightrag/api/{lollms_lightrag_server.py → lightrag_server.py}
RENAMED
@@ -4,6 +4,10 @@ import logging
|
|
4 |
import argparse
|
5 |
from lightrag import LightRAG, QueryParam
|
6 |
from lightrag.llm import lollms_model_complete, lollms_embed
|
|
|
|
|
|
|
|
|
7 |
from lightrag.utils import EmbeddingFunc
|
8 |
from typing import Optional, List
|
9 |
from enum import Enum
|
@@ -19,12 +23,36 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
19 |
|
20 |
from starlette.status import HTTP_403_FORBIDDEN
|
21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
|
23 |
def parse_args():
|
24 |
parser = argparse.ArgumentParser(
|
25 |
description="LightRAG FastAPI Server with separate working and input directories"
|
26 |
)
|
27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
# Server configuration
|
29 |
parser.add_argument(
|
30 |
"--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)"
|
@@ -45,22 +73,33 @@ def parse_args():
|
|
45 |
help="Directory containing input documents (default: ./inputs)",
|
46 |
)
|
47 |
|
48 |
-
# Model configuration
|
|
|
49 |
parser.add_argument(
|
50 |
-
"--
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
default="mistral-nemo:latest",
|
52 |
help="LLM model name (default: mistral-nemo:latest)",
|
53 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
parser.add_argument(
|
55 |
"--embedding-model",
|
56 |
default="bge-m3:latest",
|
57 |
help="Embedding model name (default: bge-m3:latest)",
|
58 |
)
|
59 |
-
parser.add_argument(
|
60 |
-
"--lollms-host",
|
61 |
-
default="http://localhost:9600",
|
62 |
-
help="lollms host URL (default: http://localhost:9600)",
|
63 |
-
)
|
64 |
|
65 |
# RAG configuration
|
66 |
parser.add_argument(
|
@@ -188,6 +227,15 @@ def get_api_key_dependency(api_key: Optional[str]):
|
|
188 |
|
189 |
|
190 |
def create_app(args):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
191 |
# Setup logging
|
192 |
logging.basicConfig(
|
193 |
format="%(levelname)s:%(message)s", level=getattr(logging, args.log_level)
|
@@ -203,7 +251,7 @@ def create_app(args):
|
|
203 |
+ "(With authentication)"
|
204 |
if api_key
|
205 |
else "",
|
206 |
-
version="1.0.
|
207 |
openapi_tags=[{"name": "api"}],
|
208 |
)
|
209 |
|
@@ -225,23 +273,32 @@ def create_app(args):
|
|
225 |
# Initialize document manager
|
226 |
doc_manager = DocumentManager(args.input_dir)
|
227 |
|
|
|
|
|
228 |
# Initialize RAG
|
229 |
rag = LightRAG(
|
230 |
working_dir=args.working_dir,
|
231 |
-
llm_model_func=lollms_model_complete,
|
232 |
-
llm_model_name=args.
|
233 |
llm_model_max_async=args.max_async,
|
234 |
llm_model_max_token_size=args.max_tokens,
|
235 |
llm_model_kwargs={
|
236 |
-
"host": args.
|
237 |
"options": {"num_ctx": args.max_tokens},
|
238 |
},
|
239 |
embedding_func=EmbeddingFunc(
|
240 |
embedding_dim=args.embedding_dim,
|
241 |
max_token_size=args.max_embed_tokens,
|
242 |
func=lambda texts: lollms_embed(
|
243 |
-
texts, embed_model=args.embedding_model, host=args.
|
244 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
245 |
),
|
246 |
)
|
247 |
|
@@ -470,10 +527,17 @@ def create_app(args):
|
|
470 |
"input_directory": str(args.input_dir),
|
471 |
"indexed_files": len(doc_manager.indexed_files),
|
472 |
"configuration": {
|
473 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
474 |
"embedding_model": args.embedding_model,
|
|
|
475 |
"max_tokens": args.max_tokens,
|
476 |
-
"lollms_host": args.lollms_host,
|
477 |
},
|
478 |
}
|
479 |
|
|
|
4 |
import argparse
|
5 |
from lightrag import LightRAG, QueryParam
|
6 |
from lightrag.llm import lollms_model_complete, lollms_embed
|
7 |
+
from lightrag.llm import ollama_model_complete, ollama_embed
|
8 |
+
from lightrag.llm import openai_complete_if_cache, openai_embedding
|
9 |
+
from lightrag.llm import azure_openai_complete_if_cache, azure_openai_embedding
|
10 |
+
|
11 |
from lightrag.utils import EmbeddingFunc
|
12 |
from typing import Optional, List
|
13 |
from enum import Enum
|
|
|
23 |
|
24 |
from starlette.status import HTTP_403_FORBIDDEN
|
25 |
|
26 |
+
def get_default_host(binding_type: str) -> str:
|
27 |
+
default_hosts = {
|
28 |
+
"ollama": "http://localhost:11434",
|
29 |
+
"lollms": "http://localhost:9600",
|
30 |
+
"azure_openai": "https://api.openai.com/v1",
|
31 |
+
"openai": "https://api.openai.com/v1"
|
32 |
+
}
|
33 |
+
return default_hosts.get(binding_type, "http://localhost:11434") # fallback to ollama if unknown
|
34 |
|
35 |
def parse_args():
|
36 |
parser = argparse.ArgumentParser(
|
37 |
description="LightRAG FastAPI Server with separate working and input directories"
|
38 |
)
|
39 |
|
40 |
+
#Start by the bindings
|
41 |
+
parser.add_argument(
|
42 |
+
"--llm-binding",
|
43 |
+
default="ollama",
|
44 |
+
help="LLM binding to be used. Supported: lollms, ollama, openai (default: ollama)",
|
45 |
+
)
|
46 |
+
parser.add_argument(
|
47 |
+
"--embedding-binding",
|
48 |
+
default="ollama",
|
49 |
+
help="Embedding binding to be used. Supported: lollms, ollama, openai (default: ollama)",
|
50 |
+
)
|
51 |
+
|
52 |
+
# Parse just these arguments first
|
53 |
+
temp_args, _ = parser.parse_known_args()
|
54 |
+
|
55 |
+
# Add remaining arguments with dynamic defaults for hosts
|
56 |
# Server configuration
|
57 |
parser.add_argument(
|
58 |
"--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)"
|
|
|
73 |
help="Directory containing input documents (default: ./inputs)",
|
74 |
)
|
75 |
|
76 |
+
# LLM Model configuration
|
77 |
+
default_llm_host = get_default_host(temp_args.llm_binding)
|
78 |
parser.add_argument(
|
79 |
+
"--llm-binding-host",
|
80 |
+
default=default_llm_host,
|
81 |
+
help=f"llm server host URL (default: {default_llm_host})",
|
82 |
+
)
|
83 |
+
|
84 |
+
parser.add_argument(
|
85 |
+
"--llm-model",
|
86 |
default="mistral-nemo:latest",
|
87 |
help="LLM model name (default: mistral-nemo:latest)",
|
88 |
)
|
89 |
+
|
90 |
+
# Embedding model configuration
|
91 |
+
default_embedding_host = get_default_host(temp_args.embedding_binding)
|
92 |
+
parser.add_argument(
|
93 |
+
"--embedding-binding-host",
|
94 |
+
default=default_embedding_host,
|
95 |
+
help=f"embedding server host URL (default: {default_embedding_host})",
|
96 |
+
)
|
97 |
+
|
98 |
parser.add_argument(
|
99 |
"--embedding-model",
|
100 |
default="bge-m3:latest",
|
101 |
help="Embedding model name (default: bge-m3:latest)",
|
102 |
)
|
|
|
|
|
|
|
|
|
|
|
103 |
|
104 |
# RAG configuration
|
105 |
parser.add_argument(
|
|
|
227 |
|
228 |
|
229 |
def create_app(args):
|
230 |
+
# Verify that bindings arer correctly setup
|
231 |
+
if args.llm_binding not in ["lollms", "ollama", "openai"]:
|
232 |
+
raise Exception("llm binding not supported")
|
233 |
+
|
234 |
+
if args.embedding_binding not in ["lollms", "ollama", "openai"]:
|
235 |
+
raise Exception("embedding binding not supported")
|
236 |
+
|
237 |
+
|
238 |
+
|
239 |
# Setup logging
|
240 |
logging.basicConfig(
|
241 |
format="%(levelname)s:%(message)s", level=getattr(logging, args.log_level)
|
|
|
251 |
+ "(With authentication)"
|
252 |
if api_key
|
253 |
else "",
|
254 |
+
version="1.0.1",
|
255 |
openapi_tags=[{"name": "api"}],
|
256 |
)
|
257 |
|
|
|
273 |
# Initialize document manager
|
274 |
doc_manager = DocumentManager(args.input_dir)
|
275 |
|
276 |
+
|
277 |
+
|
278 |
# Initialize RAG
|
279 |
rag = LightRAG(
|
280 |
working_dir=args.working_dir,
|
281 |
+
llm_model_func=lollms_model_complete if args.llm_binding=="lollms" else ollama_model_complete if args.llm_binding=="ollama" else azure_openai_complete_if_cache if args.llm_binding=="azure_openai" else openai_complete_if_cache,
|
282 |
+
llm_model_name=args.llm_model,
|
283 |
llm_model_max_async=args.max_async,
|
284 |
llm_model_max_token_size=args.max_tokens,
|
285 |
llm_model_kwargs={
|
286 |
+
"host": args.llm_binding_host,
|
287 |
"options": {"num_ctx": args.max_tokens},
|
288 |
},
|
289 |
embedding_func=EmbeddingFunc(
|
290 |
embedding_dim=args.embedding_dim,
|
291 |
max_token_size=args.max_embed_tokens,
|
292 |
func=lambda texts: lollms_embed(
|
293 |
+
texts, embed_model=args.embedding_model, host=args.embedding_binding_host
|
294 |
+
) if args.llm_binding=="lollms" else ollama_embed(
|
295 |
+
texts, embed_model=args.embedding_model, host=args.embedding_binding_host
|
296 |
+
) if args.llm_binding=="ollama" else azure_openai_embedding(
|
297 |
+
texts, model=args.embedding_model # no host is used for openai
|
298 |
+
) if args.llm_binding=="azure_openai" else openai_embedding(
|
299 |
+
texts, model=args.embedding_model # no host is used for openai
|
300 |
+
)
|
301 |
+
|
302 |
),
|
303 |
)
|
304 |
|
|
|
527 |
"input_directory": str(args.input_dir),
|
528 |
"indexed_files": len(doc_manager.indexed_files),
|
529 |
"configuration": {
|
530 |
+
# LLM configuration binding/host address (if applicable)/model (if applicable)
|
531 |
+
"llm_binding": args.llm_binding,
|
532 |
+
"llm_binding_host": args.llm_binding_host,
|
533 |
+
"llm_model": args.llm_model,
|
534 |
+
|
535 |
+
# embedding model configuration binding/host address (if applicable)/model (if applicable)
|
536 |
+
"embedding_binding": args.embedding_binding,
|
537 |
+
"embedding_binding_host": args.embedding_binding_host,
|
538 |
"embedding_model": args.embedding_model,
|
539 |
+
|
540 |
"max_tokens": args.max_tokens,
|
|
|
541 |
},
|
542 |
}
|
543 |
|
lightrag/api/ollama_lightrag_server.py
DELETED
@@ -1,491 +0,0 @@
|
|
1 |
-
from fastapi import FastAPI, HTTPException, File, UploadFile, Form
|
2 |
-
from pydantic import BaseModel
|
3 |
-
import logging
|
4 |
-
import argparse
|
5 |
-
from lightrag import LightRAG, QueryParam
|
6 |
-
from lightrag.llm import ollama_model_complete, ollama_embed
|
7 |
-
from lightrag.utils import EmbeddingFunc
|
8 |
-
from typing import Optional, List
|
9 |
-
from enum import Enum
|
10 |
-
from pathlib import Path
|
11 |
-
import shutil
|
12 |
-
import aiofiles
|
13 |
-
from ascii_colors import trace_exception
|
14 |
-
import os
|
15 |
-
|
16 |
-
from fastapi import Depends, Security
|
17 |
-
from fastapi.security import APIKeyHeader
|
18 |
-
from fastapi.middleware.cors import CORSMiddleware
|
19 |
-
|
20 |
-
from starlette.status import HTTP_403_FORBIDDEN
|
21 |
-
|
22 |
-
|
23 |
-
def parse_args():
|
24 |
-
parser = argparse.ArgumentParser(
|
25 |
-
description="LightRAG FastAPI Server with separate working and input directories"
|
26 |
-
)
|
27 |
-
|
28 |
-
# Server configuration
|
29 |
-
parser.add_argument(
|
30 |
-
"--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)"
|
31 |
-
)
|
32 |
-
parser.add_argument(
|
33 |
-
"--port", type=int, default=9621, help="Server port (default: 9621)"
|
34 |
-
)
|
35 |
-
|
36 |
-
# Directory configuration
|
37 |
-
parser.add_argument(
|
38 |
-
"--working-dir",
|
39 |
-
default="./rag_storage",
|
40 |
-
help="Working directory for RAG storage (default: ./rag_storage)",
|
41 |
-
)
|
42 |
-
parser.add_argument(
|
43 |
-
"--input-dir",
|
44 |
-
default="./inputs",
|
45 |
-
help="Directory containing input documents (default: ./inputs)",
|
46 |
-
)
|
47 |
-
|
48 |
-
# Model configuration
|
49 |
-
parser.add_argument(
|
50 |
-
"--model",
|
51 |
-
default="mistral-nemo:latest",
|
52 |
-
help="LLM model name (default: mistral-nemo:latest)",
|
53 |
-
)
|
54 |
-
parser.add_argument(
|
55 |
-
"--embedding-model",
|
56 |
-
default="bge-m3:latest",
|
57 |
-
help="Embedding model name (default: bge-m3:latest)",
|
58 |
-
)
|
59 |
-
parser.add_argument(
|
60 |
-
"--ollama-host",
|
61 |
-
default="http://localhost:11434",
|
62 |
-
help="Ollama host URL (default: http://localhost:11434)",
|
63 |
-
)
|
64 |
-
|
65 |
-
# RAG configuration
|
66 |
-
parser.add_argument(
|
67 |
-
"--max-async", type=int, default=4, help="Maximum async operations (default: 4)"
|
68 |
-
)
|
69 |
-
parser.add_argument(
|
70 |
-
"--max-tokens",
|
71 |
-
type=int,
|
72 |
-
default=32768,
|
73 |
-
help="Maximum token size (default: 32768)",
|
74 |
-
)
|
75 |
-
parser.add_argument(
|
76 |
-
"--embedding-dim",
|
77 |
-
type=int,
|
78 |
-
default=1024,
|
79 |
-
help="Embedding dimensions (default: 1024)",
|
80 |
-
)
|
81 |
-
parser.add_argument(
|
82 |
-
"--max-embed-tokens",
|
83 |
-
type=int,
|
84 |
-
default=8192,
|
85 |
-
help="Maximum embedding token size (default: 8192)",
|
86 |
-
)
|
87 |
-
|
88 |
-
# Logging configuration
|
89 |
-
parser.add_argument(
|
90 |
-
"--log-level",
|
91 |
-
default="INFO",
|
92 |
-
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
93 |
-
help="Logging level (default: INFO)",
|
94 |
-
)
|
95 |
-
parser.add_argument(
|
96 |
-
"--key",
|
97 |
-
type=str,
|
98 |
-
help="API key for authentication. This protects lightrag server against unauthorized access",
|
99 |
-
default=None,
|
100 |
-
)
|
101 |
-
|
102 |
-
return parser.parse_args()
|
103 |
-
|
104 |
-
|
105 |
-
class DocumentManager:
|
106 |
-
"""Handles document operations and tracking"""
|
107 |
-
|
108 |
-
def __init__(self, input_dir: str, supported_extensions: tuple = (".txt", ".md")):
|
109 |
-
self.input_dir = Path(input_dir)
|
110 |
-
self.supported_extensions = supported_extensions
|
111 |
-
self.indexed_files = set()
|
112 |
-
|
113 |
-
# Create input directory if it doesn't exist
|
114 |
-
self.input_dir.mkdir(parents=True, exist_ok=True)
|
115 |
-
|
116 |
-
def scan_directory(self) -> List[Path]:
|
117 |
-
"""Scan input directory for new files"""
|
118 |
-
new_files = []
|
119 |
-
for ext in self.supported_extensions:
|
120 |
-
for file_path in self.input_dir.rglob(f"*{ext}"):
|
121 |
-
if file_path not in self.indexed_files:
|
122 |
-
new_files.append(file_path)
|
123 |
-
return new_files
|
124 |
-
|
125 |
-
def mark_as_indexed(self, file_path: Path):
|
126 |
-
"""Mark a file as indexed"""
|
127 |
-
self.indexed_files.add(file_path)
|
128 |
-
|
129 |
-
def is_supported_file(self, filename: str) -> bool:
|
130 |
-
"""Check if file type is supported"""
|
131 |
-
return any(filename.lower().endswith(ext) for ext in self.supported_extensions)
|
132 |
-
|
133 |
-
|
134 |
-
# Pydantic models
|
135 |
-
class SearchMode(str, Enum):
|
136 |
-
naive = "naive"
|
137 |
-
local = "local"
|
138 |
-
global_ = "global"
|
139 |
-
hybrid = "hybrid"
|
140 |
-
|
141 |
-
|
142 |
-
class QueryRequest(BaseModel):
|
143 |
-
query: str
|
144 |
-
mode: SearchMode = SearchMode.hybrid
|
145 |
-
stream: bool = False
|
146 |
-
only_need_context: bool = False
|
147 |
-
|
148 |
-
|
149 |
-
class QueryResponse(BaseModel):
|
150 |
-
response: str
|
151 |
-
|
152 |
-
|
153 |
-
class InsertTextRequest(BaseModel):
|
154 |
-
text: str
|
155 |
-
description: Optional[str] = None
|
156 |
-
|
157 |
-
|
158 |
-
class InsertResponse(BaseModel):
|
159 |
-
status: str
|
160 |
-
message: str
|
161 |
-
document_count: int
|
162 |
-
|
163 |
-
|
164 |
-
def get_api_key_dependency(api_key: Optional[str]):
|
165 |
-
if not api_key:
|
166 |
-
# If no API key is configured, return a dummy dependency that always succeeds
|
167 |
-
async def no_auth():
|
168 |
-
return None
|
169 |
-
|
170 |
-
return no_auth
|
171 |
-
|
172 |
-
# If API key is configured, use proper authentication
|
173 |
-
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
174 |
-
|
175 |
-
async def api_key_auth(api_key_header_value: str | None = Security(api_key_header)):
|
176 |
-
if not api_key_header_value:
|
177 |
-
raise HTTPException(
|
178 |
-
status_code=HTTP_403_FORBIDDEN, detail="API Key required"
|
179 |
-
)
|
180 |
-
if api_key_header_value != api_key:
|
181 |
-
raise HTTPException(
|
182 |
-
status_code=HTTP_403_FORBIDDEN, detail="Invalid API Key"
|
183 |
-
)
|
184 |
-
return api_key_header_value
|
185 |
-
|
186 |
-
return api_key_auth
|
187 |
-
|
188 |
-
|
189 |
-
def create_app(args):
|
190 |
-
# Setup logging
|
191 |
-
logging.basicConfig(
|
192 |
-
format="%(levelname)s:%(message)s", level=getattr(logging, args.log_level)
|
193 |
-
)
|
194 |
-
|
195 |
-
# Check if API key is provided either through env var or args
|
196 |
-
api_key = os.getenv("LIGHTRAG_API_KEY") or args.key
|
197 |
-
|
198 |
-
# Initialize FastAPI
|
199 |
-
app = FastAPI(
|
200 |
-
title="LightRAG API",
|
201 |
-
description="API for querying text using LightRAG with separate storage and input directories"
|
202 |
-
+ "(With authentication)"
|
203 |
-
if api_key
|
204 |
-
else "",
|
205 |
-
version="1.0.0",
|
206 |
-
openapi_tags=[{"name": "api"}],
|
207 |
-
)
|
208 |
-
|
209 |
-
# Add CORS middleware
|
210 |
-
app.add_middleware(
|
211 |
-
CORSMiddleware,
|
212 |
-
allow_origins=["*"],
|
213 |
-
allow_credentials=True,
|
214 |
-
allow_methods=["*"],
|
215 |
-
allow_headers=["*"],
|
216 |
-
)
|
217 |
-
|
218 |
-
# Create the optional API key dependency
|
219 |
-
optional_api_key = get_api_key_dependency(api_key)
|
220 |
-
|
221 |
-
# Create working directory if it doesn't exist
|
222 |
-
Path(args.working_dir).mkdir(parents=True, exist_ok=True)
|
223 |
-
|
224 |
-
# Initialize document manager
|
225 |
-
doc_manager = DocumentManager(args.input_dir)
|
226 |
-
|
227 |
-
# Initialize RAG
|
228 |
-
rag = LightRAG(
|
229 |
-
working_dir=args.working_dir,
|
230 |
-
llm_model_func=ollama_model_complete,
|
231 |
-
llm_model_name=args.model,
|
232 |
-
llm_model_max_async=args.max_async,
|
233 |
-
llm_model_max_token_size=args.max_tokens,
|
234 |
-
llm_model_kwargs={
|
235 |
-
"host": args.ollama_host,
|
236 |
-
"options": {"num_ctx": args.max_tokens},
|
237 |
-
},
|
238 |
-
embedding_func=EmbeddingFunc(
|
239 |
-
embedding_dim=args.embedding_dim,
|
240 |
-
max_token_size=args.max_embed_tokens,
|
241 |
-
func=lambda texts: ollama_embed(
|
242 |
-
texts, embed_model=args.embedding_model, host=args.ollama_host
|
243 |
-
),
|
244 |
-
),
|
245 |
-
)
|
246 |
-
|
247 |
-
@app.on_event("startup")
|
248 |
-
async def startup_event():
|
249 |
-
"""Index all files in input directory during startup"""
|
250 |
-
try:
|
251 |
-
new_files = doc_manager.scan_directory()
|
252 |
-
for file_path in new_files:
|
253 |
-
try:
|
254 |
-
# Use async file reading
|
255 |
-
async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
|
256 |
-
content = await f.read()
|
257 |
-
# Use the async version of insert directly
|
258 |
-
await rag.ainsert(content)
|
259 |
-
doc_manager.mark_as_indexed(file_path)
|
260 |
-
logging.info(f"Indexed file: {file_path}")
|
261 |
-
except Exception as e:
|
262 |
-
trace_exception(e)
|
263 |
-
logging.error(f"Error indexing file {file_path}: {str(e)}")
|
264 |
-
|
265 |
-
logging.info(f"Indexed {len(new_files)} documents from {args.input_dir}")
|
266 |
-
|
267 |
-
except Exception as e:
|
268 |
-
logging.error(f"Error during startup indexing: {str(e)}")
|
269 |
-
|
270 |
-
@app.post("/documents/scan", dependencies=[Depends(optional_api_key)])
|
271 |
-
async def scan_for_new_documents():
|
272 |
-
"""Manually trigger scanning for new documents"""
|
273 |
-
try:
|
274 |
-
new_files = doc_manager.scan_directory()
|
275 |
-
indexed_count = 0
|
276 |
-
|
277 |
-
for file_path in new_files:
|
278 |
-
try:
|
279 |
-
with open(file_path, "r", encoding="utf-8") as f:
|
280 |
-
content = f.read()
|
281 |
-
await rag.ainsert(content)
|
282 |
-
doc_manager.mark_as_indexed(file_path)
|
283 |
-
indexed_count += 1
|
284 |
-
except Exception as e:
|
285 |
-
logging.error(f"Error indexing file {file_path}: {str(e)}")
|
286 |
-
|
287 |
-
return {
|
288 |
-
"status": "success",
|
289 |
-
"indexed_count": indexed_count,
|
290 |
-
"total_documents": len(doc_manager.indexed_files),
|
291 |
-
}
|
292 |
-
except Exception as e:
|
293 |
-
raise HTTPException(status_code=500, detail=str(e))
|
294 |
-
|
295 |
-
@app.post("/documents/upload", dependencies=[Depends(optional_api_key)])
|
296 |
-
async def upload_to_input_dir(file: UploadFile = File(...)):
|
297 |
-
"""Upload a file to the input directory"""
|
298 |
-
try:
|
299 |
-
if not doc_manager.is_supported_file(file.filename):
|
300 |
-
raise HTTPException(
|
301 |
-
status_code=400,
|
302 |
-
detail=f"Unsupported file type. Supported types: {doc_manager.supported_extensions}",
|
303 |
-
)
|
304 |
-
|
305 |
-
file_path = doc_manager.input_dir / file.filename
|
306 |
-
with open(file_path, "wb") as buffer:
|
307 |
-
shutil.copyfileobj(file.file, buffer)
|
308 |
-
|
309 |
-
# Immediately index the uploaded file
|
310 |
-
with open(file_path, "r", encoding="utf-8") as f:
|
311 |
-
content = f.read()
|
312 |
-
await rag.ainsert(content)
|
313 |
-
doc_manager.mark_as_indexed(file_path)
|
314 |
-
|
315 |
-
return {
|
316 |
-
"status": "success",
|
317 |
-
"message": f"File uploaded and indexed: {file.filename}",
|
318 |
-
"total_documents": len(doc_manager.indexed_files),
|
319 |
-
}
|
320 |
-
except Exception as e:
|
321 |
-
raise HTTPException(status_code=500, detail=str(e))
|
322 |
-
|
323 |
-
@app.post(
|
324 |
-
"/query", response_model=QueryResponse, dependencies=[Depends(optional_api_key)]
|
325 |
-
)
|
326 |
-
async def query_text(request: QueryRequest):
|
327 |
-
try:
|
328 |
-
response = await rag.aquery(
|
329 |
-
request.query,
|
330 |
-
param=QueryParam(
|
331 |
-
mode=request.mode,
|
332 |
-
stream=request.stream,
|
333 |
-
only_need_context=request.only_need_context,
|
334 |
-
),
|
335 |
-
)
|
336 |
-
|
337 |
-
if request.stream:
|
338 |
-
result = ""
|
339 |
-
async for chunk in response:
|
340 |
-
result += chunk
|
341 |
-
return QueryResponse(response=result)
|
342 |
-
else:
|
343 |
-
return QueryResponse(response=response)
|
344 |
-
except Exception as e:
|
345 |
-
raise HTTPException(status_code=500, detail=str(e))
|
346 |
-
|
347 |
-
@app.post("/query/stream", dependencies=[Depends(optional_api_key)])
|
348 |
-
async def query_text_stream(request: QueryRequest):
|
349 |
-
try:
|
350 |
-
response = rag.query(
|
351 |
-
request.query,
|
352 |
-
param=QueryParam(
|
353 |
-
mode=request.mode,
|
354 |
-
stream=True,
|
355 |
-
only_need_context=request.only_need_context,
|
356 |
-
),
|
357 |
-
)
|
358 |
-
|
359 |
-
async def stream_generator():
|
360 |
-
async for chunk in response:
|
361 |
-
yield chunk
|
362 |
-
|
363 |
-
return stream_generator()
|
364 |
-
except Exception as e:
|
365 |
-
raise HTTPException(status_code=500, detail=str(e))
|
366 |
-
|
367 |
-
@app.post(
|
368 |
-
"/documents/text",
|
369 |
-
response_model=InsertResponse,
|
370 |
-
dependencies=[Depends(optional_api_key)],
|
371 |
-
)
|
372 |
-
async def insert_text(request: InsertTextRequest):
|
373 |
-
try:
|
374 |
-
await rag.ainsert(request.text)
|
375 |
-
return InsertResponse(
|
376 |
-
status="success",
|
377 |
-
message="Text successfully inserted",
|
378 |
-
document_count=len(rag),
|
379 |
-
)
|
380 |
-
except Exception as e:
|
381 |
-
raise HTTPException(status_code=500, detail=str(e))
|
382 |
-
|
383 |
-
@app.post(
|
384 |
-
"/documents/file",
|
385 |
-
response_model=InsertResponse,
|
386 |
-
dependencies=[Depends(optional_api_key)],
|
387 |
-
)
|
388 |
-
async def insert_file(file: UploadFile = File(...), description: str = Form(None)):
|
389 |
-
try:
|
390 |
-
content = await file.read()
|
391 |
-
|
392 |
-
if file.filename.endswith((".txt", ".md")):
|
393 |
-
text = content.decode("utf-8")
|
394 |
-
await rag.ainsert(text)
|
395 |
-
else:
|
396 |
-
raise HTTPException(
|
397 |
-
status_code=400,
|
398 |
-
detail="Unsupported file type. Only .txt and .md files are supported",
|
399 |
-
)
|
400 |
-
|
401 |
-
return InsertResponse(
|
402 |
-
status="success",
|
403 |
-
message=f"File '{file.filename}' successfully inserted",
|
404 |
-
document_count=1,
|
405 |
-
)
|
406 |
-
except UnicodeDecodeError:
|
407 |
-
raise HTTPException(status_code=400, detail="File encoding not supported")
|
408 |
-
except Exception as e:
|
409 |
-
raise HTTPException(status_code=500, detail=str(e))
|
410 |
-
|
411 |
-
@app.post(
|
412 |
-
"/documents/batch",
|
413 |
-
response_model=InsertResponse,
|
414 |
-
dependencies=[Depends(optional_api_key)],
|
415 |
-
)
|
416 |
-
async def insert_batch(files: List[UploadFile] = File(...)):
|
417 |
-
try:
|
418 |
-
inserted_count = 0
|
419 |
-
failed_files = []
|
420 |
-
|
421 |
-
for file in files:
|
422 |
-
try:
|
423 |
-
content = await file.read()
|
424 |
-
if file.filename.endswith((".txt", ".md")):
|
425 |
-
text = content.decode("utf-8")
|
426 |
-
await rag.ainsert(text)
|
427 |
-
inserted_count += 1
|
428 |
-
else:
|
429 |
-
failed_files.append(f"{file.filename} (unsupported type)")
|
430 |
-
except Exception as e:
|
431 |
-
failed_files.append(f"{file.filename} ({str(e)})")
|
432 |
-
|
433 |
-
status_message = f"Successfully inserted {inserted_count} documents"
|
434 |
-
if failed_files:
|
435 |
-
status_message += f". Failed files: {', '.join(failed_files)}"
|
436 |
-
|
437 |
-
return InsertResponse(
|
438 |
-
status="success" if inserted_count > 0 else "partial_success",
|
439 |
-
message=status_message,
|
440 |
-
document_count=len(files),
|
441 |
-
)
|
442 |
-
except Exception as e:
|
443 |
-
raise HTTPException(status_code=500, detail=str(e))
|
444 |
-
|
445 |
-
@app.delete(
|
446 |
-
"/documents",
|
447 |
-
response_model=InsertResponse,
|
448 |
-
dependencies=[Depends(optional_api_key)],
|
449 |
-
)
|
450 |
-
async def clear_documents():
|
451 |
-
try:
|
452 |
-
rag.text_chunks = []
|
453 |
-
rag.entities_vdb = None
|
454 |
-
rag.relationships_vdb = None
|
455 |
-
return InsertResponse(
|
456 |
-
status="success",
|
457 |
-
message="All documents cleared successfully",
|
458 |
-
document_count=0,
|
459 |
-
)
|
460 |
-
except Exception as e:
|
461 |
-
raise HTTPException(status_code=500, detail=str(e))
|
462 |
-
|
463 |
-
@app.get("/health", dependencies=[Depends(optional_api_key)])
|
464 |
-
async def get_status():
|
465 |
-
"""Get current system status"""
|
466 |
-
return {
|
467 |
-
"status": "healthy",
|
468 |
-
"working_directory": str(args.working_dir),
|
469 |
-
"input_directory": str(args.input_dir),
|
470 |
-
"indexed_files": len(doc_manager.indexed_files),
|
471 |
-
"configuration": {
|
472 |
-
"model": args.model,
|
473 |
-
"embedding_model": args.embedding_model,
|
474 |
-
"max_tokens": args.max_tokens,
|
475 |
-
"ollama_host": args.ollama_host,
|
476 |
-
},
|
477 |
-
}
|
478 |
-
|
479 |
-
return app
|
480 |
-
|
481 |
-
|
482 |
-
def main():
|
483 |
-
args = parse_args()
|
484 |
-
import uvicorn
|
485 |
-
|
486 |
-
app = create_app(args)
|
487 |
-
uvicorn.run(app, host=args.host, port=args.port)
|
488 |
-
|
489 |
-
|
490 |
-
if __name__ == "__main__":
|
491 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lightrag/api/openai_lightrag_server.py
DELETED
@@ -1,506 +0,0 @@
|
|
1 |
-
from fastapi import FastAPI, HTTPException, File, UploadFile, Form
|
2 |
-
from pydantic import BaseModel
|
3 |
-
import asyncio
|
4 |
-
import logging
|
5 |
-
import argparse
|
6 |
-
from lightrag import LightRAG, QueryParam
|
7 |
-
from lightrag.llm import openai_complete_if_cache, openai_embedding
|
8 |
-
from lightrag.utils import EmbeddingFunc
|
9 |
-
from typing import Optional, List
|
10 |
-
from enum import Enum
|
11 |
-
from pathlib import Path
|
12 |
-
import shutil
|
13 |
-
import aiofiles
|
14 |
-
from ascii_colors import trace_exception
|
15 |
-
import nest_asyncio
|
16 |
-
|
17 |
-
import os
|
18 |
-
|
19 |
-
from fastapi import Depends, Security
|
20 |
-
from fastapi.security import APIKeyHeader
|
21 |
-
from fastapi.middleware.cors import CORSMiddleware
|
22 |
-
|
23 |
-
from starlette.status import HTTP_403_FORBIDDEN
|
24 |
-
|
25 |
-
# Apply nest_asyncio to solve event loop issues
|
26 |
-
nest_asyncio.apply()
|
27 |
-
|
28 |
-
|
29 |
-
def parse_args():
|
30 |
-
parser = argparse.ArgumentParser(
|
31 |
-
description="LightRAG FastAPI Server with OpenAI integration"
|
32 |
-
)
|
33 |
-
|
34 |
-
# Server configuration
|
35 |
-
parser.add_argument(
|
36 |
-
"--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)"
|
37 |
-
)
|
38 |
-
parser.add_argument(
|
39 |
-
"--port", type=int, default=9621, help="Server port (default: 9621)"
|
40 |
-
)
|
41 |
-
|
42 |
-
# Directory configuration
|
43 |
-
parser.add_argument(
|
44 |
-
"--working-dir",
|
45 |
-
default="./rag_storage",
|
46 |
-
help="Working directory for RAG storage (default: ./rag_storage)",
|
47 |
-
)
|
48 |
-
parser.add_argument(
|
49 |
-
"--input-dir",
|
50 |
-
default="./inputs",
|
51 |
-
help="Directory containing input documents (default: ./inputs)",
|
52 |
-
)
|
53 |
-
|
54 |
-
# Model configuration
|
55 |
-
parser.add_argument(
|
56 |
-
"--model", default="gpt-4", help="OpenAI model name (default: gpt-4)"
|
57 |
-
)
|
58 |
-
parser.add_argument(
|
59 |
-
"--embedding-model",
|
60 |
-
default="text-embedding-3-large",
|
61 |
-
help="OpenAI embedding model (default: text-embedding-3-large)",
|
62 |
-
)
|
63 |
-
|
64 |
-
# RAG configuration
|
65 |
-
parser.add_argument(
|
66 |
-
"--max-tokens",
|
67 |
-
type=int,
|
68 |
-
default=32768,
|
69 |
-
help="Maximum token size (default: 32768)",
|
70 |
-
)
|
71 |
-
parser.add_argument(
|
72 |
-
"--max-embed-tokens",
|
73 |
-
type=int,
|
74 |
-
default=8192,
|
75 |
-
help="Maximum embedding token size (default: 8192)",
|
76 |
-
)
|
77 |
-
|
78 |
-
# Logging configuration
|
79 |
-
parser.add_argument(
|
80 |
-
"--log-level",
|
81 |
-
default="INFO",
|
82 |
-
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
83 |
-
help="Logging level (default: INFO)",
|
84 |
-
)
|
85 |
-
|
86 |
-
parser.add_argument(
|
87 |
-
"--key",
|
88 |
-
type=str,
|
89 |
-
help="API key for authentication. This protects lightrag server against unauthorized access",
|
90 |
-
default=None,
|
91 |
-
)
|
92 |
-
|
93 |
-
return parser.parse_args()
|
94 |
-
|
95 |
-
|
96 |
-
class DocumentManager:
|
97 |
-
"""Handles document operations and tracking"""
|
98 |
-
|
99 |
-
def __init__(self, input_dir: str, supported_extensions: tuple = (".txt", ".md")):
|
100 |
-
self.input_dir = Path(input_dir)
|
101 |
-
self.supported_extensions = supported_extensions
|
102 |
-
self.indexed_files = set()
|
103 |
-
|
104 |
-
# Create input directory if it doesn't exist
|
105 |
-
self.input_dir.mkdir(parents=True, exist_ok=True)
|
106 |
-
|
107 |
-
def scan_directory(self) -> List[Path]:
|
108 |
-
"""Scan input directory for new files"""
|
109 |
-
new_files = []
|
110 |
-
for ext in self.supported_extensions:
|
111 |
-
for file_path in self.input_dir.rglob(f"*{ext}"):
|
112 |
-
if file_path not in self.indexed_files:
|
113 |
-
new_files.append(file_path)
|
114 |
-
return new_files
|
115 |
-
|
116 |
-
def mark_as_indexed(self, file_path: Path):
|
117 |
-
"""Mark a file as indexed"""
|
118 |
-
self.indexed_files.add(file_path)
|
119 |
-
|
120 |
-
def is_supported_file(self, filename: str) -> bool:
|
121 |
-
"""Check if file type is supported"""
|
122 |
-
return any(filename.lower().endswith(ext) for ext in self.supported_extensions)
|
123 |
-
|
124 |
-
|
125 |
-
# Pydantic models
|
126 |
-
class SearchMode(str, Enum):
|
127 |
-
naive = "naive"
|
128 |
-
local = "local"
|
129 |
-
global_ = "global"
|
130 |
-
hybrid = "hybrid"
|
131 |
-
|
132 |
-
|
133 |
-
class QueryRequest(BaseModel):
|
134 |
-
query: str
|
135 |
-
mode: SearchMode = SearchMode.hybrid
|
136 |
-
stream: bool = False
|
137 |
-
only_need_context: bool = False
|
138 |
-
|
139 |
-
|
140 |
-
class QueryResponse(BaseModel):
|
141 |
-
response: str
|
142 |
-
|
143 |
-
|
144 |
-
class InsertTextRequest(BaseModel):
|
145 |
-
text: str
|
146 |
-
description: Optional[str] = None
|
147 |
-
|
148 |
-
|
149 |
-
class InsertResponse(BaseModel):
|
150 |
-
status: str
|
151 |
-
message: str
|
152 |
-
document_count: int
|
153 |
-
|
154 |
-
|
155 |
-
def get_api_key_dependency(api_key: Optional[str]):
|
156 |
-
if not api_key:
|
157 |
-
# If no API key is configured, return a dummy dependency that always succeeds
|
158 |
-
async def no_auth():
|
159 |
-
return None
|
160 |
-
|
161 |
-
return no_auth
|
162 |
-
|
163 |
-
# If API key is configured, use proper authentication
|
164 |
-
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
165 |
-
|
166 |
-
async def api_key_auth(api_key_header_value: str | None = Security(api_key_header)):
|
167 |
-
if not api_key_header_value:
|
168 |
-
raise HTTPException(
|
169 |
-
status_code=HTTP_403_FORBIDDEN, detail="API Key required"
|
170 |
-
)
|
171 |
-
if api_key_header_value != api_key:
|
172 |
-
raise HTTPException(
|
173 |
-
status_code=HTTP_403_FORBIDDEN, detail="Invalid API Key"
|
174 |
-
)
|
175 |
-
return api_key_header_value
|
176 |
-
|
177 |
-
return api_key_auth
|
178 |
-
|
179 |
-
|
180 |
-
async def get_embedding_dim(embedding_model: str) -> int:
|
181 |
-
"""Get embedding dimensions for the specified model"""
|
182 |
-
test_text = ["This is a test sentence."]
|
183 |
-
embedding = await openai_embedding(test_text, model=embedding_model)
|
184 |
-
return embedding.shape[1]
|
185 |
-
|
186 |
-
|
187 |
-
def create_app(args):
|
188 |
-
# Setup logging
|
189 |
-
logging.basicConfig(
|
190 |
-
format="%(levelname)s:%(message)s", level=getattr(logging, args.log_level)
|
191 |
-
)
|
192 |
-
|
193 |
-
# Check if API key is provided either through env var or args
|
194 |
-
api_key = os.getenv("LIGHTRAG_API_KEY") or args.key
|
195 |
-
|
196 |
-
# Initialize FastAPI
|
197 |
-
app = FastAPI(
|
198 |
-
title="LightRAG API",
|
199 |
-
description="API for querying text using LightRAG with separate storage and input directories"
|
200 |
-
+ "(With authentication)"
|
201 |
-
if api_key
|
202 |
-
else "",
|
203 |
-
version="1.0.0",
|
204 |
-
openapi_tags=[{"name": "api"}],
|
205 |
-
)
|
206 |
-
|
207 |
-
# Add CORS middleware
|
208 |
-
app.add_middleware(
|
209 |
-
CORSMiddleware,
|
210 |
-
allow_origins=["*"],
|
211 |
-
allow_credentials=True,
|
212 |
-
allow_methods=["*"],
|
213 |
-
allow_headers=["*"],
|
214 |
-
)
|
215 |
-
|
216 |
-
# Create the optional API key dependency
|
217 |
-
optional_api_key = get_api_key_dependency(api_key)
|
218 |
-
|
219 |
-
# Add CORS middleware
|
220 |
-
app.add_middleware(
|
221 |
-
CORSMiddleware,
|
222 |
-
allow_origins=["*"],
|
223 |
-
allow_credentials=True,
|
224 |
-
allow_methods=["*"],
|
225 |
-
allow_headers=["*"],
|
226 |
-
)
|
227 |
-
|
228 |
-
# Create working directory if it doesn't exist
|
229 |
-
Path(args.working_dir).mkdir(parents=True, exist_ok=True)
|
230 |
-
|
231 |
-
# Initialize document manager
|
232 |
-
doc_manager = DocumentManager(args.input_dir)
|
233 |
-
|
234 |
-
# Get embedding dimensions
|
235 |
-
embedding_dim = asyncio.run(get_embedding_dim(args.embedding_model))
|
236 |
-
|
237 |
-
async def async_openai_complete(
|
238 |
-
prompt, system_prompt=None, history_messages=[], **kwargs
|
239 |
-
):
|
240 |
-
"""Async wrapper for OpenAI completion"""
|
241 |
-
return await openai_complete_if_cache(
|
242 |
-
args.model,
|
243 |
-
prompt,
|
244 |
-
system_prompt=system_prompt,
|
245 |
-
history_messages=history_messages,
|
246 |
-
**kwargs,
|
247 |
-
)
|
248 |
-
|
249 |
-
# Initialize RAG with OpenAI configuration
|
250 |
-
rag = LightRAG(
|
251 |
-
working_dir=args.working_dir,
|
252 |
-
llm_model_func=async_openai_complete,
|
253 |
-
llm_model_name=args.model,
|
254 |
-
llm_model_max_token_size=args.max_tokens,
|
255 |
-
embedding_func=EmbeddingFunc(
|
256 |
-
embedding_dim=embedding_dim,
|
257 |
-
max_token_size=args.max_embed_tokens,
|
258 |
-
func=lambda texts: openai_embedding(texts, model=args.embedding_model),
|
259 |
-
),
|
260 |
-
)
|
261 |
-
|
262 |
-
@app.on_event("startup")
|
263 |
-
async def startup_event():
|
264 |
-
"""Index all files in input directory during startup"""
|
265 |
-
try:
|
266 |
-
new_files = doc_manager.scan_directory()
|
267 |
-
for file_path in new_files:
|
268 |
-
try:
|
269 |
-
# Use async file reading
|
270 |
-
async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
|
271 |
-
content = await f.read()
|
272 |
-
# Use the async version of insert directly
|
273 |
-
await rag.ainsert(content)
|
274 |
-
doc_manager.mark_as_indexed(file_path)
|
275 |
-
logging.info(f"Indexed file: {file_path}")
|
276 |
-
except Exception as e:
|
277 |
-
trace_exception(e)
|
278 |
-
logging.error(f"Error indexing file {file_path}: {str(e)}")
|
279 |
-
|
280 |
-
logging.info(f"Indexed {len(new_files)} documents from {args.input_dir}")
|
281 |
-
|
282 |
-
except Exception as e:
|
283 |
-
logging.error(f"Error during startup indexing: {str(e)}")
|
284 |
-
|
285 |
-
@app.post("/documents/scan", dependencies=[Depends(optional_api_key)])
|
286 |
-
async def scan_for_new_documents():
|
287 |
-
"""Manually trigger scanning for new documents"""
|
288 |
-
try:
|
289 |
-
new_files = doc_manager.scan_directory()
|
290 |
-
indexed_count = 0
|
291 |
-
|
292 |
-
for file_path in new_files:
|
293 |
-
try:
|
294 |
-
with open(file_path, "r", encoding="utf-8") as f:
|
295 |
-
content = f.read()
|
296 |
-
rag.insert(content)
|
297 |
-
doc_manager.mark_as_indexed(file_path)
|
298 |
-
indexed_count += 1
|
299 |
-
except Exception as e:
|
300 |
-
logging.error(f"Error indexing file {file_path}: {str(e)}")
|
301 |
-
|
302 |
-
return {
|
303 |
-
"status": "success",
|
304 |
-
"indexed_count": indexed_count,
|
305 |
-
"total_documents": len(doc_manager.indexed_files),
|
306 |
-
}
|
307 |
-
except Exception as e:
|
308 |
-
raise HTTPException(status_code=500, detail=str(e))
|
309 |
-
|
310 |
-
@app.post("/documents/upload", dependencies=[Depends(optional_api_key)])
|
311 |
-
async def upload_to_input_dir(file: UploadFile = File(...)):
|
312 |
-
"""Upload a file to the input directory"""
|
313 |
-
try:
|
314 |
-
if not doc_manager.is_supported_file(file.filename):
|
315 |
-
raise HTTPException(
|
316 |
-
status_code=400,
|
317 |
-
detail=f"Unsupported file type. Supported types: {doc_manager.supported_extensions}",
|
318 |
-
)
|
319 |
-
|
320 |
-
file_path = doc_manager.input_dir / file.filename
|
321 |
-
with open(file_path, "wb") as buffer:
|
322 |
-
shutil.copyfileobj(file.file, buffer)
|
323 |
-
|
324 |
-
# Immediately index the uploaded file
|
325 |
-
with open(file_path, "r", encoding="utf-8") as f:
|
326 |
-
content = f.read()
|
327 |
-
rag.insert(content)
|
328 |
-
doc_manager.mark_as_indexed(file_path)
|
329 |
-
|
330 |
-
return {
|
331 |
-
"status": "success",
|
332 |
-
"message": f"File uploaded and indexed: {file.filename}",
|
333 |
-
"total_documents": len(doc_manager.indexed_files),
|
334 |
-
}
|
335 |
-
except Exception as e:
|
336 |
-
raise HTTPException(status_code=500, detail=str(e))
|
337 |
-
|
338 |
-
@app.post(
|
339 |
-
"/query", response_model=QueryResponse, dependencies=[Depends(optional_api_key)]
|
340 |
-
)
|
341 |
-
async def query_text(request: QueryRequest):
|
342 |
-
try:
|
343 |
-
response = await rag.aquery(
|
344 |
-
request.query,
|
345 |
-
param=QueryParam(
|
346 |
-
mode=request.mode,
|
347 |
-
stream=request.stream,
|
348 |
-
only_need_context=request.only_need_context,
|
349 |
-
),
|
350 |
-
)
|
351 |
-
|
352 |
-
if request.stream:
|
353 |
-
result = ""
|
354 |
-
async for chunk in response:
|
355 |
-
result += chunk
|
356 |
-
return QueryResponse(response=result)
|
357 |
-
else:
|
358 |
-
return QueryResponse(response=response)
|
359 |
-
except Exception as e:
|
360 |
-
raise HTTPException(status_code=500, detail=str(e))
|
361 |
-
|
362 |
-
@app.post("/query/stream", dependencies=[Depends(optional_api_key)])
|
363 |
-
async def query_text_stream(request: QueryRequest):
|
364 |
-
try:
|
365 |
-
response = rag.query(
|
366 |
-
request.query,
|
367 |
-
param=QueryParam(
|
368 |
-
mode=request.mode,
|
369 |
-
stream=True,
|
370 |
-
only_need_context=request.only_need_context,
|
371 |
-
),
|
372 |
-
)
|
373 |
-
|
374 |
-
async def stream_generator():
|
375 |
-
async for chunk in response:
|
376 |
-
yield chunk
|
377 |
-
|
378 |
-
return stream_generator()
|
379 |
-
except Exception as e:
|
380 |
-
raise HTTPException(status_code=500, detail=str(e))
|
381 |
-
|
382 |
-
@app.post(
|
383 |
-
"/documents/text",
|
384 |
-
response_model=InsertResponse,
|
385 |
-
dependencies=[Depends(optional_api_key)],
|
386 |
-
)
|
387 |
-
async def insert_text(request: InsertTextRequest):
|
388 |
-
try:
|
389 |
-
rag.insert(request.text)
|
390 |
-
return InsertResponse(
|
391 |
-
status="success",
|
392 |
-
message="Text successfully inserted",
|
393 |
-
document_count=len(rag),
|
394 |
-
)
|
395 |
-
except Exception as e:
|
396 |
-
raise HTTPException(status_code=500, detail=str(e))
|
397 |
-
|
398 |
-
@app.post(
|
399 |
-
"/documents/file",
|
400 |
-
response_model=InsertResponse,
|
401 |
-
dependencies=[Depends(optional_api_key)],
|
402 |
-
)
|
403 |
-
async def insert_file(file: UploadFile = File(...), description: str = Form(None)):
|
404 |
-
try:
|
405 |
-
content = await file.read()
|
406 |
-
|
407 |
-
if file.filename.endswith((".txt", ".md")):
|
408 |
-
text = content.decode("utf-8")
|
409 |
-
rag.insert(text)
|
410 |
-
else:
|
411 |
-
raise HTTPException(
|
412 |
-
status_code=400,
|
413 |
-
detail="Unsupported file type. Only .txt and .md files are supported",
|
414 |
-
)
|
415 |
-
|
416 |
-
return InsertResponse(
|
417 |
-
status="success",
|
418 |
-
message=f"File '{file.filename}' successfully inserted",
|
419 |
-
document_count=1,
|
420 |
-
)
|
421 |
-
except UnicodeDecodeError:
|
422 |
-
raise HTTPException(status_code=400, detail="File encoding not supported")
|
423 |
-
except Exception as e:
|
424 |
-
raise HTTPException(status_code=500, detail=str(e))
|
425 |
-
|
426 |
-
@app.post(
|
427 |
-
"/documents/batch",
|
428 |
-
response_model=InsertResponse,
|
429 |
-
dependencies=[Depends(optional_api_key)],
|
430 |
-
)
|
431 |
-
async def insert_batch(files: List[UploadFile] = File(...)):
|
432 |
-
try:
|
433 |
-
inserted_count = 0
|
434 |
-
failed_files = []
|
435 |
-
|
436 |
-
for file in files:
|
437 |
-
try:
|
438 |
-
content = await file.read()
|
439 |
-
if file.filename.endswith((".txt", ".md")):
|
440 |
-
text = content.decode("utf-8")
|
441 |
-
rag.insert(text)
|
442 |
-
inserted_count += 1
|
443 |
-
else:
|
444 |
-
failed_files.append(f"{file.filename} (unsupported type)")
|
445 |
-
except Exception as e:
|
446 |
-
failed_files.append(f"{file.filename} ({str(e)})")
|
447 |
-
|
448 |
-
status_message = f"Successfully inserted {inserted_count} documents"
|
449 |
-
if failed_files:
|
450 |
-
status_message += f". Failed files: {', '.join(failed_files)}"
|
451 |
-
|
452 |
-
return InsertResponse(
|
453 |
-
status="success" if inserted_count > 0 else "partial_success",
|
454 |
-
message=status_message,
|
455 |
-
document_count=len(files),
|
456 |
-
)
|
457 |
-
except Exception as e:
|
458 |
-
raise HTTPException(status_code=500, detail=str(e))
|
459 |
-
|
460 |
-
@app.delete(
|
461 |
-
"/documents",
|
462 |
-
response_model=InsertResponse,
|
463 |
-
dependencies=[Depends(optional_api_key)],
|
464 |
-
)
|
465 |
-
async def clear_documents():
|
466 |
-
try:
|
467 |
-
rag.text_chunks = []
|
468 |
-
rag.entities_vdb = None
|
469 |
-
rag.relationships_vdb = None
|
470 |
-
return InsertResponse(
|
471 |
-
status="success",
|
472 |
-
message="All documents cleared successfully",
|
473 |
-
document_count=0,
|
474 |
-
)
|
475 |
-
except Exception as e:
|
476 |
-
raise HTTPException(status_code=500, detail=str(e))
|
477 |
-
|
478 |
-
@app.get("/health", dependencies=[Depends(optional_api_key)])
|
479 |
-
async def get_status():
|
480 |
-
"""Get current system status"""
|
481 |
-
return {
|
482 |
-
"status": "healthy",
|
483 |
-
"working_directory": str(args.working_dir),
|
484 |
-
"input_directory": str(args.input_dir),
|
485 |
-
"indexed_files": len(doc_manager.indexed_files),
|
486 |
-
"configuration": {
|
487 |
-
"model": args.model,
|
488 |
-
"embedding_model": args.embedding_model,
|
489 |
-
"max_tokens": args.max_tokens,
|
490 |
-
"embedding_dim": embedding_dim,
|
491 |
-
},
|
492 |
-
}
|
493 |
-
|
494 |
-
return app
|
495 |
-
|
496 |
-
|
497 |
-
def main():
|
498 |
-
args = parse_args()
|
499 |
-
import uvicorn
|
500 |
-
|
501 |
-
app = create_app(args)
|
502 |
-
uvicorn.run(app, host=args.host, port=args.port)
|
503 |
-
|
504 |
-
|
505 |
-
if __name__ == "__main__":
|
506 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setup.py
CHANGED
@@ -100,10 +100,7 @@ setuptools.setup(
|
|
100 |
},
|
101 |
entry_points={
|
102 |
"console_scripts": [
|
103 |
-
"
|
104 |
-
"ollama-lightrag-server=lightrag.api.ollama_lightrag_server:main [api]",
|
105 |
-
"openai-lightrag-server=lightrag.api.openai_lightrag_server:main [api]",
|
106 |
-
"azure-openai-lightrag-server=lightrag.api.azure_openai_lightrag_server:main [api]",
|
107 |
],
|
108 |
},
|
109 |
)
|
|
|
100 |
},
|
101 |
entry_points={
|
102 |
"console_scripts": [
|
103 |
+
"lightrag-server=lightrag.api.lightrag_server:main [api]",
|
|
|
|
|
|
|
104 |
],
|
105 |
},
|
106 |
)
|