ParisNeo commited on
Commit
4fd33d8
·
1 Parent(s): 27408d4

Simplified the api services issue #565

Browse files
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 --lollms-host if running on a different host/port
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
- #### LoLLMs Server Options
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
- | --lollms-host | http://localhost:9600 | LoLLMS backend URL |
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
- #### OpenAI Server Options
 
993
 
994
- | Parameter | Default | Description |
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
- #### OpenAI AZURE Server Options
1008
 
1009
- | Parameter | Default | Description |
1010
- |-----------|---------|-------------|
1011
- | --host | 0.0.0.0 | Server host |
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
- For protecting the server using an authentication key, you can also use an environment variable named `LIGHTRAG_API_KEY`.
1025
- ### Example Usage
1026
 
1027
- #### LoLLMs RAG Server
 
 
 
 
1028
 
1029
  ```bash
1030
- # Custom configuration with specific model and working directory
1031
- lollms-lightrag-server --model mistral-nemo --port 8080 --working-dir ./custom_rag
1032
 
1033
- # Using specific models (ensure they are installed in your LoLLMs instance)
1034
- lollms-lightrag-server --model mistral-nemo:latest --embedding-model bge-m3 --embedding-dim 1024
1035
 
1036
- # Using specific models and an authentication key
1037
- lollms-lightrag-server --model mistral-nemo:latest --embedding-model bge-m3 --embedding-dim 1024 --key ky-mykey
1038
 
 
 
1039
  ```
1040
 
1041
- #### Ollama RAG Server
 
1042
 
1043
  ```bash
1044
- # Custom configuration with specific model and working directory
1045
- ollama-lightrag-server --model mistral-nemo:latest --port 8080 --working-dir ./custom_rag
1046
 
1047
- # Using specific models (ensure they are installed in your Ollama instance)
1048
- ollama-lightrag-server --model mistral-nemo:latest --embedding-model bge-m3 --embedding-dim 1024
 
 
 
1049
  ```
1050
 
1051
- #### OpenAI RAG Server
1052
 
1053
  ```bash
1054
- # Using GPT-4 with text-embedding-3-large
1055
- openai-lightrag-server --port 9624 --model gpt-4 --embedding-model text-embedding-3-large
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
- lollms-lightrag-server --help
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
- "--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
- "--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.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.model,
233
  llm_model_max_async=args.max_async,
234
  llm_model_max_token_size=args.max_tokens,
235
  llm_model_kwargs={
236
- "host": args.lollms_host,
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.lollms_host
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
- "model": args.model,
 
 
 
 
 
 
 
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
- "lollms-lightrag-server=lightrag.api.lollms_lightrag_server:main [api]",
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
  )