config.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. """
  2. Configs for the LightRAG API.
  3. """
  4. import os
  5. import re
  6. import argparse
  7. import logging
  8. from dotenv import load_dotenv
  9. from lightrag import ROLES
  10. from lightrag.utils import get_env_value, logger
  11. from lightrag.llm.binding_options import (
  12. BedrockLLMOptions,
  13. GeminiEmbeddingOptions,
  14. GeminiLLMOptions,
  15. OllamaEmbeddingOptions,
  16. OllamaLLMOptions,
  17. OpenAILLMOptions,
  18. )
  19. from lightrag.base import OllamaServerInfos
  20. import sys
  21. from lightrag.constants import (
  22. DEFAULT_WOKERS,
  23. DEFAULT_TIMEOUT,
  24. DEFAULT_TOP_K,
  25. DEFAULT_CHUNK_TOP_K,
  26. DEFAULT_MAX_ENTITY_TOKENS,
  27. DEFAULT_MAX_RELATION_TOKENS,
  28. DEFAULT_MAX_TOTAL_TOKENS,
  29. DEFAULT_COSINE_THRESHOLD,
  30. DEFAULT_RELATED_CHUNK_NUMBER,
  31. DEFAULT_MIN_RERANK_SCORE,
  32. DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE,
  33. DEFAULT_MAX_ASYNC,
  34. DEFAULT_SUMMARY_MAX_TOKENS,
  35. DEFAULT_SUMMARY_LENGTH_RECOMMENDED,
  36. DEFAULT_SUMMARY_CONTEXT_SIZE,
  37. DEFAULT_SUMMARY_LANGUAGE,
  38. DEFAULT_EMBEDDING_FUNC_MAX_ASYNC,
  39. DEFAULT_EMBEDDING_BATCH_NUM,
  40. DEFAULT_OLLAMA_MODEL_NAME,
  41. DEFAULT_OLLAMA_MODEL_TAG,
  42. DEFAULT_RERANK_BINDING,
  43. DEFAULT_LLM_TIMEOUT,
  44. DEFAULT_EMBEDDING_TIMEOUT,
  45. DEFAULT_RERANK_TIMEOUT,
  46. )
  47. # use the .env that is inside the current folder
  48. # allows to use different .env file for each lightrag instance
  49. # the OS environment variables take precedence over the .env file
  50. load_dotenv(dotenv_path=".env", override=False)
  51. ollama_server_infos = OllamaServerInfos()
  52. DEFAULT_TOKEN_SECRET = "lightrag-jwt-default-secret-key!"
  53. NO_PREFIX_SENTINEL = "NO_PREFIX"
  54. PROVIDER_ASYMMETRIC_EMBEDDING_BINDINGS = {"gemini", "jina", "voyageai"}
  55. PREFIX_ASYMMETRIC_EMBEDDING_BINDINGS = {"azure_openai", "ollama", "openai"}
  56. class DefaultRAGStorageConfig:
  57. KV_STORAGE = "JsonKVStorage"
  58. VECTOR_STORAGE = "NanoVectorDBStorage"
  59. GRAPH_STORAGE = "NetworkXStorage"
  60. DOC_STATUS_STORAGE = "JsonDocStatusStorage"
  61. def get_default_host(binding_type: str) -> str:
  62. default_hosts = {
  63. "ollama": os.getenv("LLM_BINDING_HOST", "http://localhost:11434"),
  64. "lollms": os.getenv("LLM_BINDING_HOST", "http://localhost:9600"),
  65. "azure_openai": os.getenv("AZURE_OPENAI_ENDPOINT", "https://api.openai.com/v1"),
  66. "openai": os.getenv("LLM_BINDING_HOST", "https://api.openai.com/v1"),
  67. # Let boto3 select the regional Bedrock endpoint unless the user
  68. # explicitly overrides LLM_BINDING_HOST / EMBEDDING_BINDING_HOST.
  69. "bedrock": os.getenv("LLM_BINDING_HOST", "DEFAULT_BEDROCK_ENDPOINT"),
  70. # Let google-genai pick the correct default endpoint/version unless the
  71. # user explicitly overrides LLM_BINDING_HOST / EMBEDDING_BINDING_HOST.
  72. "gemini": os.getenv("LLM_BINDING_HOST", "DEFAULT_GEMINI_ENDPOINT"),
  73. }
  74. return default_hosts.get(
  75. binding_type, os.getenv("LLM_BINDING_HOST", "http://localhost:11434")
  76. ) # fallback to ollama if unknown
  77. def resolve_asymmetric_embedding_opt_in(
  78. *,
  79. binding: str,
  80. embedding_asymmetric: bool,
  81. embedding_asymmetric_configured: bool,
  82. query_prefix: str | None,
  83. document_prefix: str | None,
  84. query_prefix_configured: bool = False,
  85. document_prefix_configured: bool = False,
  86. ) -> bool:
  87. """Resolve whether query/document-aware embedding behavior should be enabled."""
  88. has_non_empty_prefix = bool(query_prefix or document_prefix)
  89. has_prefix_config = query_prefix_configured or document_prefix_configured
  90. if not embedding_asymmetric:
  91. if has_prefix_config:
  92. state = "false" if embedding_asymmetric_configured else "unset"
  93. logger.warning(
  94. f"EMBEDDING_ASYMMETRIC is {state}; "
  95. "EMBEDDING_QUERY_PREFIX and EMBEDDING_DOCUMENT_PREFIX will be ignored."
  96. )
  97. return False
  98. if binding in PROVIDER_ASYMMETRIC_EMBEDDING_BINDINGS:
  99. if has_prefix_config:
  100. logger.warning(
  101. f"{binding} embeddings use provider task parameters for asymmetric "
  102. "mode; EMBEDDING_QUERY_PREFIX and EMBEDDING_DOCUMENT_PREFIX will be ignored."
  103. )
  104. return True
  105. if binding in PREFIX_ASYMMETRIC_EMBEDDING_BINDINGS:
  106. if not query_prefix_configured or not document_prefix_configured:
  107. raise ValueError(
  108. f"EMBEDDING_ASYMMETRIC=true for {binding} embeddings requires both "
  109. "EMBEDDING_QUERY_PREFIX and EMBEDDING_DOCUMENT_PREFIX. Use "
  110. f"{NO_PREFIX_SENTINEL} for a side that should intentionally have no prefix."
  111. )
  112. if not has_non_empty_prefix:
  113. raise ValueError(
  114. "At least one of EMBEDDING_QUERY_PREFIX or EMBEDDING_DOCUMENT_PREFIX "
  115. f"must be non-empty. Use {NO_PREFIX_SENTINEL} only for the side that "
  116. "should intentionally have no prefix."
  117. )
  118. return True
  119. raise ValueError(
  120. f"EMBEDDING_ASYMMETRIC=true is not supported for {binding} embeddings."
  121. )
  122. def get_embedding_prefix_config(env_key: str) -> tuple[str | None, bool]:
  123. """Read an embedding prefix and whether it was explicitly configured."""
  124. if env_key not in os.environ:
  125. return None, False
  126. value = os.environ[env_key]
  127. if value == "None":
  128. return None, False
  129. if value == NO_PREFIX_SENTINEL:
  130. return "", True
  131. if value == "":
  132. raise ValueError(
  133. f"{env_key} is empty. Use {NO_PREFIX_SENTINEL} to explicitly request "
  134. "no prefix, or remove the variable to leave it unconfigured."
  135. )
  136. return value, True
  137. def validate_auth_configuration(args: argparse.Namespace) -> None:
  138. """Reject insecure JWT auth settings before the API starts."""
  139. auth_accounts = (getattr(args, "auth_accounts", "") or "").strip()
  140. token_secret = (getattr(args, "token_secret", "") or "").strip()
  141. if auth_accounts and (not token_secret or token_secret == DEFAULT_TOKEN_SECRET):
  142. raise ValueError(
  143. "TOKEN_SECRET must be explicitly set to a non-default value when AUTH_ACCOUNTS is configured."
  144. )
  145. def _is_set(value: str | None) -> bool:
  146. return bool((value or "").strip())
  147. def validate_bedrock_auth_configuration(args: argparse.Namespace) -> None:
  148. """Reject Bedrock configuration with no explicit supported auth source."""
  149. bearer_token = os.getenv("AWS_BEARER_TOKEN_BEDROCK")
  150. def has_valid_auth(prefix: str | None = None) -> bool:
  151. if _is_set(bearer_token):
  152. return True
  153. if prefix:
  154. role_access_key = getattr(args, f"{prefix}_aws_access_key_id", None)
  155. role_secret_key = getattr(args, f"{prefix}_aws_secret_access_key", None)
  156. if _is_set(role_access_key) or _is_set(role_secret_key):
  157. return _is_set(role_access_key) and _is_set(role_secret_key)
  158. access_key = getattr(args, "aws_access_key_id", None)
  159. secret_key = getattr(args, "aws_secret_access_key", None)
  160. return _is_set(access_key) and _is_set(secret_key)
  161. if getattr(args, "llm_binding", None) == "bedrock":
  162. if not has_valid_auth():
  163. raise ValueError(
  164. "Bedrock LLM binding requires AWS_ACCESS_KEY_ID and "
  165. "AWS_SECRET_ACCESS_KEY, or process-level AWS_BEARER_TOKEN_BEDROCK."
  166. )
  167. if _is_set(getattr(args, "llm_binding_api_key", None)):
  168. logging.warning(
  169. "LLM_BINDING_API_KEY is set but ignored for Bedrock LLM binding. "
  170. "Use SigV4 AWS_* variables or process-level AWS_BEARER_TOKEN_BEDROCK instead."
  171. )
  172. if getattr(args, "embedding_binding", None) == "bedrock":
  173. if not has_valid_auth():
  174. raise ValueError(
  175. "Bedrock embedding binding requires AWS_ACCESS_KEY_ID and "
  176. "AWS_SECRET_ACCESS_KEY, or process-level AWS_BEARER_TOKEN_BEDROCK."
  177. )
  178. if _is_set(getattr(args, "embedding_binding_api_key", None)):
  179. logging.warning(
  180. "EMBEDDING_BINDING_API_KEY is set but ignored for Bedrock embedding binding. "
  181. "Use SigV4 AWS_* variables or process-level AWS_BEARER_TOKEN_BEDROCK instead."
  182. )
  183. for spec in ROLES:
  184. role = spec.name
  185. if getattr(
  186. args, f"{role}_llm_binding", None
  187. ) == "bedrock" and not has_valid_auth(role):
  188. raise ValueError(
  189. f"Bedrock role '{role}' requires {spec.env_prefix}_AWS_ACCESS_KEY_ID "
  190. f"and {spec.env_prefix}_AWS_SECRET_ACCESS_KEY, global "
  191. "AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, or process-level "
  192. "AWS_BEARER_TOKEN_BEDROCK."
  193. )
  194. def normalize_binding_name(binding: str | None) -> str | None:
  195. """Normalize environment-provided binding aliases to canonical names."""
  196. if binding == "aws_bedrock":
  197. return "bedrock"
  198. return binding
  199. def get_binding_env_value(env_key: str, default: str) -> str:
  200. """Read a binding env var and normalize legacy aliases."""
  201. return normalize_binding_name(get_env_value(env_key, default)) or default
  202. def parse_args() -> argparse.Namespace:
  203. """
  204. Parse command line arguments with environment variable fallback
  205. Args:
  206. is_uvicorn_mode: Whether running under uvicorn mode
  207. Returns:
  208. argparse.Namespace: Parsed arguments
  209. """
  210. parser = argparse.ArgumentParser(description="LightRAG API Server")
  211. # Server configuration
  212. parser.add_argument(
  213. "--host",
  214. default=get_env_value("HOST", "0.0.0.0"),
  215. help="Server host (default: from env or 0.0.0.0)",
  216. )
  217. parser.add_argument(
  218. "--port",
  219. type=int,
  220. default=get_env_value("PORT", 9621, int),
  221. help="Server port (default: from env or 9621)",
  222. )
  223. # Directory configuration
  224. parser.add_argument(
  225. "--working-dir",
  226. default=get_env_value("WORKING_DIR", "./rag_storage"),
  227. help="Working directory for RAG storage (default: from env or ./rag_storage)",
  228. )
  229. parser.add_argument(
  230. "--input-dir",
  231. default=get_env_value("INPUT_DIR", "./inputs"),
  232. help="Directory containing input documents (default: from env or ./inputs)",
  233. )
  234. parser.add_argument(
  235. "--timeout",
  236. default=get_env_value("TIMEOUT", DEFAULT_TIMEOUT, int, special_none=True),
  237. type=int,
  238. help="Timeout in seconds (useful when using slow AI). Use None for infinite timeout",
  239. )
  240. # RAG configuration
  241. parser.add_argument(
  242. "--max-async",
  243. type=int,
  244. default=get_env_value("MAX_ASYNC", DEFAULT_MAX_ASYNC, int),
  245. help=f"Maximum async operations (default: from env or {DEFAULT_MAX_ASYNC})",
  246. )
  247. parser.add_argument(
  248. "--summary-max-tokens",
  249. type=int,
  250. default=get_env_value("SUMMARY_MAX_TOKENS", DEFAULT_SUMMARY_MAX_TOKENS, int),
  251. help=f"Maximum token size for entity/relation summary(default: from env or {DEFAULT_SUMMARY_MAX_TOKENS})",
  252. )
  253. parser.add_argument(
  254. "--summary-context-size",
  255. type=int,
  256. default=get_env_value(
  257. "SUMMARY_CONTEXT_SIZE", DEFAULT_SUMMARY_CONTEXT_SIZE, int
  258. ),
  259. help=f"LLM Summary Context size (default: from env or {DEFAULT_SUMMARY_CONTEXT_SIZE})",
  260. )
  261. parser.add_argument(
  262. "--summary-length-recommended",
  263. type=int,
  264. default=get_env_value(
  265. "SUMMARY_LENGTH_RECOMMENDED", DEFAULT_SUMMARY_LENGTH_RECOMMENDED, int
  266. ),
  267. help=f"LLM Summary Context size (default: from env or {DEFAULT_SUMMARY_LENGTH_RECOMMENDED})",
  268. )
  269. # Logging configuration
  270. parser.add_argument(
  271. "--log-level",
  272. default=get_env_value("LOG_LEVEL", "INFO"),
  273. choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
  274. help="Logging level (default: from env or INFO)",
  275. )
  276. parser.add_argument(
  277. "--verbose",
  278. action="store_true",
  279. default=get_env_value("VERBOSE", False, bool),
  280. help="Enable verbose debug output(only valid for DEBUG log-level)",
  281. )
  282. parser.add_argument(
  283. "--key",
  284. type=str,
  285. default=get_env_value("LIGHTRAG_API_KEY", None),
  286. help="API key for authentication. This protects lightrag server against unauthorized access",
  287. )
  288. # Optional https parameters
  289. parser.add_argument(
  290. "--ssl",
  291. action="store_true",
  292. default=get_env_value("SSL", False, bool),
  293. help="Enable HTTPS (default: from env or False)",
  294. )
  295. parser.add_argument(
  296. "--ssl-certfile",
  297. default=get_env_value("SSL_CERTFILE", None),
  298. help="Path to SSL certificate file (required if --ssl is enabled)",
  299. )
  300. parser.add_argument(
  301. "--ssl-keyfile",
  302. default=get_env_value("SSL_KEYFILE", None),
  303. help="Path to SSL private key file (required if --ssl is enabled)",
  304. )
  305. # Ollama model configuration
  306. parser.add_argument(
  307. "--simulated-model-name",
  308. type=str,
  309. default=get_env_value("OLLAMA_EMULATING_MODEL_NAME", DEFAULT_OLLAMA_MODEL_NAME),
  310. help="Name for the simulated Ollama model (default: from env or lightrag)",
  311. )
  312. parser.add_argument(
  313. "--simulated-model-tag",
  314. type=str,
  315. default=get_env_value("OLLAMA_EMULATING_MODEL_TAG", DEFAULT_OLLAMA_MODEL_TAG),
  316. help="Tag for the simulated Ollama model (default: from env or latest)",
  317. )
  318. # Namespace
  319. parser.add_argument(
  320. "--workspace",
  321. type=str,
  322. default=get_env_value("WORKSPACE", ""),
  323. help="Default workspace for all storage",
  324. )
  325. # Path prefix configuration
  326. parser.add_argument(
  327. "--api-prefix",
  328. type=str,
  329. default=get_env_value("LIGHTRAG_API_PREFIX", ""),
  330. help="API path prefix (e.g., /api/v1). Prepended to all API routes. Default: none (root).",
  331. )
  332. # Server workers configuration
  333. parser.add_argument(
  334. "--workers",
  335. type=int,
  336. default=get_env_value("WORKERS", DEFAULT_WOKERS, int),
  337. help="Number of worker processes (default: from env or 1)",
  338. )
  339. # LLM and embedding bindings
  340. parser.add_argument(
  341. "--llm-binding",
  342. type=str,
  343. default=get_binding_env_value("LLM_BINDING", "ollama"),
  344. choices=[
  345. "lollms",
  346. "ollama",
  347. "openai",
  348. "openai-ollama",
  349. "azure_openai",
  350. "bedrock",
  351. "gemini",
  352. ],
  353. help="LLM binding type (default: from env or ollama)",
  354. )
  355. parser.add_argument(
  356. "--embedding-binding",
  357. type=str,
  358. default=get_binding_env_value("EMBEDDING_BINDING", "ollama"),
  359. choices=[
  360. "lollms",
  361. "ollama",
  362. "openai",
  363. "azure_openai",
  364. "bedrock",
  365. "jina",
  366. "gemini",
  367. "voyageai",
  368. ],
  369. help="Embedding binding type (default: from env or ollama)",
  370. )
  371. parser.add_argument(
  372. "--rerank-binding",
  373. type=str,
  374. default=get_env_value("RERANK_BINDING", DEFAULT_RERANK_BINDING),
  375. choices=["null", "cohere", "jina", "aliyun"],
  376. help=f"Rerank binding type (default: from env or {DEFAULT_RERANK_BINDING})",
  377. )
  378. # Conditionally add binding-specific options (Ollama, OpenAI, Azure OpenAI, Gemini)
  379. # This registers command line arguments (e.g., --openai-llm-temperature)
  380. # and reads corresponding environment variables (e.g., OPENAI_LLM_TEMPERATURE)
  381. # Determine LLM binding value consistently from command line or environment
  382. llm_binding_value = None
  383. if "--llm-binding" in sys.argv:
  384. try:
  385. idx = sys.argv.index("--llm-binding")
  386. if idx + 1 < len(sys.argv) and not sys.argv[idx + 1].startswith("-"):
  387. llm_binding_value = sys.argv[idx + 1]
  388. except IndexError:
  389. pass
  390. # Fall back to environment variable using same function as argparse default
  391. if llm_binding_value is None:
  392. llm_binding_value = get_binding_env_value("LLM_BINDING", "ollama")
  393. # Add LLM binding options based on determined value
  394. if llm_binding_value == "ollama":
  395. OllamaLLMOptions.add_args(parser)
  396. elif llm_binding_value in ["openai", "azure_openai"]:
  397. OpenAILLMOptions.add_args(parser)
  398. elif llm_binding_value == "gemini":
  399. GeminiLLMOptions.add_args(parser)
  400. elif llm_binding_value == "bedrock":
  401. BedrockLLMOptions.add_args(parser)
  402. # Determine embedding binding value consistently from command line or environment
  403. embedding_binding_value = None
  404. if "--embedding-binding" in sys.argv:
  405. try:
  406. idx = sys.argv.index("--embedding-binding")
  407. if idx + 1 < len(sys.argv) and not sys.argv[idx + 1].startswith("-"):
  408. embedding_binding_value = sys.argv[idx + 1]
  409. except IndexError:
  410. pass
  411. # Fall back to environment variable using same function as argparse default
  412. if embedding_binding_value is None:
  413. embedding_binding_value = get_binding_env_value("EMBEDDING_BINDING", "ollama")
  414. # Add embedding binding options based on determined value
  415. if embedding_binding_value == "ollama":
  416. OllamaEmbeddingOptions.add_args(parser)
  417. elif embedding_binding_value == "gemini":
  418. GeminiEmbeddingOptions.add_args(parser)
  419. args = parser.parse_args()
  420. # convert relative path to absolute path
  421. args.working_dir = os.path.abspath(args.working_dir)
  422. args.input_dir = os.path.abspath(args.input_dir)
  423. # Inject storage configuration from environment variables
  424. args.kv_storage = get_env_value(
  425. "LIGHTRAG_KV_STORAGE", DefaultRAGStorageConfig.KV_STORAGE
  426. )
  427. args.doc_status_storage = get_env_value(
  428. "LIGHTRAG_DOC_STATUS_STORAGE", DefaultRAGStorageConfig.DOC_STATUS_STORAGE
  429. )
  430. args.graph_storage = get_env_value(
  431. "LIGHTRAG_GRAPH_STORAGE", DefaultRAGStorageConfig.GRAPH_STORAGE
  432. )
  433. args.vector_storage = get_env_value(
  434. "LIGHTRAG_VECTOR_STORAGE", DefaultRAGStorageConfig.VECTOR_STORAGE
  435. )
  436. # Get MAX_PARALLEL_INSERT from environment
  437. args.max_parallel_insert = get_env_value("MAX_PARALLEL_INSERT", 2, int)
  438. # Get MAX_GRAPH_NODES from environment
  439. args.max_graph_nodes = get_env_value("MAX_GRAPH_NODES", 1000, int)
  440. # Handle openai-ollama special case
  441. if args.llm_binding == "openai-ollama":
  442. args.llm_binding = "openai"
  443. args.embedding_binding = "ollama"
  444. args.llm_binding_host = get_env_value(
  445. "LLM_BINDING_HOST", get_default_host(args.llm_binding)
  446. )
  447. args.embedding_binding_host = get_env_value(
  448. "EMBEDDING_BINDING_HOST", get_default_host(args.embedding_binding)
  449. )
  450. args.llm_binding_api_key = get_env_value("LLM_BINDING_API_KEY", None)
  451. args.embedding_binding_api_key = get_env_value("EMBEDDING_BINDING_API_KEY", "")
  452. args.aws_region = get_env_value("AWS_REGION", None, special_none=True)
  453. args.aws_access_key_id = get_env_value("AWS_ACCESS_KEY_ID", None, special_none=True)
  454. args.aws_secret_access_key = get_env_value(
  455. "AWS_SECRET_ACCESS_KEY", None, special_none=True
  456. )
  457. args.aws_session_token = get_env_value("AWS_SESSION_TOKEN", None, special_none=True)
  458. # Inject model configuration
  459. args.llm_model = get_env_value("LLM_MODEL", "mistral-nemo:latest")
  460. # EMBEDDING_MODEL defaults to None - each binding will use its own default model
  461. # e.g., OpenAI uses "text-embedding-3-small", Jina uses "jina-embeddings-v4"
  462. args.embedding_model = get_env_value("EMBEDDING_MODEL", None, special_none=True)
  463. # EMBEDDING_DIM defaults to None - each binding will use its own default dimension
  464. # Value is inherited from provider defaults via wrap_embedding_func_with_attrs decorator
  465. args.embedding_dim = get_env_value("EMBEDDING_DIM", None, int, special_none=True)
  466. args.embedding_send_dim = get_env_value("EMBEDDING_SEND_DIM", False, bool)
  467. # Inject chunk configuration
  468. args.chunk_size = get_env_value("CHUNK_SIZE", 1200, int)
  469. args.chunk_overlap_size = get_env_value("CHUNK_OVERLAP_SIZE", 100, int)
  470. # Inject LLM cache configuration
  471. args.enable_llm_cache_for_extract = get_env_value(
  472. "ENABLE_LLM_CACHE_FOR_EXTRACT", True, bool
  473. )
  474. args.enable_llm_cache = get_env_value("ENABLE_LLM_CACHE", True, bool)
  475. # PDF decryption password
  476. args.pdf_decrypt_password = get_env_value("PDF_DECRYPT_PASSWORD", None)
  477. # --- Per-role LLM configuration (driven by lightrag.ROLES registry) ---
  478. for spec in ROLES:
  479. prefix = spec.env_prefix
  480. attr_prefix = spec.name
  481. binding_key = f"{prefix}_LLM_BINDING"
  482. model_key = f"{prefix}_LLM_MODEL"
  483. host_key = f"{prefix}_LLM_BINDING_HOST"
  484. apikey_key = f"{prefix}_LLM_BINDING_API_KEY"
  485. max_async_key = f"{prefix}_MAX_ASYNC_LLM"
  486. timeout_key = f"{prefix}_LLM_TIMEOUT"
  487. role_binding = normalize_binding_name(
  488. get_env_value(binding_key, None, special_none=True)
  489. )
  490. role_model = get_env_value(model_key, None, special_none=True)
  491. role_host = get_env_value(host_key, None, special_none=True)
  492. role_apikey = get_env_value(apikey_key, None, special_none=True)
  493. role_max_async = get_env_value(max_async_key, None, int, special_none=True)
  494. role_timeout = get_env_value(timeout_key, None, int, special_none=True)
  495. role_aws_region = get_env_value(f"{prefix}_AWS_REGION", None, special_none=True)
  496. role_aws_access_key_id = get_env_value(
  497. f"{prefix}_AWS_ACCESS_KEY_ID", None, special_none=True
  498. )
  499. role_aws_secret_access_key = get_env_value(
  500. f"{prefix}_AWS_SECRET_ACCESS_KEY", None, special_none=True
  501. )
  502. role_aws_session_token = get_env_value(
  503. f"{prefix}_AWS_SESSION_TOKEN", None, special_none=True
  504. )
  505. setattr(args, f"{attr_prefix}_llm_binding", role_binding)
  506. setattr(args, f"{attr_prefix}_llm_model", role_model)
  507. setattr(args, f"{attr_prefix}_llm_binding_host", role_host)
  508. setattr(args, f"{attr_prefix}_llm_binding_api_key", role_apikey)
  509. setattr(args, f"{attr_prefix}_llm_max_async", role_max_async)
  510. setattr(args, f"{attr_prefix}_llm_timeout", role_timeout)
  511. setattr(args, f"{attr_prefix}_aws_region", role_aws_region)
  512. setattr(args, f"{attr_prefix}_aws_access_key_id", role_aws_access_key_id)
  513. setattr(
  514. args, f"{attr_prefix}_aws_secret_access_key", role_aws_secret_access_key
  515. )
  516. setattr(args, f"{attr_prefix}_aws_session_token", role_aws_session_token)
  517. if role_binding == "bedrock" and role_apikey:
  518. raise SystemExit(
  519. f"Bedrock role '{spec.name}' does not support {apikey_key}; use "
  520. "role-specific SigV4 AWS_* variables or process-level "
  521. "AWS_BEARER_TOKEN_BEDROCK."
  522. )
  523. # Cross-provider validation
  524. if role_binding and role_binding != args.llm_binding:
  525. missing = []
  526. if not role_model:
  527. missing.append(model_key)
  528. if not role_host:
  529. role_host = get_default_host(role_binding)
  530. setattr(args, f"{attr_prefix}_llm_binding_host", role_host)
  531. if role_binding != "bedrock" and not role_apikey:
  532. missing.append(apikey_key)
  533. if missing:
  534. raise SystemExit(
  535. f"Cross-provider error for role '{spec.name}': "
  536. f"binding={role_binding} differs from base={args.llm_binding}, "
  537. f"but required env vars are missing: {', '.join(missing)}"
  538. )
  539. # VLM multimodal master switch — when off, the pipeline emits a warning
  540. # and skips every i/t/e item without touching the VLM. When on, the
  541. # effective VLM binding must support image inputs.
  542. args.vlm_process_enable = get_env_value("VLM_PROCESS_ENABLE", False, bool)
  543. if args.vlm_process_enable:
  544. effective_vlm_binding = (
  545. getattr(args, "vlm_llm_binding", None) or args.llm_binding
  546. )
  547. vlm_incompatible = {"lollms"}
  548. if effective_vlm_binding in vlm_incompatible:
  549. raise SystemExit(
  550. f"VLM_PROCESS_ENABLE=true but the effective VLM binding "
  551. f"'{effective_vlm_binding}' does not support image inputs. "
  552. "Configure VLM_LLM_BINDING (or LLM_BINDING) to one of: "
  553. "openai, azure_openai, gemini, bedrock, ollama."
  554. )
  555. # Add environment variables that were previously read directly
  556. args.cors_origins = get_env_value("CORS_ORIGINS", "*")
  557. args.summary_language = get_env_value("SUMMARY_LANGUAGE", DEFAULT_SUMMARY_LANGUAGE)
  558. args.whitelist_paths = get_env_value("WHITELIST_PATHS", "/health,/api/*")
  559. # For JWT Auth
  560. args.auth_accounts = get_env_value("AUTH_ACCOUNTS", "")
  561. args.token_secret = get_env_value("TOKEN_SECRET", None)
  562. args.token_expire_hours = get_env_value("TOKEN_EXPIRE_HOURS", 48, float)
  563. args.guest_token_expire_hours = get_env_value("GUEST_TOKEN_EXPIRE_HOURS", 24, float)
  564. args.jwt_algorithm = get_env_value("JWT_ALGORITHM", "HS256")
  565. # Token auto-renewal configuration (sliding window expiration)
  566. args.token_auto_renew = get_env_value("TOKEN_AUTO_RENEW", True, bool)
  567. args.token_renew_threshold = get_env_value("TOKEN_RENEW_THRESHOLD", 0.5, float)
  568. # Rerank model configuration
  569. args.rerank_model = get_env_value("RERANK_MODEL", None)
  570. args.rerank_binding_host = get_env_value("RERANK_BINDING_HOST", None)
  571. args.rerank_binding_api_key = get_env_value("RERANK_BINDING_API_KEY", None)
  572. # Note: rerank_binding is already set by argparse, no need to override from env
  573. # Min rerank score configuration
  574. args.min_rerank_score = get_env_value(
  575. "MIN_RERANK_SCORE", DEFAULT_MIN_RERANK_SCORE, float
  576. )
  577. # LLM / Embedding request timeouts
  578. args.llm_timeout = get_env_value("LLM_TIMEOUT", DEFAULT_LLM_TIMEOUT, int)
  579. args.embedding_timeout = get_env_value(
  580. "EMBEDDING_TIMEOUT", DEFAULT_EMBEDDING_TIMEOUT, int
  581. )
  582. # Rerank async/timeout configuration (independent from base LLM)
  583. # rerank_max_async falls back to MAX_ASYNC; rerank_timeout has its own default.
  584. args.rerank_max_async = get_env_value("MAX_ASYNC_RERANK", args.max_async, int)
  585. args.rerank_timeout = get_env_value("RERANK_TIMEOUT", DEFAULT_RERANK_TIMEOUT, int)
  586. # Query configuration
  587. args.top_k = get_env_value("TOP_K", DEFAULT_TOP_K, int)
  588. args.chunk_top_k = get_env_value("CHUNK_TOP_K", DEFAULT_CHUNK_TOP_K, int)
  589. args.max_entity_tokens = get_env_value(
  590. "MAX_ENTITY_TOKENS", DEFAULT_MAX_ENTITY_TOKENS, int
  591. )
  592. args.max_relation_tokens = get_env_value(
  593. "MAX_RELATION_TOKENS", DEFAULT_MAX_RELATION_TOKENS, int
  594. )
  595. args.max_total_tokens = get_env_value(
  596. "MAX_TOTAL_TOKENS", DEFAULT_MAX_TOTAL_TOKENS, int
  597. )
  598. args.cosine_threshold = get_env_value(
  599. "COSINE_THRESHOLD", DEFAULT_COSINE_THRESHOLD, float
  600. )
  601. args.related_chunk_number = get_env_value(
  602. "RELATED_CHUNK_NUMBER", DEFAULT_RELATED_CHUNK_NUMBER, int
  603. )
  604. # Add missing environment variables for health endpoint
  605. args.force_llm_summary_on_merge = get_env_value(
  606. "FORCE_LLM_SUMMARY_ON_MERGE", DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE, int
  607. )
  608. args.embedding_func_max_async = get_env_value(
  609. "EMBEDDING_FUNC_MAX_ASYNC", DEFAULT_EMBEDDING_FUNC_MAX_ASYNC, int
  610. )
  611. args.embedding_batch_num = get_env_value(
  612. "EMBEDDING_BATCH_NUM", DEFAULT_EMBEDDING_BATCH_NUM, int
  613. )
  614. # Embedding token limit configuration
  615. args.embedding_token_limit = get_env_value(
  616. "EMBEDDING_TOKEN_LIMIT", None, int, special_none=True
  617. )
  618. # File upload size limit (in bytes, None for unlimited)
  619. # Default: 100MB (104857600 bytes)
  620. args.max_upload_size = get_env_value(
  621. "MAX_UPLOAD_SIZE", 104857600, int, special_none=True
  622. )
  623. # Embedding prefix configuration for context-aware embeddings. Empty prefixes
  624. # must be explicit via NO_PREFIX so missing config is distinguishable.
  625. (
  626. args.embedding_document_prefix,
  627. args.embedding_document_prefix_configured,
  628. ) = get_embedding_prefix_config("EMBEDDING_DOCUMENT_PREFIX")
  629. (
  630. args.embedding_query_prefix,
  631. args.embedding_query_prefix_configured,
  632. ) = get_embedding_prefix_config("EMBEDDING_QUERY_PREFIX")
  633. args.embedding_prefix_no_prefix_sentinel = NO_PREFIX_SENTINEL
  634. args.embedding_prefixes_configured = (
  635. args.embedding_document_prefix_configured
  636. or args.embedding_query_prefix_configured
  637. )
  638. # Asymmetric embedding behavior toggle
  639. args.embedding_asymmetric_configured = "EMBEDDING_ASYMMETRIC" in os.environ
  640. args.embedding_asymmetric = get_env_value("EMBEDDING_ASYMMETRIC", False, bool)
  641. ollama_server_infos.LIGHTRAG_NAME = args.simulated_model_name
  642. ollama_server_infos.LIGHTRAG_TAG = args.simulated_model_tag
  643. # Sanitize workspace: only alphanumeric characters and underscores are allowed
  644. if args.workspace:
  645. sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", args.workspace)
  646. if sanitized != args.workspace:
  647. logging.warning(
  648. f"Workspace name '{args.workspace}' contains invalid characters. "
  649. f"It has been sanitized to '{sanitized}'. "
  650. "Only alphanumeric characters and underscores are allowed."
  651. )
  652. args.workspace = sanitized
  653. validate_auth_configuration(args)
  654. validate_bedrock_auth_configuration(args)
  655. return args
  656. def update_uvicorn_mode_config():
  657. # If in uvicorn mode and workers > 1, force it to 1 and log warning
  658. if global_args.workers > 1:
  659. original_workers = global_args.workers
  660. global_args.workers = 1
  661. # Log warning directly here
  662. logging.debug(
  663. f">> Forcing workers=1 in uvicorn mode(Ignoring workers={original_workers})"
  664. )
  665. # Global configuration with lazy initialization
  666. _global_args = None
  667. _initialized = False
  668. def initialize_config(args=None, force=False):
  669. """Initialize global configuration
  670. This function allows explicit initialization of the configuration,
  671. which is useful for programmatic usage, testing, or embedding LightRAG
  672. in other applications.
  673. Args:
  674. args: Pre-parsed argparse.Namespace or None to parse from sys.argv
  675. force: Force re-initialization even if already initialized
  676. Returns:
  677. argparse.Namespace: The configured arguments
  678. Example:
  679. # Use parsed command line arguments (default)
  680. initialize_config()
  681. # Use custom configuration programmatically
  682. custom_args = argparse.Namespace(
  683. host='localhost',
  684. port=8080,
  685. working_dir='./custom_rag',
  686. # ... other config
  687. )
  688. initialize_config(custom_args)
  689. """
  690. global _global_args, _initialized
  691. if _initialized and not force:
  692. return _global_args
  693. resolved_args = args if args is not None else parse_args()
  694. validate_auth_configuration(resolved_args)
  695. validate_bedrock_auth_configuration(resolved_args)
  696. _global_args = resolved_args
  697. _initialized = True
  698. return _global_args
  699. def get_config():
  700. """Get global configuration, auto-initializing if needed
  701. Returns:
  702. argparse.Namespace: The configured arguments
  703. """
  704. if not _initialized:
  705. initialize_config()
  706. return _global_args
  707. class _GlobalArgsProxy:
  708. """Proxy object that auto-initializes configuration on first access
  709. This maintains backward compatibility with existing code while
  710. allowing programmatic control over initialization timing.
  711. The proxy fully delegates to the underlying argparse.Namespace,
  712. including support for vars() calls which is used by binding_options
  713. to extract provider-specific configuration options.
  714. """
  715. def __getattribute__(self, name):
  716. """Override attribute access to support vars() and regular attribute access.
  717. This method intercepts __dict__ access (used by vars()) and delegates
  718. to the underlying _global_args namespace, ensuring binding options
  719. can be properly extracted.
  720. """
  721. global _initialized, _global_args
  722. # Handle __dict__ access for vars() support
  723. if name == "__dict__":
  724. if not _initialized:
  725. initialize_config()
  726. return vars(_global_args)
  727. # Handle class-level attributes that should come from the proxy itself
  728. if name in ("__class__", "__repr__", "__getattribute__", "__setattr__"):
  729. return object.__getattribute__(self, name)
  730. # Delegate all other attribute access to the underlying namespace
  731. if not _initialized:
  732. initialize_config()
  733. return getattr(_global_args, name)
  734. def __setattr__(self, name, value):
  735. global _initialized, _global_args
  736. if not _initialized:
  737. initialize_config()
  738. setattr(_global_args, name, value)
  739. def __repr__(self):
  740. global _initialized, _global_args
  741. if not _initialized:
  742. return "<GlobalArgsProxy: Not initialized>"
  743. return repr(_global_args)
  744. # Create proxy instance for backward compatibility
  745. # Existing code like `from config import global_args` continues to work
  746. # The proxy will auto-initialize on first attribute access
  747. global_args = _GlobalArgsProxy()