ParisNeo commited on
Commit
8525af3
·
1 Parent(s): 51da218

Fixed linting

Browse files
.gitignore CHANGED
@@ -14,4 +14,4 @@ ignore_this.txt
14
  .ruff_cache/
15
  gui/
16
  *.log
17
- .vscode
 
14
  .ruff_cache/
15
  gui/
16
  *.log
17
+ .vscode
api/.gitignore CHANGED
@@ -1,2 +1,2 @@
1
  inputs
2
- rag_storage
 
1
  inputs
2
+ rag_storage
api/README_OPENAI.md CHANGED
@@ -169,4 +169,3 @@ This project is licensed under the MIT License - see the LICENSE file for detail
169
  - Built with [FastAPI](https://fastapi.tiangolo.com/)
170
  - Uses [LightRAG](https://github.com/HKUDS/LightRAG) for document processing
171
  - Powered by [OpenAI](https://openai.com/) for language model inference
172
-
 
169
  - Built with [FastAPI](https://fastapi.tiangolo.com/)
170
  - Uses [LightRAG](https://github.com/HKUDS/LightRAG) for document processing
171
  - Powered by [OpenAI](https://openai.com/) for language model inference
 
api/ollama_lightrag_server.py CHANGED
@@ -1,8 +1,5 @@
1
  from fastapi import FastAPI, HTTPException, File, UploadFile, Form
2
- from fastapi.responses import JSONResponse
3
  from pydantic import BaseModel
4
- import asyncio
5
- import os
6
  import logging
7
  import argparse
8
  from lightrag import LightRAG, QueryParam
@@ -13,7 +10,8 @@ from enum import Enum
13
  from pathlib import Path
14
  import shutil
15
  import aiofiles
16
- from ascii_colors import ASCIIColors, trace_exception
 
17
 
18
  def parse_args():
19
  parser = argparse.ArgumentParser(
@@ -21,45 +19,84 @@ def parse_args():
21
  )
22
 
23
  # Server configuration
24
- parser.add_argument('--host', default='0.0.0.0', help='Server host (default: 0.0.0.0)')
25
- parser.add_argument('--port', type=int, default=9621, help='Server port (default: 9621)')
26
-
 
 
 
 
27
  # Directory configuration
28
- parser.add_argument('--working-dir', default='./rag_storage',
29
- help='Working directory for RAG storage (default: ./rag_storage)')
30
- parser.add_argument('--input-dir', default='./inputs',
31
- help='Directory containing input documents (default: ./inputs)')
32
-
 
 
 
 
 
 
33
  # Model configuration
34
- parser.add_argument('--model', default='mistral-nemo:latest', help='LLM model name (default: mistral-nemo:latest)')
35
- parser.add_argument('--embedding-model', default='bge-m3:latest',
36
- help='Embedding model name (default: bge-m3:latest)')
37
- parser.add_argument('--ollama-host', default='http://localhost:11434',
38
- help='Ollama host URL (default: http://localhost:11434)')
39
-
 
 
 
 
 
 
 
 
 
 
40
  # RAG configuration
41
- parser.add_argument('--max-async', type=int, default=4, help='Maximum async operations (default: 4)')
42
- parser.add_argument('--max-tokens', type=int, default=32768, help='Maximum token size (default: 32768)')
43
- parser.add_argument('--embedding-dim', type=int, default=1024,
44
- help='Embedding dimensions (default: 1024)')
45
- parser.add_argument('--max-embed-tokens', type=int, default=8192,
46
- help='Maximum embedding token size (default: 8192)')
47
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  # Logging configuration
49
- parser.add_argument('--log-level', default='INFO',
50
- choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
51
- help='Logging level (default: INFO)')
52
-
 
 
 
53
  return parser.parse_args()
54
 
 
55
  class DocumentManager:
56
  """Handles document operations and tracking"""
57
-
58
- def __init__(self, input_dir: str, supported_extensions: tuple = ('.txt', '.md')):
59
  self.input_dir = Path(input_dir)
60
  self.supported_extensions = supported_extensions
61
  self.indexed_files = set()
62
-
63
  # Create input directory if it doesn't exist
64
  self.input_dir.mkdir(parents=True, exist_ok=True)
65
 
@@ -67,7 +104,7 @@ class DocumentManager:
67
  """Scan input directory for new files"""
68
  new_files = []
69
  for ext in self.supported_extensions:
70
- for file_path in self.input_dir.rglob(f'*{ext}'):
71
  if file_path not in self.indexed_files:
72
  new_files.append(file_path)
73
  return new_files
@@ -80,6 +117,7 @@ class DocumentManager:
80
  """Check if file type is supported"""
81
  return any(filename.lower().endswith(ext) for ext in self.supported_extensions)
82
 
 
83
  # Pydantic models
84
  class SearchMode(str, Enum):
85
  naive = "naive"
@@ -87,31 +125,38 @@ class SearchMode(str, Enum):
87
  global_ = "global"
88
  hybrid = "hybrid"
89
 
 
90
  class QueryRequest(BaseModel):
91
  query: str
92
  mode: SearchMode = SearchMode.hybrid
93
  stream: bool = False
94
 
 
95
  class QueryResponse(BaseModel):
96
  response: str
97
 
 
98
  class InsertTextRequest(BaseModel):
99
  text: str
100
  description: Optional[str] = None
101
 
 
102
  class InsertResponse(BaseModel):
103
  status: str
104
  message: str
105
  document_count: int
106
 
 
107
  def create_app(args):
108
  # Setup logging
109
- logging.basicConfig(format="%(levelname)s:%(message)s", level=getattr(logging, args.log_level))
 
 
110
 
111
  # Initialize FastAPI app
112
  app = FastAPI(
113
  title="LightRAG API",
114
- description="API for querying text using LightRAG with separate storage and input directories"
115
  )
116
 
117
  # Create working directory if it doesn't exist
@@ -127,7 +172,10 @@ def create_app(args):
127
  llm_model_name=args.model,
128
  llm_model_max_async=args.max_async,
129
  llm_model_max_token_size=args.max_tokens,
130
- llm_model_kwargs={"host": args.ollama_host, "options": {"num_ctx": args.max_tokens}},
 
 
 
131
  embedding_func=EmbeddingFunc(
132
  embedding_dim=args.embedding_dim,
133
  max_token_size=args.max_embed_tokens,
@@ -136,6 +184,7 @@ def create_app(args):
136
  ),
137
  ),
138
  )
 
139
  @app.on_event("startup")
140
  async def startup_event():
141
  """Index all files in input directory during startup"""
@@ -144,7 +193,7 @@ def create_app(args):
144
  for file_path in new_files:
145
  try:
146
  # Use async file reading
147
- async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
148
  content = await f.read()
149
  # Use the async version of insert directly
150
  await rag.ainsert(content)
@@ -153,9 +202,9 @@ def create_app(args):
153
  except Exception as e:
154
  trace_exception(e)
155
  logging.error(f"Error indexing file {file_path}: {str(e)}")
156
-
157
  logging.info(f"Indexed {len(new_files)} documents from {args.input_dir}")
158
-
159
  except Exception as e:
160
  logging.error(f"Error during startup indexing: {str(e)}")
161
 
@@ -165,21 +214,21 @@ def create_app(args):
165
  try:
166
  new_files = doc_manager.scan_directory()
167
  indexed_count = 0
168
-
169
  for file_path in new_files:
170
  try:
171
- with open(file_path, 'r', encoding='utf-8') as f:
172
  content = f.read()
173
  rag.insert(content)
174
  doc_manager.mark_as_indexed(file_path)
175
  indexed_count += 1
176
  except Exception as e:
177
  logging.error(f"Error indexing file {file_path}: {str(e)}")
178
-
179
  return {
180
  "status": "success",
181
  "indexed_count": indexed_count,
182
- "total_documents": len(doc_manager.indexed_files)
183
  }
184
  except Exception as e:
185
  raise HTTPException(status_code=500, detail=str(e))
@@ -191,23 +240,23 @@ def create_app(args):
191
  if not doc_manager.is_supported_file(file.filename):
192
  raise HTTPException(
193
  status_code=400,
194
- detail=f"Unsupported file type. Supported types: {doc_manager.supported_extensions}"
195
  )
196
-
197
  file_path = doc_manager.input_dir / file.filename
198
  with open(file_path, "wb") as buffer:
199
  shutil.copyfileobj(file.file, buffer)
200
-
201
  # Immediately index the uploaded file
202
  with open(file_path, "r", encoding="utf-8") as f:
203
  content = f.read()
204
  rag.insert(content)
205
  doc_manager.mark_as_indexed(file_path)
206
-
207
  return {
208
  "status": "success",
209
  "message": f"File uploaded and indexed: {file.filename}",
210
- "total_documents": len(doc_manager.indexed_files)
211
  }
212
  except Exception as e:
213
  raise HTTPException(status_code=500, detail=str(e))
@@ -217,9 +266,9 @@ def create_app(args):
217
  try:
218
  response = await rag.aquery(
219
  request.query,
220
- param=QueryParam(mode=request.mode, stream=request.stream)
221
  )
222
-
223
  if request.stream:
224
  result = ""
225
  async for chunk in response:
@@ -234,14 +283,13 @@ def create_app(args):
234
  async def query_text_stream(request: QueryRequest):
235
  try:
236
  response = rag.query(
237
- request.query,
238
- param=QueryParam(mode=request.mode, stream=True)
239
  )
240
-
241
  async def stream_generator():
242
  async for chunk in response:
243
  yield chunk
244
-
245
  return stream_generator()
246
  except Exception as e:
247
  raise HTTPException(status_code=500, detail=str(e))
@@ -253,32 +301,29 @@ def create_app(args):
253
  return InsertResponse(
254
  status="success",
255
  message="Text successfully inserted",
256
- document_count=len(rag)
257
  )
258
  except Exception as e:
259
  raise HTTPException(status_code=500, detail=str(e))
260
 
261
  @app.post("/documents/file", response_model=InsertResponse)
262
- async def insert_file(
263
- file: UploadFile = File(...),
264
- description: str = Form(None)
265
- ):
266
  try:
267
  content = await file.read()
268
-
269
- if file.filename.endswith(('.txt', '.md')):
270
- text = content.decode('utf-8')
271
  rag.insert(text)
272
  else:
273
  raise HTTPException(
274
  status_code=400,
275
- detail="Unsupported file type. Only .txt and .md files are supported"
276
  )
277
-
278
  return InsertResponse(
279
  status="success",
280
  message=f"File '{file.filename}' successfully inserted",
281
- document_count=len(rag)
282
  )
283
  except UnicodeDecodeError:
284
  raise HTTPException(status_code=400, detail="File encoding not supported")
@@ -290,27 +335,27 @@ def create_app(args):
290
  try:
291
  inserted_count = 0
292
  failed_files = []
293
-
294
  for file in files:
295
  try:
296
  content = await file.read()
297
- if file.filename.endswith(('.txt', '.md')):
298
- text = content.decode('utf-8')
299
  rag.insert(text)
300
  inserted_count += 1
301
  else:
302
  failed_files.append(f"{file.filename} (unsupported type)")
303
  except Exception as e:
304
  failed_files.append(f"{file.filename} ({str(e)})")
305
-
306
  status_message = f"Successfully inserted {inserted_count} documents"
307
  if failed_files:
308
  status_message += f". Failed files: {', '.join(failed_files)}"
309
-
310
  return InsertResponse(
311
  status="success" if inserted_count > 0 else "partial_success",
312
  message=status_message,
313
- document_count=len(rag)
314
  )
315
  except Exception as e:
316
  raise HTTPException(status_code=500, detail=str(e))
@@ -324,12 +369,11 @@ def create_app(args):
324
  return InsertResponse(
325
  status="success",
326
  message="All documents cleared successfully",
327
- document_count=0
328
  )
329
  except Exception as e:
330
  raise HTTPException(status_code=500, detail=str(e))
331
 
332
-
333
  @app.get("/health")
334
  async def get_status():
335
  """Get current system status"""
@@ -342,14 +386,16 @@ def create_app(args):
342
  "model": args.model,
343
  "embedding_model": args.embedding_model,
344
  "max_tokens": args.max_tokens,
345
- "ollama_host": args.ollama_host
346
- }
347
  }
348
 
349
  return app
350
 
 
351
  if __name__ == "__main__":
352
  args = parse_args()
353
  import uvicorn
 
354
  app = create_app(args)
355
  uvicorn.run(app, host=args.host, port=args.port)
 
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
 
10
  from pathlib import Path
11
  import shutil
12
  import aiofiles
13
+ from ascii_colors import trace_exception
14
+
15
 
16
  def parse_args():
17
  parser = argparse.ArgumentParser(
 
19
  )
20
 
21
  # Server configuration
22
+ parser.add_argument(
23
+ "--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)"
24
+ )
25
+ parser.add_argument(
26
+ "--port", type=int, default=9621, help="Server port (default: 9621)"
27
+ )
28
+
29
  # Directory configuration
30
+ parser.add_argument(
31
+ "--working-dir",
32
+ default="./rag_storage",
33
+ help="Working directory for RAG storage (default: ./rag_storage)",
34
+ )
35
+ parser.add_argument(
36
+ "--input-dir",
37
+ default="./inputs",
38
+ help="Directory containing input documents (default: ./inputs)",
39
+ )
40
+
41
  # Model configuration
42
+ parser.add_argument(
43
+ "--model",
44
+ default="mistral-nemo:latest",
45
+ help="LLM model name (default: mistral-nemo:latest)",
46
+ )
47
+ parser.add_argument(
48
+ "--embedding-model",
49
+ default="bge-m3:latest",
50
+ help="Embedding model name (default: bge-m3:latest)",
51
+ )
52
+ parser.add_argument(
53
+ "--ollama-host",
54
+ default="http://localhost:11434",
55
+ help="Ollama host URL (default: http://localhost:11434)",
56
+ )
57
+
58
  # RAG configuration
59
+ parser.add_argument(
60
+ "--max-async", type=int, default=4, help="Maximum async operations (default: 4)"
61
+ )
62
+ parser.add_argument(
63
+ "--max-tokens",
64
+ type=int,
65
+ default=32768,
66
+ help="Maximum token size (default: 32768)",
67
+ )
68
+ parser.add_argument(
69
+ "--embedding-dim",
70
+ type=int,
71
+ default=1024,
72
+ help="Embedding dimensions (default: 1024)",
73
+ )
74
+ parser.add_argument(
75
+ "--max-embed-tokens",
76
+ type=int,
77
+ default=8192,
78
+ help="Maximum embedding token size (default: 8192)",
79
+ )
80
+
81
  # Logging configuration
82
+ parser.add_argument(
83
+ "--log-level",
84
+ default="INFO",
85
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
86
+ help="Logging level (default: INFO)",
87
+ )
88
+
89
  return parser.parse_args()
90
 
91
+
92
  class DocumentManager:
93
  """Handles document operations and tracking"""
94
+
95
+ def __init__(self, input_dir: str, supported_extensions: tuple = (".txt", ".md")):
96
  self.input_dir = Path(input_dir)
97
  self.supported_extensions = supported_extensions
98
  self.indexed_files = set()
99
+
100
  # Create input directory if it doesn't exist
101
  self.input_dir.mkdir(parents=True, exist_ok=True)
102
 
 
104
  """Scan input directory for new files"""
105
  new_files = []
106
  for ext in self.supported_extensions:
107
+ for file_path in self.input_dir.rglob(f"*{ext}"):
108
  if file_path not in self.indexed_files:
109
  new_files.append(file_path)
110
  return new_files
 
117
  """Check if file type is supported"""
118
  return any(filename.lower().endswith(ext) for ext in self.supported_extensions)
119
 
120
+
121
  # Pydantic models
122
  class SearchMode(str, Enum):
123
  naive = "naive"
 
125
  global_ = "global"
126
  hybrid = "hybrid"
127
 
128
+
129
  class QueryRequest(BaseModel):
130
  query: str
131
  mode: SearchMode = SearchMode.hybrid
132
  stream: bool = False
133
 
134
+
135
  class QueryResponse(BaseModel):
136
  response: str
137
 
138
+
139
  class InsertTextRequest(BaseModel):
140
  text: str
141
  description: Optional[str] = None
142
 
143
+
144
  class InsertResponse(BaseModel):
145
  status: str
146
  message: str
147
  document_count: int
148
 
149
+
150
  def create_app(args):
151
  # Setup logging
152
+ logging.basicConfig(
153
+ format="%(levelname)s:%(message)s", level=getattr(logging, args.log_level)
154
+ )
155
 
156
  # Initialize FastAPI app
157
  app = FastAPI(
158
  title="LightRAG API",
159
+ description="API for querying text using LightRAG with separate storage and input directories",
160
  )
161
 
162
  # Create working directory if it doesn't exist
 
172
  llm_model_name=args.model,
173
  llm_model_max_async=args.max_async,
174
  llm_model_max_token_size=args.max_tokens,
175
+ llm_model_kwargs={
176
+ "host": args.ollama_host,
177
+ "options": {"num_ctx": args.max_tokens},
178
+ },
179
  embedding_func=EmbeddingFunc(
180
  embedding_dim=args.embedding_dim,
181
  max_token_size=args.max_embed_tokens,
 
184
  ),
185
  ),
186
  )
187
+
188
  @app.on_event("startup")
189
  async def startup_event():
190
  """Index all files in input directory during startup"""
 
193
  for file_path in new_files:
194
  try:
195
  # Use async file reading
196
+ async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
197
  content = await f.read()
198
  # Use the async version of insert directly
199
  await rag.ainsert(content)
 
202
  except Exception as e:
203
  trace_exception(e)
204
  logging.error(f"Error indexing file {file_path}: {str(e)}")
205
+
206
  logging.info(f"Indexed {len(new_files)} documents from {args.input_dir}")
207
+
208
  except Exception as e:
209
  logging.error(f"Error during startup indexing: {str(e)}")
210
 
 
214
  try:
215
  new_files = doc_manager.scan_directory()
216
  indexed_count = 0
217
+
218
  for file_path in new_files:
219
  try:
220
+ with open(file_path, "r", encoding="utf-8") as f:
221
  content = f.read()
222
  rag.insert(content)
223
  doc_manager.mark_as_indexed(file_path)
224
  indexed_count += 1
225
  except Exception as e:
226
  logging.error(f"Error indexing file {file_path}: {str(e)}")
227
+
228
  return {
229
  "status": "success",
230
  "indexed_count": indexed_count,
231
+ "total_documents": len(doc_manager.indexed_files),
232
  }
233
  except Exception as e:
234
  raise HTTPException(status_code=500, detail=str(e))
 
240
  if not doc_manager.is_supported_file(file.filename):
241
  raise HTTPException(
242
  status_code=400,
243
+ detail=f"Unsupported file type. Supported types: {doc_manager.supported_extensions}",
244
  )
245
+
246
  file_path = doc_manager.input_dir / file.filename
247
  with open(file_path, "wb") as buffer:
248
  shutil.copyfileobj(file.file, buffer)
249
+
250
  # Immediately index the uploaded file
251
  with open(file_path, "r", encoding="utf-8") as f:
252
  content = f.read()
253
  rag.insert(content)
254
  doc_manager.mark_as_indexed(file_path)
255
+
256
  return {
257
  "status": "success",
258
  "message": f"File uploaded and indexed: {file.filename}",
259
+ "total_documents": len(doc_manager.indexed_files),
260
  }
261
  except Exception as e:
262
  raise HTTPException(status_code=500, detail=str(e))
 
266
  try:
267
  response = await rag.aquery(
268
  request.query,
269
+ param=QueryParam(mode=request.mode, stream=request.stream),
270
  )
271
+
272
  if request.stream:
273
  result = ""
274
  async for chunk in response:
 
283
  async def query_text_stream(request: QueryRequest):
284
  try:
285
  response = rag.query(
286
+ request.query, param=QueryParam(mode=request.mode, stream=True)
 
287
  )
288
+
289
  async def stream_generator():
290
  async for chunk in response:
291
  yield chunk
292
+
293
  return stream_generator()
294
  except Exception as e:
295
  raise HTTPException(status_code=500, detail=str(e))
 
301
  return InsertResponse(
302
  status="success",
303
  message="Text successfully inserted",
304
+ document_count=len(rag),
305
  )
306
  except Exception as e:
307
  raise HTTPException(status_code=500, detail=str(e))
308
 
309
  @app.post("/documents/file", response_model=InsertResponse)
310
+ async def insert_file(file: UploadFile = File(...), description: str = Form(None)):
 
 
 
311
  try:
312
  content = await file.read()
313
+
314
+ if file.filename.endswith((".txt", ".md")):
315
+ text = content.decode("utf-8")
316
  rag.insert(text)
317
  else:
318
  raise HTTPException(
319
  status_code=400,
320
+ detail="Unsupported file type. Only .txt and .md files are supported",
321
  )
322
+
323
  return InsertResponse(
324
  status="success",
325
  message=f"File '{file.filename}' successfully inserted",
326
+ document_count=len(rag),
327
  )
328
  except UnicodeDecodeError:
329
  raise HTTPException(status_code=400, detail="File encoding not supported")
 
335
  try:
336
  inserted_count = 0
337
  failed_files = []
338
+
339
  for file in files:
340
  try:
341
  content = await file.read()
342
+ if file.filename.endswith((".txt", ".md")):
343
+ text = content.decode("utf-8")
344
  rag.insert(text)
345
  inserted_count += 1
346
  else:
347
  failed_files.append(f"{file.filename} (unsupported type)")
348
  except Exception as e:
349
  failed_files.append(f"{file.filename} ({str(e)})")
350
+
351
  status_message = f"Successfully inserted {inserted_count} documents"
352
  if failed_files:
353
  status_message += f". Failed files: {', '.join(failed_files)}"
354
+
355
  return InsertResponse(
356
  status="success" if inserted_count > 0 else "partial_success",
357
  message=status_message,
358
+ document_count=len(rag),
359
  )
360
  except Exception as e:
361
  raise HTTPException(status_code=500, detail=str(e))
 
369
  return InsertResponse(
370
  status="success",
371
  message="All documents cleared successfully",
372
+ document_count=0,
373
  )
374
  except Exception as e:
375
  raise HTTPException(status_code=500, detail=str(e))
376
 
 
377
  @app.get("/health")
378
  async def get_status():
379
  """Get current system status"""
 
386
  "model": args.model,
387
  "embedding_model": args.embedding_model,
388
  "max_tokens": args.max_tokens,
389
+ "ollama_host": args.ollama_host,
390
+ },
391
  }
392
 
393
  return app
394
 
395
+
396
  if __name__ == "__main__":
397
  args = parse_args()
398
  import uvicorn
399
+
400
  app = create_app(args)
401
  uvicorn.run(app, host=args.host, port=args.port)
api/openai_lightrag_server.py CHANGED
@@ -1,8 +1,6 @@
1
  from fastapi import FastAPI, HTTPException, File, UploadFile, Form
2
- from fastapi.responses import JSONResponse
3
  from pydantic import BaseModel
4
  import asyncio
5
- import os
6
  import logging
7
  import argparse
8
  from lightrag import LightRAG, QueryParam
@@ -13,53 +11,81 @@ from enum import Enum
13
  from pathlib import Path
14
  import shutil
15
  import aiofiles
16
- from ascii_colors import ASCIIColors, trace_exception
17
- import numpy as np
18
  import nest_asyncio
19
 
20
  # Apply nest_asyncio to solve event loop issues
21
  nest_asyncio.apply()
22
 
 
23
  def parse_args():
24
  parser = argparse.ArgumentParser(
25
  description="LightRAG FastAPI Server with OpenAI integration"
26
  )
27
 
28
  # Server configuration
29
- parser.add_argument('--host', default='0.0.0.0', help='Server host (default: 0.0.0.0)')
30
- parser.add_argument('--port', type=int, default=9621, help='Server port (default: 9621)')
31
-
 
 
 
 
32
  # Directory configuration
33
- parser.add_argument('--working-dir', default='./rag_storage',
34
- help='Working directory for RAG storage (default: ./rag_storage)')
35
- parser.add_argument('--input-dir', default='./inputs',
36
- help='Directory containing input documents (default: ./inputs)')
37
-
 
 
 
 
 
 
38
  # Model configuration
39
- parser.add_argument('--model', default='gpt-4', help='OpenAI model name (default: gpt-4)')
40
- parser.add_argument('--embedding-model', default='text-embedding-3-large',
41
- help='OpenAI embedding model (default: text-embedding-3-large)')
42
-
 
 
 
 
 
43
  # RAG configuration
44
- parser.add_argument('--max-tokens', type=int, default=32768, help='Maximum token size (default: 32768)')
45
- parser.add_argument('--max-embed-tokens', type=int, default=8192,
46
- help='Maximum embedding token size (default: 8192)')
47
-
 
 
 
 
 
 
 
 
 
48
  # Logging configuration
49
- parser.add_argument('--log-level', default='INFO',
50
- choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
51
- help='Logging level (default: INFO)')
52
-
 
 
 
53
  return parser.parse_args()
54
 
 
55
  class DocumentManager:
56
  """Handles document operations and tracking"""
57
-
58
- def __init__(self, input_dir: str, supported_extensions: tuple = ('.txt', '.md')):
59
  self.input_dir = Path(input_dir)
60
  self.supported_extensions = supported_extensions
61
  self.indexed_files = set()
62
-
63
  # Create input directory if it doesn't exist
64
  self.input_dir.mkdir(parents=True, exist_ok=True)
65
 
@@ -67,7 +93,7 @@ class DocumentManager:
67
  """Scan input directory for new files"""
68
  new_files = []
69
  for ext in self.supported_extensions:
70
- for file_path in self.input_dir.rglob(f'*{ext}'):
71
  if file_path not in self.indexed_files:
72
  new_files.append(file_path)
73
  return new_files
@@ -80,6 +106,7 @@ class DocumentManager:
80
  """Check if file type is supported"""
81
  return any(filename.lower().endswith(ext) for ext in self.supported_extensions)
82
 
 
83
  # Pydantic models
84
  class SearchMode(str, Enum):
85
  naive = "naive"
@@ -87,37 +114,45 @@ class SearchMode(str, Enum):
87
  global_ = "global"
88
  hybrid = "hybrid"
89
 
 
90
  class QueryRequest(BaseModel):
91
  query: str
92
  mode: SearchMode = SearchMode.hybrid
93
  stream: bool = False
94
 
 
95
  class QueryResponse(BaseModel):
96
  response: str
97
 
 
98
  class InsertTextRequest(BaseModel):
99
  text: str
100
  description: Optional[str] = None
101
 
 
102
  class InsertResponse(BaseModel):
103
  status: str
104
  message: str
105
  document_count: int
106
 
 
107
  async def get_embedding_dim(embedding_model: str) -> int:
108
  """Get embedding dimensions for the specified model"""
109
  test_text = ["This is a test sentence."]
110
  embedding = await openai_embedding(test_text, model=embedding_model)
111
  return embedding.shape[1]
112
 
 
113
  def create_app(args):
114
  # Setup logging
115
- logging.basicConfig(format="%(levelname)s:%(message)s", level=getattr(logging, args.log_level))
 
 
116
 
117
  # Initialize FastAPI app
118
  app = FastAPI(
119
  title="LightRAG API",
120
- description="API for querying text using LightRAG with OpenAI integration"
121
  )
122
 
123
  # Create working directory if it doesn't exist
@@ -129,6 +164,18 @@ def create_app(args):
129
  # Get embedding dimensions
130
  embedding_dim = asyncio.run(get_embedding_dim(args.embedding_model))
131
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  # Initialize RAG with OpenAI configuration
133
  rag = LightRAG(
134
  working_dir=args.working_dir,
@@ -142,15 +189,6 @@ def create_app(args):
142
  ),
143
  )
144
 
145
- async def async_openai_complete(prompt, system_prompt=None, history_messages=[], **kwargs):
146
- """Async wrapper for OpenAI completion"""
147
- return await openai_complete_if_cache(
148
- args.model,
149
- prompt,
150
- system_prompt=system_prompt,
151
- history_messages=history_messages,
152
- **kwargs
153
- )
154
  @app.on_event("startup")
155
  async def startup_event():
156
  """Index all files in input directory during startup"""
@@ -159,7 +197,7 @@ def create_app(args):
159
  for file_path in new_files:
160
  try:
161
  # Use async file reading
162
- async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
163
  content = await f.read()
164
  # Use the async version of insert directly
165
  await rag.ainsert(content)
@@ -168,9 +206,9 @@ def create_app(args):
168
  except Exception as e:
169
  trace_exception(e)
170
  logging.error(f"Error indexing file {file_path}: {str(e)}")
171
-
172
  logging.info(f"Indexed {len(new_files)} documents from {args.input_dir}")
173
-
174
  except Exception as e:
175
  logging.error(f"Error during startup indexing: {str(e)}")
176
 
@@ -180,21 +218,21 @@ def create_app(args):
180
  try:
181
  new_files = doc_manager.scan_directory()
182
  indexed_count = 0
183
-
184
  for file_path in new_files:
185
  try:
186
- with open(file_path, 'r', encoding='utf-8') as f:
187
  content = f.read()
188
  rag.insert(content)
189
  doc_manager.mark_as_indexed(file_path)
190
  indexed_count += 1
191
  except Exception as e:
192
  logging.error(f"Error indexing file {file_path}: {str(e)}")
193
-
194
  return {
195
  "status": "success",
196
  "indexed_count": indexed_count,
197
- "total_documents": len(doc_manager.indexed_files)
198
  }
199
  except Exception as e:
200
  raise HTTPException(status_code=500, detail=str(e))
@@ -206,23 +244,23 @@ def create_app(args):
206
  if not doc_manager.is_supported_file(file.filename):
207
  raise HTTPException(
208
  status_code=400,
209
- detail=f"Unsupported file type. Supported types: {doc_manager.supported_extensions}"
210
  )
211
-
212
  file_path = doc_manager.input_dir / file.filename
213
  with open(file_path, "wb") as buffer:
214
  shutil.copyfileobj(file.file, buffer)
215
-
216
  # Immediately index the uploaded file
217
  with open(file_path, "r", encoding="utf-8") as f:
218
  content = f.read()
219
  rag.insert(content)
220
  doc_manager.mark_as_indexed(file_path)
221
-
222
  return {
223
  "status": "success",
224
  "message": f"File uploaded and indexed: {file.filename}",
225
- "total_documents": len(doc_manager.indexed_files)
226
  }
227
  except Exception as e:
228
  raise HTTPException(status_code=500, detail=str(e))
@@ -232,9 +270,9 @@ def create_app(args):
232
  try:
233
  response = await rag.aquery(
234
  request.query,
235
- param=QueryParam(mode=request.mode, stream=request.stream)
236
  )
237
-
238
  if request.stream:
239
  result = ""
240
  async for chunk in response:
@@ -249,14 +287,13 @@ def create_app(args):
249
  async def query_text_stream(request: QueryRequest):
250
  try:
251
  response = rag.query(
252
- request.query,
253
- param=QueryParam(mode=request.mode, stream=True)
254
  )
255
-
256
  async def stream_generator():
257
  async for chunk in response:
258
  yield chunk
259
-
260
  return stream_generator()
261
  except Exception as e:
262
  raise HTTPException(status_code=500, detail=str(e))
@@ -268,32 +305,29 @@ def create_app(args):
268
  return InsertResponse(
269
  status="success",
270
  message="Text successfully inserted",
271
- document_count=len(rag)
272
  )
273
  except Exception as e:
274
  raise HTTPException(status_code=500, detail=str(e))
275
 
276
  @app.post("/documents/file", response_model=InsertResponse)
277
- async def insert_file(
278
- file: UploadFile = File(...),
279
- description: str = Form(None)
280
- ):
281
  try:
282
  content = await file.read()
283
-
284
- if file.filename.endswith(('.txt', '.md')):
285
- text = content.decode('utf-8')
286
  rag.insert(text)
287
  else:
288
  raise HTTPException(
289
  status_code=400,
290
- detail="Unsupported file type. Only .txt and .md files are supported"
291
  )
292
-
293
  return InsertResponse(
294
  status="success",
295
  message=f"File '{file.filename}' successfully inserted",
296
- document_count=len(rag)
297
  )
298
  except UnicodeDecodeError:
299
  raise HTTPException(status_code=400, detail="File encoding not supported")
@@ -305,27 +339,27 @@ def create_app(args):
305
  try:
306
  inserted_count = 0
307
  failed_files = []
308
-
309
  for file in files:
310
  try:
311
  content = await file.read()
312
- if file.filename.endswith(('.txt', '.md')):
313
- text = content.decode('utf-8')
314
  rag.insert(text)
315
  inserted_count += 1
316
  else:
317
  failed_files.append(f"{file.filename} (unsupported type)")
318
  except Exception as e:
319
  failed_files.append(f"{file.filename} ({str(e)})")
320
-
321
  status_message = f"Successfully inserted {inserted_count} documents"
322
  if failed_files:
323
  status_message += f". Failed files: {', '.join(failed_files)}"
324
-
325
  return InsertResponse(
326
  status="success" if inserted_count > 0 else "partial_success",
327
  message=status_message,
328
- document_count=len(rag)
329
  )
330
  except Exception as e:
331
  raise HTTPException(status_code=500, detail=str(e))
@@ -339,7 +373,7 @@ def create_app(args):
339
  return InsertResponse(
340
  status="success",
341
  message="All documents cleared successfully",
342
- document_count=0
343
  )
344
  except Exception as e:
345
  raise HTTPException(status_code=500, detail=str(e))
@@ -356,14 +390,16 @@ def create_app(args):
356
  "model": args.model,
357
  "embedding_model": args.embedding_model,
358
  "max_tokens": args.max_tokens,
359
- "embedding_dim": embedding_dim
360
- }
361
  }
362
 
363
  return app
364
 
 
365
  if __name__ == "__main__":
366
  args = parse_args()
367
  import uvicorn
 
368
  app = create_app(args)
369
  uvicorn.run(app, host=args.host, port=args.port)
 
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
 
11
  from pathlib import Path
12
  import shutil
13
  import aiofiles
14
+ from ascii_colors import trace_exception
 
15
  import nest_asyncio
16
 
17
  # Apply nest_asyncio to solve event loop issues
18
  nest_asyncio.apply()
19
 
20
+
21
  def parse_args():
22
  parser = argparse.ArgumentParser(
23
  description="LightRAG FastAPI Server with OpenAI integration"
24
  )
25
 
26
  # Server configuration
27
+ parser.add_argument(
28
+ "--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)"
29
+ )
30
+ parser.add_argument(
31
+ "--port", type=int, default=9621, help="Server port (default: 9621)"
32
+ )
33
+
34
  # Directory configuration
35
+ parser.add_argument(
36
+ "--working-dir",
37
+ default="./rag_storage",
38
+ help="Working directory for RAG storage (default: ./rag_storage)",
39
+ )
40
+ parser.add_argument(
41
+ "--input-dir",
42
+ default="./inputs",
43
+ help="Directory containing input documents (default: ./inputs)",
44
+ )
45
+
46
  # Model configuration
47
+ parser.add_argument(
48
+ "--model", default="gpt-4", help="OpenAI model name (default: gpt-4)"
49
+ )
50
+ parser.add_argument(
51
+ "--embedding-model",
52
+ default="text-embedding-3-large",
53
+ help="OpenAI embedding model (default: text-embedding-3-large)",
54
+ )
55
+
56
  # RAG configuration
57
+ parser.add_argument(
58
+ "--max-tokens",
59
+ type=int,
60
+ default=32768,
61
+ help="Maximum token size (default: 32768)",
62
+ )
63
+ parser.add_argument(
64
+ "--max-embed-tokens",
65
+ type=int,
66
+ default=8192,
67
+ help="Maximum embedding token size (default: 8192)",
68
+ )
69
+
70
  # Logging configuration
71
+ parser.add_argument(
72
+ "--log-level",
73
+ default="INFO",
74
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
75
+ help="Logging level (default: INFO)",
76
+ )
77
+
78
  return parser.parse_args()
79
 
80
+
81
  class DocumentManager:
82
  """Handles document operations and tracking"""
83
+
84
+ def __init__(self, input_dir: str, supported_extensions: tuple = (".txt", ".md")):
85
  self.input_dir = Path(input_dir)
86
  self.supported_extensions = supported_extensions
87
  self.indexed_files = set()
88
+
89
  # Create input directory if it doesn't exist
90
  self.input_dir.mkdir(parents=True, exist_ok=True)
91
 
 
93
  """Scan input directory for new files"""
94
  new_files = []
95
  for ext in self.supported_extensions:
96
+ for file_path in self.input_dir.rglob(f"*{ext}"):
97
  if file_path not in self.indexed_files:
98
  new_files.append(file_path)
99
  return new_files
 
106
  """Check if file type is supported"""
107
  return any(filename.lower().endswith(ext) for ext in self.supported_extensions)
108
 
109
+
110
  # Pydantic models
111
  class SearchMode(str, Enum):
112
  naive = "naive"
 
114
  global_ = "global"
115
  hybrid = "hybrid"
116
 
117
+
118
  class QueryRequest(BaseModel):
119
  query: str
120
  mode: SearchMode = SearchMode.hybrid
121
  stream: bool = False
122
 
123
+
124
  class QueryResponse(BaseModel):
125
  response: str
126
 
127
+
128
  class InsertTextRequest(BaseModel):
129
  text: str
130
  description: Optional[str] = None
131
 
132
+
133
  class InsertResponse(BaseModel):
134
  status: str
135
  message: str
136
  document_count: int
137
 
138
+
139
  async def get_embedding_dim(embedding_model: str) -> int:
140
  """Get embedding dimensions for the specified model"""
141
  test_text = ["This is a test sentence."]
142
  embedding = await openai_embedding(test_text, model=embedding_model)
143
  return embedding.shape[1]
144
 
145
+
146
  def create_app(args):
147
  # Setup logging
148
+ logging.basicConfig(
149
+ format="%(levelname)s:%(message)s", level=getattr(logging, args.log_level)
150
+ )
151
 
152
  # Initialize FastAPI app
153
  app = FastAPI(
154
  title="LightRAG API",
155
+ description="API for querying text using LightRAG with OpenAI integration",
156
  )
157
 
158
  # Create working directory if it doesn't exist
 
164
  # Get embedding dimensions
165
  embedding_dim = asyncio.run(get_embedding_dim(args.embedding_model))
166
 
167
+ async def async_openai_complete(
168
+ prompt, system_prompt=None, history_messages=[], **kwargs
169
+ ):
170
+ """Async wrapper for OpenAI completion"""
171
+ return await openai_complete_if_cache(
172
+ args.model,
173
+ prompt,
174
+ system_prompt=system_prompt,
175
+ history_messages=history_messages,
176
+ **kwargs,
177
+ )
178
+
179
  # Initialize RAG with OpenAI configuration
180
  rag = LightRAG(
181
  working_dir=args.working_dir,
 
189
  ),
190
  )
191
 
 
 
 
 
 
 
 
 
 
192
  @app.on_event("startup")
193
  async def startup_event():
194
  """Index all files in input directory during startup"""
 
197
  for file_path in new_files:
198
  try:
199
  # Use async file reading
200
+ async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
201
  content = await f.read()
202
  # Use the async version of insert directly
203
  await rag.ainsert(content)
 
206
  except Exception as e:
207
  trace_exception(e)
208
  logging.error(f"Error indexing file {file_path}: {str(e)}")
209
+
210
  logging.info(f"Indexed {len(new_files)} documents from {args.input_dir}")
211
+
212
  except Exception as e:
213
  logging.error(f"Error during startup indexing: {str(e)}")
214
 
 
218
  try:
219
  new_files = doc_manager.scan_directory()
220
  indexed_count = 0
221
+
222
  for file_path in new_files:
223
  try:
224
+ with open(file_path, "r", encoding="utf-8") as f:
225
  content = f.read()
226
  rag.insert(content)
227
  doc_manager.mark_as_indexed(file_path)
228
  indexed_count += 1
229
  except Exception as e:
230
  logging.error(f"Error indexing file {file_path}: {str(e)}")
231
+
232
  return {
233
  "status": "success",
234
  "indexed_count": indexed_count,
235
+ "total_documents": len(doc_manager.indexed_files),
236
  }
237
  except Exception as e:
238
  raise HTTPException(status_code=500, detail=str(e))
 
244
  if not doc_manager.is_supported_file(file.filename):
245
  raise HTTPException(
246
  status_code=400,
247
+ detail=f"Unsupported file type. Supported types: {doc_manager.supported_extensions}",
248
  )
249
+
250
  file_path = doc_manager.input_dir / file.filename
251
  with open(file_path, "wb") as buffer:
252
  shutil.copyfileobj(file.file, buffer)
253
+
254
  # Immediately index the uploaded file
255
  with open(file_path, "r", encoding="utf-8") as f:
256
  content = f.read()
257
  rag.insert(content)
258
  doc_manager.mark_as_indexed(file_path)
259
+
260
  return {
261
  "status": "success",
262
  "message": f"File uploaded and indexed: {file.filename}",
263
+ "total_documents": len(doc_manager.indexed_files),
264
  }
265
  except Exception as e:
266
  raise HTTPException(status_code=500, detail=str(e))
 
270
  try:
271
  response = await rag.aquery(
272
  request.query,
273
+ param=QueryParam(mode=request.mode, stream=request.stream),
274
  )
275
+
276
  if request.stream:
277
  result = ""
278
  async for chunk in response:
 
287
  async def query_text_stream(request: QueryRequest):
288
  try:
289
  response = rag.query(
290
+ request.query, param=QueryParam(mode=request.mode, stream=True)
 
291
  )
292
+
293
  async def stream_generator():
294
  async for chunk in response:
295
  yield chunk
296
+
297
  return stream_generator()
298
  except Exception as e:
299
  raise HTTPException(status_code=500, detail=str(e))
 
305
  return InsertResponse(
306
  status="success",
307
  message="Text successfully inserted",
308
+ document_count=len(rag),
309
  )
310
  except Exception as e:
311
  raise HTTPException(status_code=500, detail=str(e))
312
 
313
  @app.post("/documents/file", response_model=InsertResponse)
314
+ async def insert_file(file: UploadFile = File(...), description: str = Form(None)):
 
 
 
315
  try:
316
  content = await file.read()
317
+
318
+ if file.filename.endswith((".txt", ".md")):
319
+ text = content.decode("utf-8")
320
  rag.insert(text)
321
  else:
322
  raise HTTPException(
323
  status_code=400,
324
+ detail="Unsupported file type. Only .txt and .md files are supported",
325
  )
326
+
327
  return InsertResponse(
328
  status="success",
329
  message=f"File '{file.filename}' successfully inserted",
330
+ document_count=len(rag),
331
  )
332
  except UnicodeDecodeError:
333
  raise HTTPException(status_code=400, detail="File encoding not supported")
 
339
  try:
340
  inserted_count = 0
341
  failed_files = []
342
+
343
  for file in files:
344
  try:
345
  content = await file.read()
346
+ if file.filename.endswith((".txt", ".md")):
347
+ text = content.decode("utf-8")
348
  rag.insert(text)
349
  inserted_count += 1
350
  else:
351
  failed_files.append(f"{file.filename} (unsupported type)")
352
  except Exception as e:
353
  failed_files.append(f"{file.filename} ({str(e)})")
354
+
355
  status_message = f"Successfully inserted {inserted_count} documents"
356
  if failed_files:
357
  status_message += f". Failed files: {', '.join(failed_files)}"
358
+
359
  return InsertResponse(
360
  status="success" if inserted_count > 0 else "partial_success",
361
  message=status_message,
362
+ document_count=len(rag),
363
  )
364
  except Exception as e:
365
  raise HTTPException(status_code=500, detail=str(e))
 
373
  return InsertResponse(
374
  status="success",
375
  message="All documents cleared successfully",
376
+ document_count=0,
377
  )
378
  except Exception as e:
379
  raise HTTPException(status_code=500, detail=str(e))
 
390
  "model": args.model,
391
  "embedding_model": args.embedding_model,
392
  "max_tokens": args.max_tokens,
393
+ "embedding_dim": embedding_dim,
394
+ },
395
  }
396
 
397
  return app
398
 
399
+
400
  if __name__ == "__main__":
401
  args = parse_args()
402
  import uvicorn
403
+
404
  app = create_app(args)
405
  uvicorn.run(app, host=args.host, port=args.port)
api/requirements.txt CHANGED
@@ -1,4 +1,4 @@
 
1
  fastapi
2
- uvicorn
3
  python-multipart
4
- ascii_colors
 
1
+ ascii_colors
2
  fastapi
 
3
  python-multipart
4
+ uvicorn