lightrag_server.py 111 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637
  1. """
  2. LightRAG FastAPI Server
  3. """
  4. from fastapi import FastAPI, Depends, HTTPException, Request
  5. from fastapi.exceptions import RequestValidationError
  6. from fastapi.responses import JSONResponse, FileResponse, HTMLResponse, Response
  7. from fastapi.openapi.docs import (
  8. get_swagger_ui_html,
  9. get_swagger_ui_oauth2_redirect_html,
  10. )
  11. import json
  12. import os
  13. import re
  14. import logging
  15. import logging.config
  16. import sys
  17. import textwrap
  18. import uvicorn
  19. import pipmaster as pm
  20. from typing import Any
  21. from fastapi.staticfiles import StaticFiles
  22. from fastapi.responses import RedirectResponse
  23. from pathlib import Path
  24. from ascii_colors import ASCIIColors
  25. from fastapi.middleware.cors import CORSMiddleware
  26. from contextlib import asynccontextmanager
  27. from dotenv import load_dotenv
  28. from lightrag.api.utils_api import (
  29. get_combined_auth_dependency,
  30. display_splash_screen,
  31. check_env_file,
  32. )
  33. from .config import (
  34. global_args,
  35. update_uvicorn_mode_config,
  36. get_default_host,
  37. resolve_asymmetric_embedding_opt_in,
  38. PREFIX_ASYMMETRIC_EMBEDDING_BINDINGS,
  39. )
  40. from lightrag.utils import get_env_value
  41. from lightrag import LightRAG, ROLES, RoleLLMConfig, __version__ as core_version
  42. from lightrag.api import __api_version__
  43. from lightrag.utils import EmbeddingFunc
  44. from lightrag.constants import (
  45. DEFAULT_LOG_MAX_BYTES,
  46. DEFAULT_LOG_BACKUP_COUNT,
  47. DEFAULT_LOG_FILENAME,
  48. )
  49. from lightrag.api.routers.document_routes import (
  50. DocumentManager,
  51. create_document_routes,
  52. )
  53. from lightrag.parser.routing import (
  54. parser_rules_from_env,
  55. validate_parser_routing_config,
  56. )
  57. from lightrag.parser.external.mineru.cache import MinerUParserOptions
  58. from lightrag.api.routers.query_routes import create_query_routes
  59. from lightrag.api.routers.graph_routes import create_graph_routes
  60. from lightrag.api.routers.ollama_api import OllamaAPI
  61. from lightrag.utils import logger, set_verbose_debug
  62. from lightrag.kg.shared_storage import (
  63. get_namespace_data,
  64. get_default_workspace,
  65. # set_default_workspace,
  66. cleanup_keyed_lock,
  67. finalize_share_data,
  68. )
  69. from fastapi.security import OAuth2PasswordRequestForm
  70. from lightrag.api.auth import auth_handler
  71. # use the .env that is inside the current folder
  72. # allows to use different .env file for each lightrag instance
  73. # the OS environment variables take precedence over the .env file
  74. load_dotenv(dotenv_path=".env", override=False)
  75. webui_title = os.getenv("WEBUI_TITLE")
  76. webui_description = os.getenv("WEBUI_DESCRIPTION")
  77. # Global authentication configuration
  78. auth_configured = bool(auth_handler.accounts)
  79. def _inject_swagger_theme(html: str, theme: str) -> str:
  80. if theme not in {"dark", "light"}:
  81. theme = "auto"
  82. # The script resolves dark / light / (auto + prefers-color-scheme) into a
  83. # single boolean attribute `data-lightrag-docs-dark` on <html>. CSS below
  84. # only matches when that attribute is present, so light/auto-light paths
  85. # leave Swagger UI's default palette untouched.
  86. theme_snippet = textwrap.dedent(
  87. f"""
  88. <script>
  89. (function () {{
  90. var ALLOWED = {{ dark: 1, light: 1, auto: 1 }};
  91. var currentTheme = {json.dumps(theme)};
  92. var mql = window.matchMedia('(prefers-color-scheme: dark)');
  93. function resolveDark(value) {{
  94. if (value === 'dark') return true;
  95. if (value === 'auto') return mql.matches;
  96. return false;
  97. }}
  98. function apply(value) {{
  99. currentTheme = ALLOWED[value] ? value : 'auto';
  100. var root = document.documentElement;
  101. if (resolveDark(currentTheme)) {{
  102. root.setAttribute('data-lightrag-docs-dark', '1');
  103. }} else {{
  104. root.removeAttribute('data-lightrag-docs-dark');
  105. }}
  106. }}
  107. apply(currentTheme);
  108. // Re-resolve when the OS theme flips while `theme=auto` is active.
  109. var onMqlChange = function () {{ apply(currentTheme); }};
  110. if (mql.addEventListener) mql.addEventListener('change', onMqlChange);
  111. else if (mql.addListener) mql.addListener(onMqlChange);
  112. window.addEventListener('message', function (event) {{
  113. var data = event.data;
  114. if (!data || data.type !== 'lightrag:set-docs-theme') return;
  115. apply(data.theme);
  116. }});
  117. }})();
  118. </script>
  119. <style>
  120. html[data-lightrag-docs-dark="1"] {{
  121. color-scheme: dark;
  122. }}
  123. html[data-lightrag-docs-dark="1"] body,
  124. html[data-lightrag-docs-dark="1"] .swagger-ui,
  125. html[data-lightrag-docs-dark="1"] .swagger-ui .scheme-container,
  126. html[data-lightrag-docs-dark="1"] .swagger-ui section.models,
  127. html[data-lightrag-docs-dark="1"] .swagger-ui .model-box,
  128. html[data-lightrag-docs-dark="1"] .swagger-ui .opblock,
  129. html[data-lightrag-docs-dark="1"] .swagger-ui .dialog-ux .modal-ux,
  130. html[data-lightrag-docs-dark="1"] .swagger-ui .auth-container {{
  131. background: #0f172a;
  132. color: #e5e7eb;
  133. }}
  134. html[data-lightrag-docs-dark="1"] .swagger-ui .info .title,
  135. html[data-lightrag-docs-dark="1"] .swagger-ui .opblock-tag,
  136. html[data-lightrag-docs-dark="1"] .swagger-ui .opblock .opblock-summary-description,
  137. html[data-lightrag-docs-dark="1"] .swagger-ui .model-title,
  138. html[data-lightrag-docs-dark="1"] .swagger-ui .parameter__name,
  139. html[data-lightrag-docs-dark="1"] .swagger-ui .parameter__type,
  140. html[data-lightrag-docs-dark="1"] .swagger-ui .response-col_status,
  141. html[data-lightrag-docs-dark="1"] .swagger-ui .response-col_description,
  142. html[data-lightrag-docs-dark="1"] .swagger-ui .auth-container h4,
  143. html[data-lightrag-docs-dark="1"] .swagger-ui .auth-container label,
  144. html[data-lightrag-docs-dark="1"] .swagger-ui .auth-container p,
  145. html[data-lightrag-docs-dark="1"] .swagger-ui .markdown p,
  146. html[data-lightrag-docs-dark="1"] .swagger-ui .markdown li,
  147. html[data-lightrag-docs-dark="1"] .swagger-ui .renderedMarkdown p,
  148. html[data-lightrag-docs-dark="1"] .swagger-ui .renderedMarkdown li,
  149. html[data-lightrag-docs-dark="1"] .swagger-ui table thead tr th,
  150. html[data-lightrag-docs-dark="1"] .swagger-ui table tbody tr td,
  151. html[data-lightrag-docs-dark="1"] .swagger-ui .tab li,
  152. html[data-lightrag-docs-dark="1"] .swagger-ui .tab li button.tablinks {{
  153. color: #e5e7eb;
  154. }}
  155. html[data-lightrag-docs-dark="1"] .swagger-ui .opblock-description-wrapper p,
  156. html[data-lightrag-docs-dark="1"] .swagger-ui .opblock-external-docs-wrapper p,
  157. html[data-lightrag-docs-dark="1"] .swagger-ui .response-col_links {{
  158. color: #cbd5f5;
  159. }}
  160. html[data-lightrag-docs-dark="1"] .swagger-ui input,
  161. html[data-lightrag-docs-dark="1"] .swagger-ui textarea,
  162. html[data-lightrag-docs-dark="1"] .swagger-ui select {{
  163. background: #020617;
  164. border-color: #334155;
  165. color: #f8fafc;
  166. }}
  167. html[data-lightrag-docs-dark="1"] .swagger-ui .markdown code,
  168. html[data-lightrag-docs-dark="1"] .swagger-ui .renderedMarkdown code,
  169. html[data-lightrag-docs-dark="1"] .swagger-ui .highlight-code,
  170. html[data-lightrag-docs-dark="1"] .swagger-ui .highlight-code pre,
  171. html[data-lightrag-docs-dark="1"] .swagger-ui .microlight,
  172. html[data-lightrag-docs-dark="1"] .swagger-ui .body-param__example,
  173. html[data-lightrag-docs-dark="1"] .swagger-ui .example,
  174. html[data-lightrag-docs-dark="1"] .swagger-ui .model-example pre {{
  175. background: #020617;
  176. color: #e2e8f0;
  177. }}
  178. html[data-lightrag-docs-dark="1"] .swagger-ui table thead tr th,
  179. html[data-lightrag-docs-dark="1"] .swagger-ui table tbody tr td {{
  180. border-color: #334155;
  181. }}
  182. html[data-lightrag-docs-dark="1"] .swagger-ui .tab li.active button.tablinks,
  183. html[data-lightrag-docs-dark="1"] .swagger-ui .tab li.tabitem.active {{
  184. color: #f8fafc;
  185. border-bottom-color: #34d399;
  186. }}
  187. html[data-lightrag-docs-dark="1"] .swagger-ui .btn.authorize,
  188. html[data-lightrag-docs-dark="1"] .swagger-ui .auth-wrapper .authorize {{
  189. background: #064e3b;
  190. border-color: #34d399;
  191. color: #d1fae5;
  192. }}
  193. html[data-lightrag-docs-dark="1"] .swagger-ui .btn.authorize svg {{
  194. fill: #d1fae5;
  195. }}
  196. html[data-lightrag-docs-dark="1"] .swagger-ui .dialog-ux .modal-ux,
  197. html[data-lightrag-docs-dark="1"] .swagger-ui .scheme-container,
  198. html[data-lightrag-docs-dark="1"] .swagger-ui section.models,
  199. html[data-lightrag-docs-dark="1"] .swagger-ui .opblock {{
  200. border-color: #334155;
  201. box-shadow: none;
  202. }}
  203. /* Schemas panel: section.models contains its own grey-on-grey
  204. buttons (`Schemas` header, each model row, "Expand all") that
  205. ignore the top-level body color. Force the whole subtree to
  206. use surface backgrounds and bright text. */
  207. html[data-lightrag-docs-dark="1"] .swagger-ui section.models,
  208. html[data-lightrag-docs-dark="1"] .swagger-ui section.models.is-open,
  209. html[data-lightrag-docs-dark="1"] .swagger-ui section.models h4,
  210. html[data-lightrag-docs-dark="1"] .swagger-ui section.models .model-container,
  211. html[data-lightrag-docs-dark="1"] .swagger-ui section.models .models-control,
  212. html[data-lightrag-docs-dark="1"] .swagger-ui .model-box {{
  213. background: #111827;
  214. border-color: #334155;
  215. }}
  216. html[data-lightrag-docs-dark="1"] .swagger-ui section.models h4,
  217. html[data-lightrag-docs-dark="1"] .swagger-ui section.models h4 button,
  218. html[data-lightrag-docs-dark="1"] .swagger-ui section.models h4 a,
  219. html[data-lightrag-docs-dark="1"] .swagger-ui section.models h4 span,
  220. html[data-lightrag-docs-dark="1"] .swagger-ui section.models .models-control,
  221. html[data-lightrag-docs-dark="1"] .swagger-ui section.models .models-control button,
  222. html[data-lightrag-docs-dark="1"] .swagger-ui section.models .model-toggle,
  223. html[data-lightrag-docs-dark="1"] .swagger-ui .model,
  224. html[data-lightrag-docs-dark="1"] .swagger-ui .model .model-title__text,
  225. html[data-lightrag-docs-dark="1"] .swagger-ui .model .property,
  226. html[data-lightrag-docs-dark="1"] .swagger-ui .model .prop-name,
  227. html[data-lightrag-docs-dark="1"] .swagger-ui .model .prop-type,
  228. html[data-lightrag-docs-dark="1"] .swagger-ui .model .prop-format,
  229. html[data-lightrag-docs-dark="1"] .swagger-ui .expand-operation,
  230. html[data-lightrag-docs-dark="1"] .swagger-ui .expand-operation span {{
  231. color: #e5e7eb;
  232. }}
  233. html[data-lightrag-docs-dark="1"] .swagger-ui section.models h4 svg,
  234. html[data-lightrag-docs-dark="1"] .swagger-ui section.models .models-control svg,
  235. html[data-lightrag-docs-dark="1"] .swagger-ui .model-toggle::after,
  236. html[data-lightrag-docs-dark="1"] .swagger-ui .expand-operation svg,
  237. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-accordion__icon svg,
  238. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-accordion__icon svg path {{
  239. fill: #e5e7eb;
  240. }}
  241. /* The "Expand all" pill and per-row toggle buttons inherit a light
  242. grey background from Swagger; clear it so they don't punch a
  243. pale rectangle into the dark panel. */
  244. html[data-lightrag-docs-dark="1"] .swagger-ui .expand-operation,
  245. html[data-lightrag-docs-dark="1"] .swagger-ui section.models h4 button,
  246. html[data-lightrag-docs-dark="1"] .swagger-ui section.models .models-control button {{
  247. background: transparent;
  248. }}
  249. /* Swagger's new JSON Schema 2020-12 renderer hard-codes light-mode
  250. greys (#505050 / #3b4151 / #afaeae / #6b6b6b) on every title,
  251. keyword, attribute and json-viewer node — completely independent
  252. from the .model / .swagger-ui ancestors we already restyled.
  253. Override the whole renderer subtree so model/property names,
  254. types, and the per-row "Expand all" button stay readable. */
  255. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12,
  256. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12__title,
  257. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-property .json-schema-2020-12__title,
  258. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-expand-deep-button,
  259. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-accordion,
  260. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword__name,
  261. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword__name--primary,
  262. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword__value--primary,
  263. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12__attribute,
  264. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12__attribute--primary,
  265. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-json-viewer__name,
  266. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-json-viewer__name--primary,
  267. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-json-viewer__value--primary,
  268. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword--const .json-schema-2020-12-json-viewer__name,
  269. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword--const .json-schema-2020-12-json-viewer__value,
  270. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword--default .json-schema-2020-12-json-viewer__name,
  271. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword--default .json-schema-2020-12-json-viewer__value,
  272. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword--enum .json-schema-2020-12-json-viewer__name,
  273. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword--enum .json-schema-2020-12-json-viewer__value,
  274. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword--examples .json-schema-2020-12-json-viewer__name,
  275. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword--examples .json-schema-2020-12-json-viewer__value {{
  276. color: #e5e7eb;
  277. }}
  278. /* Secondary / extension / description text — keep them visible but
  279. dimmer than primary titles. */
  280. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword__name--secondary,
  281. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword__value,
  282. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword__value--secondary,
  283. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword__name--extension,
  284. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword__value--extension,
  285. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword--description,
  286. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-json-viewer__name--secondary,
  287. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-json-viewer__value,
  288. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-json-viewer__value--secondary,
  289. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-json-viewer__name--extension,
  290. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-json-viewer__value--extension,
  291. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-json-viewer-extension-keyword .json-schema-2020-12-json-viewer__name,
  292. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-json-viewer-extension-keyword .json-schema-2020-12-json-viewer__value,
  293. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12__attribute--muted {{
  294. color: #cbd5f5;
  295. }}
  296. /* The deep-expand button inside each schemas row has its own
  297. background and shouldn't paint a pale capsule on dark surface. */
  298. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-expand-deep-button,
  299. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-accordion {{
  300. background-color: transparent;
  301. }}
  302. /* Restore Swagger's red warning palette. The broad keyword__value /
  303. __attribute / json-viewer__value overrides above otherwise win
  304. the cascade over `.json-schema-2020-12-*--warning` (higher
  305. specificity), flattening deprecated/schema-warning markers into
  306. plain text. Re-declared *after* the generic rules so equal-
  307. specificity selectors lose to these explicit ones. */
  308. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-keyword__value--warning,
  309. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12-json-viewer__value--warning,
  310. html[data-lightrag-docs-dark="1"] .swagger-ui .json-schema-2020-12__attribute--warning {{
  311. color: #fca5a5;
  312. border-color: #fca5a5;
  313. }}
  314. /* `.model-toggle::after` paints its caret with a `background:url(
  315. data:image/svg+xml,…<path d=…/>)` embedded SVG whose path has no
  316. fill attribute and no currentColor reference — `fill` rules can't
  317. touch it. Invert the rendered pixels so the black arrow flips to
  318. white on the dark schema surface. The glyph is single-color, so
  319. invert has no perceptible side effect. */
  320. html[data-lightrag-docs-dark="1"] .swagger-ui .model-toggle::after {{
  321. filter: invert(1);
  322. }}
  323. /* Per-operation Authorize lock icon. Swagger renders it via
  324. `<symbol id="locked|unlocked">` whose <path> has no fill attr
  325. and no currentColor reference; Swagger's CSS also never sets
  326. fill on .authorization__btn svg, leaving the path at the SVG
  327. default (black). Set fill on the outer <svg> — fill is inherited
  328. through <use> into the referenced symbol because the path itself
  329. is unstyled, so one declaration colors both locked and unlocked
  330. states. */
  331. html[data-lightrag-docs-dark="1"] .swagger-ui .authorization__btn svg,
  332. html[data-lightrag-docs-dark="1"] .swagger-ui .authorization__btn .locked,
  333. html[data-lightrag-docs-dark="1"] .swagger-ui .authorization__btn .unlocked {{
  334. fill: #e5e7eb;
  335. }}
  336. </style>
  337. """
  338. ).strip()
  339. needle = "</head>"
  340. if needle not in html:
  341. logger.warning(
  342. "Swagger UI HTML missing </head> tag; theme patch was skipped. "
  343. "FastAPI's swagger template may have changed."
  344. )
  345. return html
  346. return html.replace(needle, f"{theme_snippet}\n{needle}", 1)
  347. # Fixed WebUI mount path. Used as `app.mount(WEBUI_PATH, ...)` and as the
  348. # in-app component of `webuiPrefix` injected into window.__LIGHTRAG_CONFIG__
  349. # (which the browser sees as `LIGHTRAG_API_PREFIX + WEBUI_PATH + "/"`).
  350. # Not user-configurable: a single mount path simplifies the operator surface
  351. # and matches how LightRAG is deployed in practice. See
  352. # docs/MultiSiteDeployment.md.
  353. WEBUI_PATH = "/webui"
  354. def _normalize_api_prefix(value: str | None) -> str:
  355. """Canonicalize an API prefix before handing it to FastAPI's ``root_path``.
  356. Strips surrounding whitespace, ensures a leading slash, drops a trailing
  357. slash, and treats empty/"/" as "no prefix". Raw CLI/env input like
  358. ``"site01"`` or ``"/site01/"`` would otherwise feed an invalid form to
  359. FastAPI and to the WebUI prefix injection.
  360. """
  361. if value is None:
  362. return ""
  363. value = value.strip()
  364. if not value or value == "/":
  365. return ""
  366. if not value.startswith("/"):
  367. value = "/" + value
  368. return value.rstrip("/")
  369. class _RootPathNormalizationMiddleware:
  370. """Make Mount sub-apps work when the reverse proxy strips the API prefix.
  371. When ``LIGHTRAG_API_PREFIX=/site01`` and nginx strips ``/site01`` before
  372. forwarding, the backend sees ``scope["path"]="/webui/"`` while FastAPI's
  373. ``__call__`` sets ``scope["root_path"]="/site01"``. Starlette's outer
  374. Mount.matches still hits via ``get_route_path`` 's fallback branch (path
  375. not starting with root_path is returned unchanged), but it mutates the
  376. child scope to ``root_path="/site01/webui"`` without touching
  377. ``scope["path"]``. The inner ``StaticFiles.get_path`` then sees a
  378. non-overlapping pair and falls through to a literal ``webui`` filename
  379. lookup → 404 on the actual file system.
  380. Prepending ``root_path`` to a non-prefixed ``scope["path"]`` restores the
  381. canonical ASGI form (path always contains root_path), matching what a
  382. verbatim-forwarding proxy produces natively. Plain Routes are unaffected
  383. because their handlers do not redo nested ``get_route_path`` resolution.
  384. See docs/MultiSiteDeployment.md for the deployment modes this enables.
  385. """
  386. def __init__(self, app):
  387. self.app = app
  388. async def __call__(self, scope, receive, send):
  389. if scope.get("type") in ("http", "websocket"):
  390. root_path = scope.get("root_path", "")
  391. path = scope.get("path", "")
  392. if root_path and not path.startswith(root_path):
  393. scope = {**scope, "path": root_path + path}
  394. raw_path = scope.get("raw_path")
  395. if isinstance(raw_path, (bytes, bytearray)):
  396. scope["raw_path"] = root_path.encode("ascii") + bytes(raw_path)
  397. await self.app(scope, receive, send)
  398. def _clean_workspace_value(value: Any) -> str | None:
  399. if value is None:
  400. return None
  401. text = str(value).strip()
  402. return text or None
  403. def _get_storage_workspace(storage: Any) -> str | None:
  404. if storage is None:
  405. return None
  406. effective_workspace = _clean_workspace_value(
  407. getattr(storage, "effective_workspace", None)
  408. )
  409. if effective_workspace:
  410. return effective_workspace
  411. final_namespace = _clean_workspace_value(getattr(storage, "final_namespace", None))
  412. namespace = _clean_workspace_value(getattr(storage, "namespace", None))
  413. if final_namespace and namespace:
  414. suffix = f"_{namespace}"
  415. if final_namespace.endswith(suffix):
  416. workspace = final_namespace[: -len(suffix)]
  417. if workspace:
  418. return workspace
  419. return _clean_workspace_value(getattr(storage, "workspace", None))
  420. def _get_storage_workspaces(rag: Any) -> dict[str, str | None]:
  421. return {
  422. "kv_storage": _get_storage_workspace(getattr(rag, "full_docs", None)),
  423. "doc_status_storage": _get_storage_workspace(getattr(rag, "doc_status", None)),
  424. "graph_storage": _get_storage_workspace(
  425. getattr(rag, "chunk_entity_relation_graph", None)
  426. ),
  427. "vector_storage": _get_storage_workspace(getattr(rag, "entities_vdb", None)),
  428. }
  429. def _build_mineru_status() -> dict[str, Any]:
  430. """Snapshot MinerU-related env vars for the /health endpoint.
  431. Reads env directly (no MinerURawClient instantiation — that has
  432. side effects like token validation). Reuses MinerUParserOptions to
  433. share defaulting logic with the actual parser path.
  434. """
  435. api_mode_raw = os.getenv("MINERU_API_MODE", "").strip().lower()
  436. api_mode: str | None = api_mode_raw or None
  437. endpoint = ""
  438. if api_mode == "official":
  439. endpoint = os.getenv("MINERU_OFFICIAL_ENDPOINT", "").strip()
  440. elif api_mode == "local":
  441. endpoint = os.getenv("MINERU_LOCAL_ENDPOINT", "").strip()
  442. options: dict[str, Any] = {}
  443. if api_mode in ("official", "local"):
  444. try:
  445. opts = MinerUParserOptions.from_env(api_mode=api_mode)
  446. except Exception:
  447. opts = None
  448. if opts is not None:
  449. options = {
  450. "language": opts.language,
  451. "enable_table": opts.enable_table,
  452. "enable_formula": opts.enable_formula,
  453. }
  454. if opts.api_mode == "official":
  455. options["model_version"] = opts.model_version
  456. options["is_ocr"] = opts.is_ocr
  457. else:
  458. options["local_backend"] = opts.local_backend
  459. options["local_parse_method"] = opts.local_parse_method
  460. options["local_image_analysis"] = opts.local_image_analysis
  461. return {"endpoint": endpoint, "api_mode": api_mode, "options": options}
  462. def _build_docling_status() -> dict[str, Any]:
  463. """Snapshot Docling-related env vars for the /health endpoint."""
  464. endpoint = os.getenv("DOCLING_ENDPOINT", "").strip()
  465. if not endpoint:
  466. return {"endpoint": "", "options": {}}
  467. return {
  468. "endpoint": endpoint,
  469. "options": {
  470. "do_ocr": get_env_value("DOCLING_DO_OCR", True, bool),
  471. "force_ocr": get_env_value("DOCLING_FORCE_OCR", True, bool),
  472. "ocr_engine": os.getenv("DOCLING_OCR_ENGINE", "auto").strip() or "auto",
  473. "ocr_lang": os.getenv("DOCLING_OCR_LANG", "").strip(),
  474. "do_formula_enrichment": get_env_value(
  475. "DOCLING_DO_FORMULA_ENRICHMENT", False, bool
  476. ),
  477. },
  478. }
  479. class LLMConfigCache:
  480. """Smart LLM and Embedding configuration cache class"""
  481. def __init__(self, args):
  482. self.args = args
  483. # Initialize configurations based on binding conditions
  484. self.openai_llm_options = None
  485. self.gemini_llm_options = None
  486. self.gemini_embedding_options = None
  487. self.ollama_llm_options = None
  488. self.ollama_embedding_options = None
  489. self.bedrock_llm_options = None
  490. # Only initialize and log OpenAI options when using OpenAI-related bindings
  491. if args.llm_binding in ["openai", "azure_openai"]:
  492. from lightrag.llm.binding_options import OpenAILLMOptions
  493. self.openai_llm_options = OpenAILLMOptions.options_dict(args)
  494. logger.info(f"OpenAI LLM Options: {self.openai_llm_options}")
  495. if args.llm_binding == "gemini":
  496. from lightrag.llm.binding_options import GeminiLLMOptions
  497. self.gemini_llm_options = GeminiLLMOptions.options_dict(args)
  498. logger.info(f"Gemini LLM Options: {self.gemini_llm_options}")
  499. if args.llm_binding == "bedrock":
  500. from lightrag.llm.binding_options import BedrockLLMOptions
  501. self.bedrock_llm_options = BedrockLLMOptions.options_dict(args)
  502. logger.info(f"Bedrock LLM Options: {self.bedrock_llm_options}")
  503. # Only initialize and log Ollama LLM options when using Ollama LLM binding
  504. if args.llm_binding == "ollama":
  505. try:
  506. from lightrag.llm.binding_options import OllamaLLMOptions
  507. self.ollama_llm_options = OllamaLLMOptions.options_dict(args)
  508. logger.info(f"Ollama LLM Options: {self.ollama_llm_options}")
  509. except ImportError:
  510. logger.warning(
  511. "OllamaLLMOptions not available, using default configuration"
  512. )
  513. self.ollama_llm_options = {}
  514. # Only initialize and log Ollama Embedding options when using Ollama Embedding binding
  515. if args.embedding_binding == "ollama":
  516. try:
  517. from lightrag.llm.binding_options import OllamaEmbeddingOptions
  518. self.ollama_embedding_options = OllamaEmbeddingOptions.options_dict(
  519. args
  520. )
  521. logger.info(
  522. f"Ollama Embedding Options: {self.ollama_embedding_options}"
  523. )
  524. except ImportError:
  525. logger.warning(
  526. "OllamaEmbeddingOptions not available, using default configuration"
  527. )
  528. self.ollama_embedding_options = {}
  529. # Only initialize and log Gemini Embedding options when using Gemini Embedding binding
  530. if args.embedding_binding == "gemini":
  531. try:
  532. from lightrag.llm.binding_options import GeminiEmbeddingOptions
  533. self.gemini_embedding_options = GeminiEmbeddingOptions.options_dict(
  534. args
  535. )
  536. logger.info(
  537. f"Gemini Embedding Options: {self.gemini_embedding_options}"
  538. )
  539. except ImportError:
  540. logger.warning(
  541. "GeminiEmbeddingOptions not available, using default configuration"
  542. )
  543. self.gemini_embedding_options = {}
  544. _PROVIDER_LOG_LABELS = {
  545. "azure_openai": "Azure OpenAI",
  546. "bedrock": "Bedrock",
  547. "gemini": "Gemini",
  548. "lollms": "Lollms",
  549. "ollama": "Ollama",
  550. "openai": "OpenAI",
  551. }
  552. def _provider_log_label(binding: Any) -> str:
  553. binding_name = str(binding)
  554. return _PROVIDER_LOG_LABELS.get(
  555. binding_name, binding_name.replace("_", " ").title()
  556. )
  557. def _log_role_provider_options(rag: Any) -> None:
  558. """Log sanitized provider options for every role LLM."""
  559. try:
  560. role_configs = rag.get_llm_role_config()
  561. except Exception as e:
  562. logger.warning(f"Failed to read role LLM configuration for logging: {e}")
  563. return
  564. logger.info("Role LLM Option:")
  565. for spec in ROLES:
  566. role_config = role_configs.get(spec.name)
  567. if not isinstance(role_config, dict):
  568. continue
  569. metadata = role_config.get("metadata") or {}
  570. binding = role_config.get("binding") or metadata.get("binding")
  571. if not binding:
  572. continue
  573. provider_options = metadata.get("provider_options") or {}
  574. logger.info(
  575. " - %s: %s %s",
  576. spec.name,
  577. _provider_log_label(binding),
  578. provider_options,
  579. )
  580. def check_frontend_build():
  581. """Check if frontend is built and optionally check if source is up-to-date
  582. Returns:
  583. tuple: (assets_exist: bool, is_outdated: bool)
  584. - assets_exist: True if WebUI build files exist
  585. - is_outdated: True if source is newer than build (only in dev environment)
  586. """
  587. webui_dir = Path(__file__).parent / "webui"
  588. index_html = webui_dir / "index.html"
  589. # 1. Check if build files exist
  590. if not index_html.exists():
  591. ASCIIColors.yellow("\n" + "=" * 80)
  592. ASCIIColors.yellow("WARNING: Frontend Not Built")
  593. ASCIIColors.yellow("=" * 80)
  594. ASCIIColors.yellow("The WebUI frontend has not been built yet.")
  595. ASCIIColors.yellow("The API server will start without the WebUI interface.")
  596. ASCIIColors.yellow(
  597. "\nTo enable WebUI, build the frontend using these commands:\n"
  598. )
  599. ASCIIColors.cyan(" cd lightrag_webui")
  600. ASCIIColors.cyan(" bun install --frozen-lockfile")
  601. ASCIIColors.cyan(" bun run build")
  602. ASCIIColors.cyan(" cd ..")
  603. ASCIIColors.yellow("\nThen restart the service.\n")
  604. ASCIIColors.cyan(
  605. "Note: Make sure you have Bun installed. Visit https://bun.sh for installation."
  606. )
  607. ASCIIColors.yellow("=" * 80 + "\n")
  608. return (False, False) # Assets don't exist, not outdated
  609. # 2. Check if this is a development environment (source directory exists)
  610. try:
  611. source_dir = Path(__file__).parent.parent.parent / "lightrag_webui"
  612. src_dir = source_dir / "src"
  613. # Determine if this is a development environment: source directory exists and contains src directory
  614. if not source_dir.exists() or not src_dir.exists():
  615. # Production environment, skip source code check
  616. logger.debug(
  617. "Production environment detected, skipping source freshness check"
  618. )
  619. return (True, False) # Assets exist, not outdated (prod environment)
  620. # Development environment, perform source code timestamp check
  621. logger.debug("Development environment detected, checking source freshness")
  622. # Source code file extensions (files to check)
  623. source_extensions = {
  624. ".ts",
  625. ".tsx",
  626. ".js",
  627. ".jsx",
  628. ".mjs",
  629. ".cjs", # TypeScript/JavaScript
  630. ".css",
  631. ".scss",
  632. ".sass",
  633. ".less", # Style files
  634. ".json",
  635. ".jsonc", # Configuration/data files
  636. ".html",
  637. ".htm", # Template files
  638. ".md",
  639. ".mdx", # Markdown
  640. }
  641. # Key configuration files (in lightrag_webui root directory)
  642. key_files = [
  643. source_dir / "package.json",
  644. source_dir / "bun.lock",
  645. source_dir / "vite.config.ts",
  646. source_dir / "tsconfig.json",
  647. source_dir / "tailraid.config.js",
  648. source_dir / "index.html",
  649. ]
  650. # Get the latest modification time of source code
  651. latest_source_time = 0
  652. # Check source code files in src directory
  653. for file_path in src_dir.rglob("*"):
  654. if file_path.is_file():
  655. # Only check source code files, ignore temporary files and logs
  656. if file_path.suffix.lower() in source_extensions:
  657. mtime = file_path.stat().st_mtime
  658. latest_source_time = max(latest_source_time, mtime)
  659. # Check key configuration files
  660. for key_file in key_files:
  661. if key_file.exists():
  662. mtime = key_file.stat().st_mtime
  663. latest_source_time = max(latest_source_time, mtime)
  664. # Get build time
  665. build_time = index_html.stat().st_mtime
  666. # Compare timestamps (5 second tolerance to avoid file system time precision issues)
  667. if latest_source_time > build_time + 5:
  668. ASCIIColors.yellow("\n" + "=" * 80)
  669. ASCIIColors.yellow("WARNING: Frontend Source Code Has Been Updated")
  670. ASCIIColors.yellow("=" * 80)
  671. ASCIIColors.yellow(
  672. "The frontend source code is newer than the current build."
  673. )
  674. ASCIIColors.yellow(
  675. "This might happen after 'git pull' or manual code changes.\n"
  676. )
  677. ASCIIColors.cyan(
  678. "Recommended: Rebuild the frontend to use the latest changes:"
  679. )
  680. ASCIIColors.cyan(" cd lightrag_webui")
  681. ASCIIColors.cyan(" bun install --frozen-lockfile")
  682. ASCIIColors.cyan(" bun run build")
  683. ASCIIColors.cyan(" cd ..")
  684. ASCIIColors.yellow("\nThe server will continue with the current build.")
  685. ASCIIColors.yellow("=" * 80 + "\n")
  686. return (True, True) # Assets exist, outdated
  687. else:
  688. logger.info("Frontend build is up-to-date")
  689. return (True, False) # Assets exist, up-to-date
  690. except Exception as e:
  691. # If check fails, log warning but don't affect startup
  692. logger.warning(f"Failed to check frontend source freshness: {e}")
  693. return (True, False) # Assume assets exist and up-to-date on error
  694. def create_app(args):
  695. # Check frontend build first and get status
  696. webui_assets_exist, is_frontend_outdated = check_frontend_build()
  697. # Create unified API version display with warning symbol if frontend is outdated
  698. api_version_display = (
  699. f"{__api_version__}⚠️" if is_frontend_outdated else __api_version__
  700. )
  701. # Setup logging
  702. logger.setLevel(args.log_level)
  703. set_verbose_debug(args.verbose)
  704. validate_parser_routing_config()
  705. # Create configuration cache (this will output configuration logs)
  706. config_cache = LLMConfigCache(args)
  707. # Verify that bindings are correctly setup
  708. if args.llm_binding not in [
  709. "lollms",
  710. "ollama",
  711. "openai",
  712. "azure_openai",
  713. "bedrock",
  714. "gemini",
  715. ]:
  716. raise Exception("llm binding not supported")
  717. if args.embedding_binding not in [
  718. "lollms",
  719. "ollama",
  720. "openai",
  721. "azure_openai",
  722. "bedrock",
  723. "jina",
  724. "gemini",
  725. "voyageai",
  726. ]:
  727. raise Exception(f"embedding binding '{args.embedding_binding}' not supported")
  728. # Set default hosts if not provided
  729. if args.llm_binding_host is None:
  730. args.llm_binding_host = get_default_host(args.llm_binding)
  731. if args.embedding_binding_host is None:
  732. args.embedding_binding_host = get_default_host(args.embedding_binding)
  733. # Add SSL validation
  734. if args.ssl:
  735. if not args.ssl_certfile or not args.ssl_keyfile:
  736. raise Exception(
  737. "SSL certificate and key files must be provided when SSL is enabled"
  738. )
  739. if not os.path.exists(args.ssl_certfile):
  740. raise Exception(f"SSL certificate file not found: {args.ssl_certfile}")
  741. if not os.path.exists(args.ssl_keyfile):
  742. raise Exception(f"SSL key file not found: {args.ssl_keyfile}")
  743. # Check if API key is provided either through env var or args
  744. api_key = os.getenv("LIGHTRAG_API_KEY") or args.key
  745. # Initialize document manager with workspace support for data isolation
  746. doc_manager = DocumentManager(args.input_dir, workspace=args.workspace)
  747. @asynccontextmanager
  748. async def lifespan(app: FastAPI):
  749. """Lifespan context manager for startup and shutdown events"""
  750. # Store background tasks
  751. app.state.background_tasks = set()
  752. try:
  753. # Initialize database connections
  754. # Note: initialize_storages() now auto-initializes pipeline_status for rag.workspace
  755. await rag.initialize_storages()
  756. # Data migration regardless of storage implementation
  757. await rag.check_and_migrate_data()
  758. ASCIIColors.green("\nServer is ready to accept connections! 🚀\n")
  759. yield
  760. finally:
  761. # Clean up database connections
  762. await rag.finalize_storages()
  763. if "LIGHTRAG_GUNICORN_MODE" not in os.environ:
  764. # Only perform cleanup in Uvicorn single-process mode
  765. logger.debug("Unvicorn Mode: finalizing shared storage...")
  766. finalize_share_data()
  767. else:
  768. # In Gunicorn mode with preload_app=True, cleanup is handled by on_exit hooks
  769. logger.debug(
  770. "Gunicorn Mode: postpone shared storage finalization to master process"
  771. )
  772. base_description = (
  773. "Providing API for LightRAG core, Web UI and Ollama Model Emulation"
  774. )
  775. swagger_description = (
  776. base_description
  777. + (" (API-Key Enabled)" if api_key else "")
  778. + "\n\n[View ReDoc documentation](/redoc)"
  779. )
  780. # The WebUI mount path is fixed at "/webui" — see
  781. # docs/MultiSiteDeployment.md for the rationale.
  782. api_prefix = _normalize_api_prefix(getattr(args, "api_prefix", None))
  783. webui_path = WEBUI_PATH
  784. app_kwargs = {
  785. "title": "LightRAG Server API",
  786. "description": swagger_description,
  787. "version": __api_version__,
  788. "openapi_url": "/openapi.json",
  789. "docs_url": None, # custom endpoint for offline Swagger support
  790. "redoc_url": "/redoc",
  791. "root_path": api_prefix if api_prefix else None,
  792. "lifespan": lifespan,
  793. }
  794. # Configure Swagger UI parameters
  795. # Enable persistAuthorization and tryItOutEnabled for better user experience
  796. app_kwargs["swagger_ui_parameters"] = {
  797. "persistAuthorization": True,
  798. "tryItOutEnabled": True,
  799. }
  800. app = FastAPI(**app_kwargs)
  801. # Add custom validation error handler for /query/data endpoint
  802. @app.exception_handler(RequestValidationError)
  803. async def validation_exception_handler(
  804. request: Request, exc: RequestValidationError
  805. ):
  806. # Check if this is a request to /query/data endpoint
  807. if request.url.path.endswith("/query/data"):
  808. # Extract error details
  809. error_details = []
  810. for error in exc.errors():
  811. field_path = " -> ".join(str(loc) for loc in error["loc"])
  812. error_details.append(f"{field_path}: {error['msg']}")
  813. error_message = "; ".join(error_details)
  814. # Return in the expected format for /query/data
  815. return JSONResponse(
  816. status_code=400,
  817. content={
  818. "status": "failure",
  819. "message": f"Validation error: {error_message}",
  820. "data": {},
  821. "metadata": {},
  822. },
  823. )
  824. else:
  825. # For other endpoints, return the default FastAPI validation error
  826. return JSONResponse(status_code=422, content={"detail": exc.errors()})
  827. def get_cors_origins():
  828. """Get allowed origins from global_args
  829. Returns a list of allowed origins, defaults to ["*"] if not set
  830. """
  831. origins_str = global_args.cors_origins
  832. if origins_str == "*":
  833. return ["*"]
  834. return [origin.strip() for origin in origins_str.split(",")]
  835. # Normalize scope["path"] for proxy-strip deployments so the WebUI
  836. # Mount (and any other Mount) routes correctly. Added before CORS so it
  837. # runs first in the middleware stack — see _RootPathNormalizationMiddleware
  838. # docstring.
  839. if api_prefix:
  840. app.add_middleware(_RootPathNormalizationMiddleware)
  841. # Add CORS middleware
  842. app.add_middleware(
  843. CORSMiddleware,
  844. allow_origins=get_cors_origins(),
  845. allow_credentials=True,
  846. allow_methods=["*"],
  847. allow_headers=["*"],
  848. expose_headers=[
  849. "X-New-Token"
  850. ], # Expose token renewal header for cross-origin requests
  851. )
  852. # Create combined auth dependency for all endpoints
  853. combined_auth = get_combined_auth_dependency(api_key)
  854. def get_workspace_from_request(request: Request) -> str | None:
  855. """
  856. Extract workspace from HTTP request header or use default.
  857. This enables multi-workspace API support by checking the custom
  858. 'LIGHTRAG-WORKSPACE' header. If not present, falls back to the
  859. server's default workspace configuration.
  860. Args:
  861. request: FastAPI Request object
  862. Returns:
  863. Workspace identifier (may be empty string for global namespace)
  864. """
  865. # Check custom header first
  866. workspace = request.headers.get("LIGHTRAG-WORKSPACE", "").strip()
  867. if not workspace:
  868. workspace = None
  869. else:
  870. sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", workspace)
  871. if sanitized != workspace:
  872. logger.warning(
  873. f"Workspace header '{workspace}' contains invalid characters. "
  874. f"Sanitized to '{sanitized}'."
  875. )
  876. workspace = sanitized
  877. return workspace
  878. # Create working directory if it doesn't exist
  879. Path(args.working_dir).mkdir(parents=True, exist_ok=True)
  880. def create_optimized_openai_llm_func(
  881. config_cache: LLMConfigCache, args, llm_timeout: int
  882. ):
  883. """Create optimized OpenAI LLM function with pre-processed configuration"""
  884. async def optimized_openai_alike_model_complete(
  885. prompt,
  886. system_prompt=None,
  887. history_messages=None,
  888. **kwargs,
  889. ) -> str:
  890. from lightrag.llm.openai import openai_complete_if_cache
  891. if history_messages is None:
  892. history_messages = []
  893. # Use pre-processed configuration to avoid repeated parsing.
  894. # response_format and legacy keyword_extraction/entity_extraction
  895. # flags flow through **kwargs; openai_complete_if_cache handles
  896. # the deprecation shim for the legacy booleans.
  897. kwargs["timeout"] = llm_timeout
  898. if config_cache.openai_llm_options:
  899. kwargs.update(config_cache.openai_llm_options)
  900. return await openai_complete_if_cache(
  901. args.llm_model,
  902. prompt,
  903. system_prompt=system_prompt,
  904. history_messages=history_messages,
  905. base_url=args.llm_binding_host,
  906. api_key=args.llm_binding_api_key,
  907. **kwargs,
  908. )
  909. return optimized_openai_alike_model_complete
  910. def create_optimized_azure_openai_llm_func(
  911. config_cache: LLMConfigCache, args, llm_timeout: int
  912. ):
  913. """Create optimized Azure OpenAI LLM function with pre-processed configuration"""
  914. async def optimized_azure_openai_model_complete(
  915. prompt,
  916. system_prompt=None,
  917. history_messages=None,
  918. **kwargs,
  919. ) -> str:
  920. from lightrag.llm.azure_openai import azure_openai_complete_if_cache
  921. if history_messages is None:
  922. history_messages = []
  923. # response_format and legacy extraction booleans flow through kwargs
  924. # to azure_openai_complete_if_cache, which handles deprecation shims.
  925. kwargs["timeout"] = llm_timeout
  926. if config_cache.openai_llm_options:
  927. kwargs.update(config_cache.openai_llm_options)
  928. return await azure_openai_complete_if_cache(
  929. args.llm_model,
  930. prompt,
  931. system_prompt=system_prompt,
  932. history_messages=history_messages,
  933. base_url=args.llm_binding_host,
  934. api_key=os.getenv("AZURE_OPENAI_API_KEY", args.llm_binding_api_key),
  935. api_version=os.getenv("AZURE_OPENAI_API_VERSION", "2024-08-01-preview"),
  936. **kwargs,
  937. )
  938. return optimized_azure_openai_model_complete
  939. def create_optimized_gemini_llm_func(
  940. config_cache: LLMConfigCache, args, llm_timeout: int
  941. ):
  942. """Create optimized Gemini LLM function with cached configuration"""
  943. async def optimized_gemini_model_complete(
  944. prompt,
  945. system_prompt=None,
  946. history_messages=None,
  947. **kwargs,
  948. ) -> str:
  949. from lightrag.llm.gemini import gemini_complete_if_cache
  950. if history_messages is None:
  951. history_messages = []
  952. # response_format and legacy extraction booleans flow through kwargs
  953. # to gemini_complete_if_cache, which handles deprecation shims.
  954. kwargs["timeout"] = llm_timeout
  955. if (
  956. config_cache.gemini_llm_options is not None
  957. and "generation_config" not in kwargs
  958. ):
  959. kwargs["generation_config"] = dict(config_cache.gemini_llm_options)
  960. return await gemini_complete_if_cache(
  961. args.llm_model,
  962. prompt,
  963. system_prompt=system_prompt,
  964. history_messages=history_messages,
  965. api_key=args.llm_binding_api_key,
  966. base_url=args.llm_binding_host,
  967. **kwargs,
  968. )
  969. return optimized_gemini_model_complete
  970. def create_llm_model_func(binding: str):
  971. """
  972. Create LLM model function based on binding type.
  973. Uses optimized functions for OpenAI bindings and lazy import for others.
  974. """
  975. try:
  976. if binding == "lollms":
  977. from lightrag.llm.lollms import lollms_model_complete
  978. return lollms_model_complete
  979. elif binding == "ollama":
  980. from lightrag.llm.ollama import ollama_model_complete
  981. return ollama_model_complete
  982. elif binding == "bedrock":
  983. return bedrock_model_complete # Already defined locally
  984. elif binding == "azure_openai":
  985. # Use optimized function with pre-processed configuration
  986. return create_optimized_azure_openai_llm_func(
  987. config_cache, args, llm_timeout
  988. )
  989. elif binding == "gemini":
  990. return create_optimized_gemini_llm_func(config_cache, args, llm_timeout)
  991. else: # openai and compatible
  992. # Use optimized function with pre-processed configuration
  993. return create_optimized_openai_llm_func(config_cache, args, llm_timeout)
  994. except ImportError as e:
  995. raise Exception(f"Failed to import {binding} LLM binding: {e}")
  996. def create_llm_model_kwargs(binding: str, args, llm_timeout: int) -> dict:
  997. """
  998. Create LLM model kwargs based on binding type.
  999. Uses lazy import for binding-specific options.
  1000. """
  1001. if binding in ["lollms", "ollama"]:
  1002. try:
  1003. from lightrag.llm.binding_options import OllamaLLMOptions
  1004. return {
  1005. "host": args.llm_binding_host,
  1006. "timeout": llm_timeout,
  1007. "options": OllamaLLMOptions.options_dict(args),
  1008. "api_key": args.llm_binding_api_key,
  1009. }
  1010. except ImportError as e:
  1011. raise Exception(f"Failed to import {binding} options: {e}")
  1012. return {}
  1013. def resolve_role_llm_settings(
  1014. role: str, override_meta: dict | None = None
  1015. ) -> dict[str, Any]:
  1016. attr = role.lower()
  1017. override_meta = override_meta or {}
  1018. role_binding = (
  1019. override_meta.get("binding")
  1020. or getattr(args, f"{attr}_llm_binding", None)
  1021. or args.llm_binding
  1022. )
  1023. role_model = (
  1024. override_meta.get("model")
  1025. or getattr(args, f"{attr}_llm_model", None)
  1026. or args.llm_model
  1027. )
  1028. role_host = (
  1029. override_meta.get("host")
  1030. or getattr(args, f"{attr}_llm_binding_host", None)
  1031. or args.llm_binding_host
  1032. )
  1033. explicit_role_apikey = override_meta.get("api_key") or getattr(
  1034. args, f"{attr}_llm_binding_api_key", None
  1035. )
  1036. if role_binding == "bedrock":
  1037. if explicit_role_apikey:
  1038. raise ValueError(
  1039. f"Bedrock role '{role}' does not support role-specific "
  1040. "LLM_BINDING_API_KEY; use role-specific SigV4 AWS_* "
  1041. "variables or process-level AWS_BEARER_TOKEN_BEDROCK."
  1042. )
  1043. role_apikey = None
  1044. else:
  1045. role_apikey = explicit_role_apikey or args.llm_binding_api_key
  1046. role_timeout = (
  1047. override_meta.get("timeout")
  1048. or getattr(args, f"{attr}_llm_timeout", None)
  1049. or llm_timeout
  1050. )
  1051. role_max_async = override_meta.get("max_async")
  1052. if role_max_async is None:
  1053. role_max_async = getattr(args, f"{attr}_llm_max_async", None)
  1054. is_cross_provider = role_binding != args.llm_binding
  1055. role_provider_options = override_meta.get("provider_options")
  1056. if role_provider_options is None:
  1057. if role_binding in ["openai", "azure_openai"]:
  1058. from lightrag.llm.binding_options import OpenAILLMOptions
  1059. role_provider_options = OpenAILLMOptions.options_dict_for_role(
  1060. args, role, is_cross_provider
  1061. )
  1062. elif role_binding == "gemini":
  1063. from lightrag.llm.binding_options import GeminiLLMOptions
  1064. role_provider_options = GeminiLLMOptions.options_dict_for_role(
  1065. args, role, is_cross_provider
  1066. )
  1067. elif role_binding in ["lollms", "ollama"]:
  1068. from lightrag.llm.binding_options import OllamaLLMOptions
  1069. role_provider_options = OllamaLLMOptions.options_dict_for_role(
  1070. args, role, is_cross_provider
  1071. )
  1072. elif role_binding == "bedrock":
  1073. from lightrag.llm.binding_options import BedrockLLMOptions
  1074. role_provider_options = BedrockLLMOptions.options_dict_for_role(
  1075. args, role, is_cross_provider
  1076. )
  1077. else:
  1078. role_provider_options = {}
  1079. bedrock_aws_options = {}
  1080. if role_binding == "bedrock":
  1081. override_bedrock_aws_options = override_meta.get("bedrock_aws_options", {})
  1082. bedrock_aws_options = {
  1083. "aws_region": override_meta.get("aws_region")
  1084. or override_bedrock_aws_options.get("aws_region")
  1085. or getattr(args, f"{attr}_aws_region", None)
  1086. or getattr(args, "aws_region", None),
  1087. "aws_access_key_id": override_meta.get("aws_access_key_id")
  1088. or override_bedrock_aws_options.get("aws_access_key_id")
  1089. or getattr(args, f"{attr}_aws_access_key_id", None)
  1090. or getattr(args, "aws_access_key_id", None),
  1091. "aws_secret_access_key": override_meta.get("aws_secret_access_key")
  1092. or override_bedrock_aws_options.get("aws_secret_access_key")
  1093. or getattr(args, f"{attr}_aws_secret_access_key", None)
  1094. or getattr(args, "aws_secret_access_key", None),
  1095. "aws_session_token": override_meta.get("aws_session_token")
  1096. or override_bedrock_aws_options.get("aws_session_token")
  1097. or getattr(args, f"{attr}_aws_session_token", None)
  1098. or getattr(args, "aws_session_token", None),
  1099. }
  1100. return {
  1101. "binding": role_binding,
  1102. "model": role_model,
  1103. "host": role_host,
  1104. "api_key": role_apikey,
  1105. "timeout": role_timeout,
  1106. "max_async": role_max_async,
  1107. "provider_options": role_provider_options,
  1108. "is_cross_provider": is_cross_provider,
  1109. "bedrock_aws_options": bedrock_aws_options,
  1110. }
  1111. def create_role_llm_func(role: str, override_meta: dict | None = None):
  1112. """Create an independent raw LLM function for a role."""
  1113. settings = resolve_role_llm_settings(role, override_meta)
  1114. role_binding = settings["binding"]
  1115. role_model = settings["model"]
  1116. role_host = settings["host"]
  1117. role_apikey = settings["api_key"]
  1118. role_timeout = settings["timeout"]
  1119. role_provider_options = settings["provider_options"]
  1120. bedrock_aws_options = settings["bedrock_aws_options"]
  1121. try:
  1122. if role_binding == "ollama":
  1123. from lightrag.llm.ollama import _ollama_model_if_cache
  1124. async def role_ollama_complete(
  1125. prompt,
  1126. system_prompt=None,
  1127. history_messages=None,
  1128. enable_cot: bool = False,
  1129. **kwargs,
  1130. ):
  1131. # response_format and legacy extraction booleans flow
  1132. # through kwargs to _ollama_model_if_cache, which handles
  1133. # the deprecation shim and emits a single warning.
  1134. if history_messages is None:
  1135. history_messages = []
  1136. if role_provider_options:
  1137. kwargs.setdefault("options", dict(role_provider_options))
  1138. return await _ollama_model_if_cache(
  1139. role_model,
  1140. prompt,
  1141. system_prompt=system_prompt,
  1142. history_messages=history_messages,
  1143. enable_cot=enable_cot,
  1144. host=role_host,
  1145. timeout=role_timeout,
  1146. api_key=role_apikey,
  1147. **kwargs,
  1148. )
  1149. return role_ollama_complete
  1150. if role_binding == "lollms":
  1151. from lightrag.llm.lollms import lollms_model_if_cache
  1152. async def role_lollms_complete(
  1153. prompt,
  1154. system_prompt=None,
  1155. history_messages=None,
  1156. enable_cot: bool = False,
  1157. **kwargs,
  1158. ):
  1159. # response_format and legacy extraction booleans flow
  1160. # through kwargs to lollms_model_if_cache, which drops
  1161. # them and emits deprecation warnings when booleans are set.
  1162. if history_messages is None:
  1163. history_messages = []
  1164. if role_provider_options:
  1165. kwargs = {**role_provider_options, **kwargs}
  1166. return await lollms_model_if_cache(
  1167. role_model,
  1168. prompt,
  1169. system_prompt=system_prompt,
  1170. history_messages=history_messages,
  1171. enable_cot=enable_cot,
  1172. base_url=role_host,
  1173. api_key=role_apikey,
  1174. timeout=role_timeout,
  1175. **kwargs,
  1176. )
  1177. return role_lollms_complete
  1178. if role_binding == "bedrock":
  1179. from lightrag.llm.bedrock import bedrock_complete_if_cache
  1180. async def role_bedrock_complete(
  1181. prompt,
  1182. system_prompt=None,
  1183. history_messages=None,
  1184. **kwargs,
  1185. ) -> str:
  1186. if history_messages is None:
  1187. history_messages = []
  1188. if role_provider_options:
  1189. kwargs = {**role_provider_options, **kwargs}
  1190. return await bedrock_complete_if_cache(
  1191. role_model,
  1192. prompt,
  1193. system_prompt=system_prompt,
  1194. history_messages=history_messages,
  1195. endpoint_url=role_host,
  1196. **bedrock_aws_options,
  1197. **kwargs,
  1198. )
  1199. return role_bedrock_complete
  1200. if role_binding == "azure_openai":
  1201. from lightrag.llm.azure_openai import azure_openai_complete_if_cache
  1202. async def role_azure_openai_complete(
  1203. prompt,
  1204. system_prompt=None,
  1205. history_messages=None,
  1206. **kwargs,
  1207. ) -> str:
  1208. if history_messages is None:
  1209. history_messages = []
  1210. kwargs["timeout"] = role_timeout
  1211. if role_provider_options:
  1212. kwargs.update(role_provider_options)
  1213. return await azure_openai_complete_if_cache(
  1214. role_model,
  1215. prompt,
  1216. system_prompt=system_prompt,
  1217. history_messages=history_messages,
  1218. base_url=role_host,
  1219. api_key=role_apikey or os.getenv("AZURE_OPENAI_API_KEY"),
  1220. api_version=os.getenv(
  1221. "AZURE_OPENAI_API_VERSION", "2024-08-01-preview"
  1222. ),
  1223. **kwargs,
  1224. )
  1225. return role_azure_openai_complete
  1226. if role_binding == "gemini":
  1227. from lightrag.llm.gemini import gemini_complete_if_cache
  1228. async def role_gemini_complete(
  1229. prompt,
  1230. system_prompt=None,
  1231. history_messages=None,
  1232. **kwargs,
  1233. ) -> str:
  1234. if history_messages is None:
  1235. history_messages = []
  1236. kwargs["timeout"] = role_timeout
  1237. if role_provider_options and "generation_config" not in kwargs:
  1238. kwargs["generation_config"] = dict(role_provider_options)
  1239. return await gemini_complete_if_cache(
  1240. role_model,
  1241. prompt,
  1242. system_prompt=system_prompt,
  1243. history_messages=history_messages,
  1244. api_key=role_apikey,
  1245. base_url=role_host,
  1246. **kwargs,
  1247. )
  1248. return role_gemini_complete
  1249. from lightrag.llm.openai import openai_complete_if_cache
  1250. async def role_openai_complete(
  1251. prompt,
  1252. system_prompt=None,
  1253. history_messages=None,
  1254. **kwargs,
  1255. ) -> str:
  1256. if history_messages is None:
  1257. history_messages = []
  1258. kwargs["timeout"] = role_timeout
  1259. if role_provider_options:
  1260. kwargs.update(role_provider_options)
  1261. return await openai_complete_if_cache(
  1262. role_model,
  1263. prompt,
  1264. system_prompt=system_prompt,
  1265. history_messages=history_messages,
  1266. base_url=role_host,
  1267. api_key=role_apikey,
  1268. **kwargs,
  1269. )
  1270. return role_openai_complete
  1271. except ImportError as e:
  1272. raise Exception(f"Failed to create LLM for role '{role}': {e}")
  1273. def create_role_llm_model_kwargs(
  1274. role: str, override_meta: dict | None = None
  1275. ) -> dict[str, Any] | None:
  1276. """Create role-specific kwargs for runtime wrapper injection.
  1277. Role functions built above already encapsulate provider host/model/api_key/options,
  1278. so we intentionally return an empty dict here to prevent base kwargs inheritance
  1279. from polluting cross-provider role calls.
  1280. """
  1281. _ = role
  1282. _ = override_meta
  1283. return {}
  1284. def create_optimized_embedding_function(
  1285. config_cache: LLMConfigCache,
  1286. binding,
  1287. model,
  1288. host,
  1289. api_key,
  1290. args,
  1291. document_prefix=None,
  1292. query_prefix=None,
  1293. ) -> EmbeddingFunc:
  1294. """
  1295. Create optimized embedding function and return an EmbeddingFunc instance
  1296. with proper max_token_size inheritance from provider defaults.
  1297. This function:
  1298. 1. Imports the provider embedding function
  1299. 2. Extracts max_token_size and embedding_dim from provider if it's an EmbeddingFunc
  1300. 3. Creates an optimized wrapper that calls the underlying function directly (avoiding double-wrapping)
  1301. 4. Returns a properly configured EmbeddingFunc instance
  1302. Configuration Rules:
  1303. - When EMBEDDING_MODEL is not set: Uses provider's default model and dimension
  1304. (e.g., jina-embeddings-v4 with 2048 dims, text-embedding-3-small with 1536 dims)
  1305. - When EMBEDDING_MODEL is set to a custom model: User MUST also set EMBEDDING_DIM
  1306. to match the custom model's dimension (e.g., for jina-embeddings-v3, set EMBEDDING_DIM=1024)
  1307. Note: The embedding_dim parameter is automatically injected by EmbeddingFunc wrapper
  1308. when send_dimensions=True (enabled for Jina and Gemini bindings). This wrapper calls
  1309. the underlying provider function directly (.func) to avoid double-wrapping, so we must
  1310. explicitly pass embedding_dim to the provider's underlying function.
  1311. """
  1312. # Step 1: Import provider function and extract default attributes
  1313. provider_func = None
  1314. provider_max_token_size = None
  1315. provider_embedding_dim = None
  1316. provider_supports_asymmetric = False
  1317. try:
  1318. if binding == "openai":
  1319. from lightrag.llm.openai import openai_embed
  1320. provider_func = openai_embed
  1321. elif binding == "ollama":
  1322. from lightrag.llm.ollama import ollama_embed
  1323. provider_func = ollama_embed
  1324. elif binding == "gemini":
  1325. from lightrag.llm.gemini import gemini_embed
  1326. provider_func = gemini_embed
  1327. elif binding == "jina":
  1328. from lightrag.llm.jina import jina_embed
  1329. provider_func = jina_embed
  1330. elif binding == "azure_openai":
  1331. from lightrag.llm.azure_openai import azure_openai_embed
  1332. provider_func = azure_openai_embed
  1333. elif binding == "bedrock":
  1334. from lightrag.llm.bedrock import bedrock_embed
  1335. provider_func = bedrock_embed
  1336. elif binding == "lollms":
  1337. from lightrag.llm.lollms import lollms_embed
  1338. provider_func = lollms_embed
  1339. elif binding == "voyageai":
  1340. from lightrag.llm.voyageai import voyageai_embed
  1341. provider_func = voyageai_embed
  1342. # Extract attributes if provider is an EmbeddingFunc
  1343. if provider_func and isinstance(provider_func, EmbeddingFunc):
  1344. provider_max_token_size = provider_func.max_token_size
  1345. provider_embedding_dim = provider_func.embedding_dim
  1346. provider_supports_asymmetric = provider_func.supports_asymmetric
  1347. logger.debug(
  1348. f"Extracted from {binding} provider: "
  1349. f"max_token_size={provider_max_token_size}, "
  1350. f"embedding_dim={provider_embedding_dim}, "
  1351. f"supports_asymmetric={provider_supports_asymmetric}"
  1352. )
  1353. except ImportError as e:
  1354. logger.warning(f"Could not import provider function for {binding}: {e}")
  1355. # Step 2: Apply priority (user config > provider default)
  1356. # For max_token_size: explicit env var > provider default > None
  1357. final_max_token_size = args.embedding_token_limit or provider_max_token_size
  1358. # For embedding_dim: user config (always has value) takes priority
  1359. # Only use provider default if user config is explicitly None (which shouldn't happen)
  1360. final_embedding_dim = (
  1361. args.embedding_dim if args.embedding_dim else provider_embedding_dim
  1362. )
  1363. # Asymmetric embedding is explicit opt-in only. Provider-specific
  1364. # validation decides whether task parameters or prefixes are required.
  1365. asymmetric_opt_in = resolve_asymmetric_embedding_opt_in(
  1366. binding=binding,
  1367. embedding_asymmetric=args.embedding_asymmetric,
  1368. embedding_asymmetric_configured=args.embedding_asymmetric_configured,
  1369. query_prefix=query_prefix,
  1370. document_prefix=document_prefix,
  1371. query_prefix_configured=args.embedding_query_prefix_configured,
  1372. document_prefix_configured=args.embedding_document_prefix_configured,
  1373. )
  1374. # Step 3: Create optimized embedding function (calls underlying function directly)
  1375. # Note: When model is None, each binding will use its own default model
  1376. async def optimized_embedding_function(
  1377. texts, embedding_dim=None, context="document"
  1378. ):
  1379. try:
  1380. if binding == "lollms":
  1381. from lightrag.llm.lollms import lollms_embed
  1382. # Get real function, skip EmbeddingFunc wrapper if present
  1383. actual_func = (
  1384. lollms_embed.func
  1385. if isinstance(lollms_embed, EmbeddingFunc)
  1386. else lollms_embed
  1387. )
  1388. # lollms embed_model is not used (server uses configured vectorizer)
  1389. # Only pass base_url and api_key
  1390. return await actual_func(texts, base_url=host, api_key=api_key)
  1391. elif binding == "ollama":
  1392. from lightrag.llm.ollama import ollama_embed
  1393. # Get real function, skip EmbeddingFunc wrapper if present
  1394. actual_func = (
  1395. ollama_embed.func
  1396. if isinstance(ollama_embed, EmbeddingFunc)
  1397. else ollama_embed
  1398. )
  1399. # Use pre-processed configuration if available
  1400. if config_cache.ollama_embedding_options is not None:
  1401. ollama_options = config_cache.ollama_embedding_options
  1402. else:
  1403. from lightrag.llm.binding_options import OllamaEmbeddingOptions
  1404. ollama_options = OllamaEmbeddingOptions.options_dict(args)
  1405. # Pass embed_model only if provided, let function use its default (bge-m3:latest)
  1406. kwargs = {
  1407. "texts": texts,
  1408. "host": host,
  1409. "api_key": api_key,
  1410. "options": ollama_options,
  1411. }
  1412. if provider_supports_asymmetric and asymmetric_opt_in:
  1413. kwargs["context"] = context
  1414. if query_prefix:
  1415. kwargs["query_prefix"] = query_prefix
  1416. if document_prefix:
  1417. kwargs["document_prefix"] = document_prefix
  1418. if model:
  1419. kwargs["embed_model"] = model
  1420. return await actual_func(**kwargs)
  1421. elif binding == "azure_openai":
  1422. from lightrag.llm.azure_openai import azure_openai_embed
  1423. actual_func = (
  1424. azure_openai_embed.func
  1425. if isinstance(azure_openai_embed, EmbeddingFunc)
  1426. else azure_openai_embed
  1427. )
  1428. # Pass model only if provided, let function use its default otherwise
  1429. kwargs = {
  1430. "texts": texts,
  1431. "api_key": api_key,
  1432. "embedding_dim": embedding_dim,
  1433. }
  1434. if model:
  1435. kwargs["model"] = model
  1436. if provider_supports_asymmetric and asymmetric_opt_in:
  1437. kwargs["context"] = context
  1438. if query_prefix:
  1439. kwargs["query_prefix"] = query_prefix
  1440. if document_prefix:
  1441. kwargs["document_prefix"] = document_prefix
  1442. return await actual_func(**kwargs)
  1443. elif binding == "bedrock":
  1444. from lightrag.llm.bedrock import bedrock_embed
  1445. actual_func = (
  1446. bedrock_embed.func
  1447. if isinstance(bedrock_embed, EmbeddingFunc)
  1448. else bedrock_embed
  1449. )
  1450. # Pass model only if provided, let function use its default otherwise
  1451. kwargs = {
  1452. "texts": texts,
  1453. "aws_region": getattr(args, "aws_region", None),
  1454. "aws_access_key_id": getattr(args, "aws_access_key_id", None),
  1455. "aws_secret_access_key": getattr(
  1456. args, "aws_secret_access_key", None
  1457. ),
  1458. "aws_session_token": getattr(args, "aws_session_token", None),
  1459. }
  1460. if host is not None:
  1461. kwargs["endpoint_url"] = host
  1462. if model:
  1463. kwargs["model"] = model
  1464. return await actual_func(**kwargs)
  1465. elif binding == "jina":
  1466. from lightrag.llm.jina import jina_embed
  1467. actual_func = (
  1468. jina_embed.func
  1469. if isinstance(jina_embed, EmbeddingFunc)
  1470. else jina_embed
  1471. )
  1472. # Pass model only if provided, let function use its default (jina-embeddings-v4)
  1473. kwargs = {
  1474. "texts": texts,
  1475. "embedding_dim": embedding_dim,
  1476. "base_url": host,
  1477. "api_key": api_key,
  1478. }
  1479. if model:
  1480. kwargs["model"] = model
  1481. if provider_supports_asymmetric and asymmetric_opt_in:
  1482. kwargs["context"] = context
  1483. kwargs["task"] = None
  1484. return await actual_func(**kwargs)
  1485. elif binding == "gemini":
  1486. from lightrag.llm.gemini import gemini_embed
  1487. actual_func = (
  1488. gemini_embed.func
  1489. if isinstance(gemini_embed, EmbeddingFunc)
  1490. else gemini_embed
  1491. )
  1492. # Use pre-processed configuration if available
  1493. if config_cache.gemini_embedding_options is not None:
  1494. gemini_options = config_cache.gemini_embedding_options
  1495. else:
  1496. from lightrag.llm.binding_options import GeminiEmbeddingOptions
  1497. gemini_options = GeminiEmbeddingOptions.options_dict(args)
  1498. # Pass model only if provided, let function use its default (gemini-embedding-001)
  1499. kwargs = {
  1500. "texts": texts,
  1501. "base_url": host,
  1502. "api_key": api_key,
  1503. "embedding_dim": embedding_dim,
  1504. }
  1505. if model:
  1506. kwargs["model"] = model
  1507. task_type = gemini_options.get("task_type")
  1508. if task_type is not None:
  1509. kwargs["task_type"] = task_type
  1510. if provider_supports_asymmetric and asymmetric_opt_in:
  1511. kwargs["context"] = context
  1512. return await actual_func(**kwargs)
  1513. elif binding == "voyageai":
  1514. from lightrag.llm.voyageai import voyageai_embed
  1515. actual_func = (
  1516. voyageai_embed.func
  1517. if isinstance(voyageai_embed, EmbeddingFunc)
  1518. else voyageai_embed
  1519. )
  1520. kwargs = {
  1521. "texts": texts,
  1522. "api_key": api_key,
  1523. "embedding_dim": embedding_dim,
  1524. }
  1525. if model:
  1526. kwargs["model"] = model
  1527. if provider_supports_asymmetric and asymmetric_opt_in:
  1528. kwargs["context"] = context
  1529. return await actual_func(**kwargs)
  1530. else: # openai and compatible
  1531. from lightrag.llm.openai import openai_embed
  1532. actual_func = (
  1533. openai_embed.func
  1534. if isinstance(openai_embed, EmbeddingFunc)
  1535. else openai_embed
  1536. )
  1537. # Pass model only if provided, let function use its default (text-embedding-3-small)
  1538. kwargs = {
  1539. "texts": texts,
  1540. "base_url": host,
  1541. "api_key": api_key,
  1542. "embedding_dim": embedding_dim,
  1543. }
  1544. if model:
  1545. kwargs["model"] = model
  1546. if provider_supports_asymmetric and asymmetric_opt_in:
  1547. kwargs["context"] = context
  1548. if query_prefix:
  1549. kwargs["query_prefix"] = query_prefix
  1550. if document_prefix:
  1551. kwargs["document_prefix"] = document_prefix
  1552. return await actual_func(**kwargs)
  1553. except ImportError as e:
  1554. raise Exception(f"Failed to import {binding} embedding: {e}")
  1555. # Step 4: Wrap in EmbeddingFunc and return
  1556. embedding_func_instance = EmbeddingFunc(
  1557. embedding_dim=final_embedding_dim,
  1558. func=optimized_embedding_function,
  1559. max_token_size=final_max_token_size,
  1560. send_dimensions=False, # Will be set later based on binding requirements
  1561. model_name=model,
  1562. supports_asymmetric=provider_supports_asymmetric and asymmetric_opt_in,
  1563. )
  1564. # Log final embedding configuration. Only include prefix info when
  1565. # prefixes will actually be applied (prefix-based asymmetric mode).
  1566. prefix_info = ""
  1567. if (
  1568. asymmetric_opt_in
  1569. and binding in PREFIX_ASYMMETRIC_EMBEDDING_BINDINGS
  1570. and (document_prefix or query_prefix)
  1571. ):
  1572. prefix_info = f" document_prefix={repr(document_prefix)} query_prefix={repr(query_prefix)}"
  1573. logger.info(
  1574. f"Embedding config: binding={binding} model={model} "
  1575. f"embedding_dim={final_embedding_dim} max_token_size={final_max_token_size}{prefix_info}"
  1576. )
  1577. return embedding_func_instance
  1578. llm_timeout = args.llm_timeout
  1579. embedding_timeout = args.embedding_timeout
  1580. async def bedrock_model_complete(
  1581. prompt,
  1582. system_prompt=None,
  1583. history_messages=None,
  1584. **kwargs,
  1585. ) -> str:
  1586. # Lazy import
  1587. from lightrag.llm.bedrock import bedrock_complete_if_cache
  1588. if history_messages is None:
  1589. history_messages = []
  1590. # Bedrock Converse API has no JSON mode; response_format and the legacy
  1591. # extraction booleans flow through kwargs to bedrock_complete_if_cache,
  1592. # which drops them and emits deprecation warnings when booleans are set.
  1593. if config_cache.bedrock_llm_options:
  1594. kwargs = {**config_cache.bedrock_llm_options, **kwargs}
  1595. return await bedrock_complete_if_cache(
  1596. args.llm_model,
  1597. prompt,
  1598. system_prompt=system_prompt,
  1599. history_messages=history_messages,
  1600. endpoint_url=args.llm_binding_host,
  1601. aws_region=getattr(args, "aws_region", None),
  1602. aws_access_key_id=getattr(args, "aws_access_key_id", None),
  1603. aws_secret_access_key=getattr(args, "aws_secret_access_key", None),
  1604. aws_session_token=getattr(args, "aws_session_token", None),
  1605. **kwargs,
  1606. )
  1607. # Create embedding function with optimized configuration and max_token_size inheritance
  1608. import inspect
  1609. # Create the EmbeddingFunc instance (now returns complete EmbeddingFunc with max_token_size)
  1610. embedding_func = create_optimized_embedding_function(
  1611. config_cache=config_cache,
  1612. binding=args.embedding_binding,
  1613. model=args.embedding_model,
  1614. host=args.embedding_binding_host,
  1615. api_key=None
  1616. if args.embedding_binding == "bedrock"
  1617. else args.embedding_binding_api_key,
  1618. args=args,
  1619. document_prefix=args.embedding_document_prefix,
  1620. query_prefix=args.embedding_query_prefix,
  1621. )
  1622. # Get embedding_send_dim from centralized configuration
  1623. embedding_send_dim = args.embedding_send_dim
  1624. # Check if the underlying function signature has embedding_dim parameter
  1625. sig = inspect.signature(embedding_func.func)
  1626. has_embedding_dim_param = "embedding_dim" in sig.parameters
  1627. # Determine send_dimensions value based on binding type
  1628. # Jina and Gemini REQUIRE dimension parameter (forced to True)
  1629. # OpenAI and others: controlled by EMBEDDING_SEND_DIM environment variable
  1630. if args.embedding_binding in ["jina", "gemini"]:
  1631. # Jina and Gemini APIs require dimension parameter - always send it
  1632. send_dimensions = has_embedding_dim_param
  1633. dimension_control = f"forced by {args.embedding_binding.title()} API"
  1634. else:
  1635. # For OpenAI and other bindings, respect EMBEDDING_SEND_DIM setting
  1636. send_dimensions = embedding_send_dim and has_embedding_dim_param
  1637. if send_dimensions or not embedding_send_dim:
  1638. dimension_control = "by env var"
  1639. else:
  1640. dimension_control = "by not hasparam"
  1641. # Set send_dimensions on the EmbeddingFunc instance
  1642. embedding_func.send_dimensions = send_dimensions
  1643. logger.info(
  1644. f"Send embedding dimension: {send_dimensions} {dimension_control} "
  1645. f"(dimensions={embedding_func.embedding_dim}, has_param={has_embedding_dim_param}, "
  1646. f"binding={args.embedding_binding})"
  1647. )
  1648. # Log max_token_size source
  1649. if embedding_func.max_token_size:
  1650. source = (
  1651. "env variable"
  1652. if args.embedding_token_limit
  1653. else f"{args.embedding_binding} provider default"
  1654. )
  1655. logger.info(
  1656. f"Embedding max_token_size: {embedding_func.max_token_size} (from {source})"
  1657. )
  1658. else:
  1659. logger.info(
  1660. "Embedding max_token_size: None (Embedding token limit is disabled)."
  1661. )
  1662. # Configure rerank function based on args.rerank_bindingparameter
  1663. rerank_model_func = None
  1664. if args.rerank_binding != "null":
  1665. from lightrag.rerank import cohere_rerank, jina_rerank, ali_rerank
  1666. # Map rerank binding to corresponding function
  1667. rerank_functions = {
  1668. "cohere": cohere_rerank,
  1669. "jina": jina_rerank,
  1670. "aliyun": ali_rerank,
  1671. }
  1672. # Select the appropriate rerank function based on binding
  1673. selected_rerank_func = rerank_functions.get(args.rerank_binding)
  1674. if not selected_rerank_func:
  1675. logger.error(f"Unsupported rerank binding: {args.rerank_binding}")
  1676. raise ValueError(f"Unsupported rerank binding: {args.rerank_binding}")
  1677. # Get default values from selected_rerank_func if args values are None
  1678. if args.rerank_model is None or args.rerank_binding_host is None:
  1679. sig = inspect.signature(selected_rerank_func)
  1680. # Set default model if args.rerank_model is None
  1681. if args.rerank_model is None and "model" in sig.parameters:
  1682. default_model = sig.parameters["model"].default
  1683. if default_model != inspect.Parameter.empty:
  1684. args.rerank_model = default_model
  1685. # Set default base_url if args.rerank_binding_host is None
  1686. if args.rerank_binding_host is None and "base_url" in sig.parameters:
  1687. default_base_url = sig.parameters["base_url"].default
  1688. if default_base_url != inspect.Parameter.empty:
  1689. args.rerank_binding_host = default_base_url
  1690. async def server_rerank_func(
  1691. query: str, documents: list, top_n: int = None, extra_body: dict = None
  1692. ):
  1693. """Server rerank function with configuration from environment variables"""
  1694. # Prepare kwargs for rerank function
  1695. kwargs = {
  1696. "query": query,
  1697. "documents": documents,
  1698. "top_n": top_n,
  1699. "api_key": args.rerank_binding_api_key,
  1700. "model": args.rerank_model,
  1701. "base_url": args.rerank_binding_host,
  1702. }
  1703. # Add Cohere-specific parameters if using cohere binding
  1704. if args.rerank_binding == "cohere":
  1705. # Enable chunking if configured (useful for models with token limits like ColBERT)
  1706. kwargs["enable_chunking"] = (
  1707. os.getenv("RERANK_ENABLE_CHUNKING", "false").lower() == "true"
  1708. )
  1709. kwargs["max_tokens_per_doc"] = int(
  1710. os.getenv("RERANK_MAX_TOKENS_PER_DOC", "4096")
  1711. )
  1712. return await selected_rerank_func(**kwargs, extra_body=extra_body)
  1713. rerank_model_func = server_rerank_func
  1714. logger.info(
  1715. f"Reranking is enabled: {args.rerank_model or 'default model'} using {args.rerank_binding} provider"
  1716. )
  1717. else:
  1718. logger.info("Reranking is disabled")
  1719. # Create ollama_server_infos from command line arguments
  1720. from lightrag.api.config import OllamaServerInfos
  1721. ollama_server_infos = OllamaServerInfos(
  1722. name=args.simulated_model_name, tag=args.simulated_model_tag
  1723. )
  1724. # LightRAG.__post_init__ normalizes addon_params and backfills env-based defaults
  1725. # (SUMMARY_LANGUAGE, ENTITY_TYPE_PROMPT_FILE, ...), so we only need to pass the
  1726. # API-level overrides here.
  1727. addon_params = {
  1728. "language": args.summary_language,
  1729. }
  1730. role_llm_configs = {
  1731. spec.name: {
  1732. **resolve_role_llm_settings(spec.name),
  1733. "func": create_role_llm_func(spec.name),
  1734. "kwargs": create_role_llm_model_kwargs(spec.name),
  1735. }
  1736. for spec in ROLES
  1737. }
  1738. # Initialize RAG with unified configuration
  1739. try:
  1740. rag = LightRAG(
  1741. working_dir=args.working_dir,
  1742. workspace=args.workspace,
  1743. llm_model_func=create_llm_model_func(args.llm_binding),
  1744. llm_model_name=args.llm_model,
  1745. llm_model_max_async=args.max_async,
  1746. summary_max_tokens=args.summary_max_tokens,
  1747. summary_context_size=args.summary_context_size,
  1748. chunk_token_size=int(args.chunk_size),
  1749. chunk_overlap_token_size=int(args.chunk_overlap_size),
  1750. llm_model_kwargs=create_llm_model_kwargs(
  1751. args.llm_binding, args, llm_timeout
  1752. ),
  1753. embedding_func=embedding_func,
  1754. default_llm_timeout=llm_timeout,
  1755. default_embedding_timeout=embedding_timeout,
  1756. kv_storage=args.kv_storage,
  1757. graph_storage=args.graph_storage,
  1758. vector_storage=args.vector_storage,
  1759. doc_status_storage=args.doc_status_storage,
  1760. vector_db_storage_cls_kwargs={
  1761. "cosine_better_than_threshold": args.cosine_threshold
  1762. },
  1763. enable_llm_cache_for_entity_extract=args.enable_llm_cache_for_extract,
  1764. enable_llm_cache=args.enable_llm_cache,
  1765. vlm_process_enable=args.vlm_process_enable,
  1766. rerank_model_func=rerank_model_func,
  1767. rerank_model_max_async=args.rerank_max_async,
  1768. default_rerank_timeout=args.rerank_timeout,
  1769. max_parallel_insert=args.max_parallel_insert,
  1770. max_graph_nodes=args.max_graph_nodes,
  1771. addon_params=addon_params,
  1772. ollama_server_infos=ollama_server_infos,
  1773. role_llm_configs={
  1774. spec.name: RoleLLMConfig(
  1775. func=role_llm_configs[spec.name]["func"],
  1776. kwargs=role_llm_configs[spec.name]["kwargs"],
  1777. max_async=role_llm_configs[spec.name]["max_async"],
  1778. timeout=role_llm_configs[spec.name]["timeout"],
  1779. metadata={
  1780. "base_binding": args.llm_binding,
  1781. "binding": role_llm_configs[spec.name]["binding"],
  1782. "model": role_llm_configs[spec.name]["model"],
  1783. "host": role_llm_configs[spec.name]["host"],
  1784. "api_key": role_llm_configs[spec.name]["api_key"],
  1785. "provider_options": role_llm_configs[spec.name][
  1786. "provider_options"
  1787. ],
  1788. "bedrock_aws_options": role_llm_configs[spec.name][
  1789. "bedrock_aws_options"
  1790. ],
  1791. "is_cross_provider": role_llm_configs[spec.name][
  1792. "is_cross_provider"
  1793. ],
  1794. },
  1795. )
  1796. for spec in ROLES
  1797. },
  1798. )
  1799. except Exception as e:
  1800. logger.error(f"Failed to initialize LightRAG: {e}")
  1801. raise
  1802. _log_role_provider_options(rag)
  1803. rag.register_role_llm_builder(
  1804. lambda role, meta: (
  1805. create_role_llm_func(role, meta),
  1806. create_role_llm_model_kwargs(role, meta),
  1807. )
  1808. )
  1809. # Add routes
  1810. # root_path is set on the app for reverse proxy support;
  1811. # routes stay at their natural paths and are prefixed by the proxy or uvicorn --root-path
  1812. app.include_router(create_document_routes(rag, doc_manager, api_key))
  1813. app.include_router(create_query_routes(rag, api_key, args.top_k))
  1814. app.include_router(create_graph_routes(rag, api_key))
  1815. # Add Ollama API routes
  1816. ollama_api = OllamaAPI(rag, top_k=args.top_k, api_key=api_key)
  1817. app.include_router(ollama_api.router, prefix="/api")
  1818. # Custom Swagger UI endpoint for offline support
  1819. @app.get("/docs", include_in_schema=False)
  1820. async def custom_swagger_ui_html(request: Request):
  1821. """Custom Swagger UI HTML with local static files"""
  1822. response = get_swagger_ui_html(
  1823. openapi_url=app.openapi_url,
  1824. title=app.title + " - Swagger UI",
  1825. oauth2_redirect_url="/docs/oauth2-redirect",
  1826. swagger_js_url="/static/swagger-ui/swagger-ui-bundle.js",
  1827. swagger_css_url="/static/swagger-ui/swagger-ui.css",
  1828. swagger_favicon_url="/static/swagger-ui/favicon-32x32.png",
  1829. swagger_ui_parameters=app.swagger_ui_parameters,
  1830. )
  1831. html = response.body.decode("utf-8")
  1832. html = _inject_swagger_theme(
  1833. html, request.query_params.get("theme", "auto").lower()
  1834. )
  1835. return HTMLResponse(content=html)
  1836. @app.get("/docs/oauth2-redirect", include_in_schema=False)
  1837. async def swagger_ui_redirect():
  1838. """OAuth2 redirect for Swagger UI"""
  1839. return get_swagger_ui_oauth2_redirect_html()
  1840. @app.get("/")
  1841. async def redirect_to_webui(request: Request):
  1842. """Redirect root path based on WebUI availability.
  1843. Prepend the ASGI root_path so that, behind a reverse proxy, the
  1844. absolute redirect target keeps the configured prefix instead of
  1845. bypassing it.
  1846. """
  1847. root = request.scope.get("root_path", "")
  1848. if webui_assets_exist:
  1849. return RedirectResponse(url=f"{root}{webui_path}/")
  1850. else:
  1851. return RedirectResponse(url=f"{root}/docs")
  1852. @app.get("/auth-status")
  1853. async def get_auth_status():
  1854. """Get authentication status and guest token if auth is not configured"""
  1855. if not auth_handler.accounts:
  1856. # Authentication not configured, return guest token
  1857. guest_token = auth_handler.create_token(
  1858. username="guest", role="guest", metadata={"auth_mode": "disabled"}
  1859. )
  1860. return {
  1861. "auth_configured": False,
  1862. "access_token": guest_token,
  1863. "token_type": "bearer",
  1864. "auth_mode": "disabled",
  1865. "message": "Authentication is disabled. Using guest access.",
  1866. "core_version": core_version,
  1867. "api_version": api_version_display,
  1868. "webui_title": webui_title,
  1869. "webui_description": webui_description,
  1870. }
  1871. return {
  1872. "auth_configured": True,
  1873. "auth_mode": "enabled",
  1874. "core_version": core_version,
  1875. "api_version": api_version_display,
  1876. "webui_title": webui_title,
  1877. "webui_description": webui_description,
  1878. }
  1879. @app.post("/login")
  1880. async def login(form_data: OAuth2PasswordRequestForm = Depends()):
  1881. if not auth_handler.accounts:
  1882. # Authentication not configured, return guest token
  1883. guest_token = auth_handler.create_token(
  1884. username="guest", role="guest", metadata={"auth_mode": "disabled"}
  1885. )
  1886. return {
  1887. "access_token": guest_token,
  1888. "token_type": "bearer",
  1889. "auth_mode": "disabled",
  1890. "message": "Authentication is disabled. Using guest access.",
  1891. "core_version": core_version,
  1892. "api_version": api_version_display,
  1893. "webui_title": webui_title,
  1894. "webui_description": webui_description,
  1895. }
  1896. username = form_data.username
  1897. if not auth_handler.verify_password(username, form_data.password):
  1898. raise HTTPException(status_code=401, detail="Incorrect credentials")
  1899. # Regular user login
  1900. user_token = auth_handler.create_token(
  1901. username=username, role="user", metadata={"auth_mode": "enabled"}
  1902. )
  1903. return {
  1904. "access_token": user_token,
  1905. "token_type": "bearer",
  1906. "auth_mode": "enabled",
  1907. "core_version": core_version,
  1908. "api_version": api_version_display,
  1909. "webui_title": webui_title,
  1910. "webui_description": webui_description,
  1911. }
  1912. @app.get(
  1913. "/health",
  1914. dependencies=[Depends(combined_auth)],
  1915. summary="Get system health and configuration status",
  1916. description="Returns comprehensive system status including WebUI availability, configuration, and operational metrics",
  1917. response_description="System health status with configuration details",
  1918. responses={
  1919. 200: {
  1920. "description": "Successful response with system status",
  1921. "content": {
  1922. "application/json": {
  1923. "example": {
  1924. "status": "healthy",
  1925. "webui_available": True,
  1926. "working_directory": "/path/to/working/dir",
  1927. "input_directory": "/path/to/input/dir",
  1928. "configuration": {
  1929. "llm_binding": "openai",
  1930. "llm_model": "gpt-4",
  1931. "embedding_binding": "openai",
  1932. "embedding_model": "text-embedding-ada-002",
  1933. "workspace": "default",
  1934. "storage_workspaces": {
  1935. "kv_storage": "default",
  1936. "doc_status_storage": "default",
  1937. "graph_storage": "default",
  1938. "vector_storage": "default",
  1939. },
  1940. "parser_routing": "pdf:mineru",
  1941. "mineru": {
  1942. "endpoint": "http://localhost:8080",
  1943. "api_mode": "local",
  1944. "options": {
  1945. "language": "ch",
  1946. "enable_table": True,
  1947. "enable_formula": True,
  1948. "local_backend": "pipeline",
  1949. "local_parse_method": "auto",
  1950. "local_image_analysis": False,
  1951. },
  1952. },
  1953. "docling": {
  1954. "endpoint": "",
  1955. "options": {},
  1956. },
  1957. },
  1958. "auth_mode": "enabled",
  1959. "pipeline_busy": False,
  1960. "core_version": "0.0.1",
  1961. "api_version": "0.0.1",
  1962. }
  1963. }
  1964. },
  1965. }
  1966. },
  1967. )
  1968. async def get_status(request: Request):
  1969. """Get current system status including WebUI availability"""
  1970. try:
  1971. workspace = get_workspace_from_request(request)
  1972. default_workspace = get_default_workspace()
  1973. if workspace is None:
  1974. workspace = default_workspace
  1975. pipeline_status = await get_namespace_data(
  1976. "pipeline_status", workspace=workspace
  1977. )
  1978. pipeline_busy = bool(pipeline_status.get("busy", False))
  1979. pipeline_scanning = bool(pipeline_status.get("scanning", False))
  1980. pipeline_destructive_busy = bool(
  1981. pipeline_status.get("destructive_busy", False)
  1982. )
  1983. pipeline_pending_enqueues = int(
  1984. pipeline_status.get("pending_enqueues", 0) or 0
  1985. )
  1986. pipeline_active = (
  1987. pipeline_busy
  1988. or pipeline_scanning
  1989. or pipeline_destructive_busy
  1990. or pipeline_pending_enqueues > 0
  1991. )
  1992. if not auth_configured:
  1993. auth_mode = "disabled"
  1994. else:
  1995. auth_mode = "enabled"
  1996. # Cleanup expired keyed locks and get status
  1997. keyed_lock_info = cleanup_keyed_lock()
  1998. return {
  1999. "status": "healthy",
  2000. "webui_available": webui_assets_exist,
  2001. "working_directory": str(args.working_dir),
  2002. "input_directory": str(args.input_dir),
  2003. "configuration": {
  2004. # LLM configuration binding/host address (if applicable)/model (if applicable)
  2005. "llm_binding": args.llm_binding,
  2006. "llm_binding_host": args.llm_binding_host,
  2007. "llm_model": args.llm_model,
  2008. # embedding model configuration binding/host address (if applicable)/model (if applicable)
  2009. "embedding_binding": args.embedding_binding,
  2010. "embedding_binding_host": args.embedding_binding_host,
  2011. "embedding_model": args.embedding_model,
  2012. "summary_max_tokens": args.summary_max_tokens,
  2013. "summary_context_size": args.summary_context_size,
  2014. "kv_storage": args.kv_storage,
  2015. "doc_status_storage": args.doc_status_storage,
  2016. "graph_storage": args.graph_storage,
  2017. "vector_storage": args.vector_storage,
  2018. "enable_llm_cache_for_extract": args.enable_llm_cache_for_extract,
  2019. "enable_llm_cache": args.enable_llm_cache,
  2020. "vlm_process_enable": args.vlm_process_enable,
  2021. "workspace": default_workspace,
  2022. "storage_workspaces": _get_storage_workspaces(rag),
  2023. "max_graph_nodes": args.max_graph_nodes,
  2024. # Rerank configuration
  2025. "enable_rerank": rerank_model_func is not None,
  2026. "rerank_binding": args.rerank_binding,
  2027. "rerank_model": args.rerank_model if rerank_model_func else None,
  2028. "rerank_binding_host": args.rerank_binding_host
  2029. if rerank_model_func
  2030. else None,
  2031. "rerank_max_async": args.rerank_max_async,
  2032. "rerank_timeout": args.rerank_timeout,
  2033. # Environment variable status (requested configuration)
  2034. "summary_language": args.summary_language,
  2035. "force_llm_summary_on_merge": args.force_llm_summary_on_merge,
  2036. "max_parallel_insert": args.max_parallel_insert,
  2037. "cosine_threshold": args.cosine_threshold,
  2038. "min_rerank_score": args.min_rerank_score,
  2039. "related_chunk_number": args.related_chunk_number,
  2040. "max_async": args.max_async,
  2041. "llm_timeout": args.llm_timeout,
  2042. "embedding_func_max_async": args.embedding_func_max_async,
  2043. "embedding_batch_num": args.embedding_batch_num,
  2044. "embedding_timeout": args.embedding_timeout,
  2045. "role_llm_config": rag.get_llm_role_config(),
  2046. # Parser routing snapshot — surfaced in the WebUI status card
  2047. "parser_routing": parser_rules_from_env(),
  2048. "mineru": _build_mineru_status(),
  2049. "docling": _build_docling_status(),
  2050. },
  2051. "auth_mode": auth_mode,
  2052. "pipeline_busy": pipeline_busy,
  2053. "pipeline_active": pipeline_active,
  2054. "pipeline_scanning": pipeline_scanning,
  2055. "pipeline_destructive_busy": pipeline_destructive_busy,
  2056. "pipeline_pending_enqueues": pipeline_pending_enqueues,
  2057. "keyed_locks": keyed_lock_info,
  2058. "llm_queue_status": await rag.get_llm_queue_status(include_base=True),
  2059. "embedding_queue_status": await rag.get_embedding_queue_status(),
  2060. "rerank_queue_status": await rag.get_rerank_queue_status(),
  2061. "core_version": core_version,
  2062. "api_version": api_version_display,
  2063. "webui_title": webui_title,
  2064. "webui_description": webui_description,
  2065. }
  2066. except Exception as e:
  2067. logger.error(f"Error getting health status: {str(e)}")
  2068. raise HTTPException(status_code=500, detail=str(e))
  2069. # Pre-render the runtime-config <script> once. The browser-visible URL
  2070. # prefixes are NOT baked into the bundle anymore — index.html ships with
  2071. # a placeholder comment that we replace with this snippet on every HTML
  2072. # response, so one build serves any reverse-proxy mount point.
  2073. #
  2074. # `</` → `<\/` escaping prevents an embedded "</script>" sequence from
  2075. # breaking out of the inline script (defense-in-depth — values come from
  2076. # admin config, not user input).
  2077. _runtime_config_payload = json.dumps(
  2078. {
  2079. "apiPrefix": api_prefix,
  2080. "webuiPrefix": f"{api_prefix}{webui_path}/",
  2081. }
  2082. ).replace("</", "<\\/")
  2083. runtime_config_script = (
  2084. f"<script>window.__LIGHTRAG_CONFIG__ = {_runtime_config_payload};</script>"
  2085. )
  2086. # Custom StaticFiles class for smart caching + runtime config injection
  2087. class SmartStaticFiles(StaticFiles): # Renamed from NoCacheStaticFiles
  2088. # Replaced in index.html on every request. Keep in sync with
  2089. # lightrag_webui/index.html.
  2090. RUNTIME_CONFIG_PLACEHOLDER = b"<!-- __LIGHTRAG_RUNTIME_CONFIG__ -->"
  2091. async def get_response(self, path: str, scope):
  2092. response = await super().get_response(path, scope)
  2093. # `path` is empty when accessing the mount root (StaticFiles
  2094. # rewrites it to index.html internally) — match on media_type
  2095. # too so we still inject in that case.
  2096. is_html = (
  2097. path.endswith(".html")
  2098. or path == ""
  2099. or path.endswith("/")
  2100. or getattr(response, "media_type", None) == "text/html"
  2101. )
  2102. if (
  2103. is_html
  2104. and getattr(response, "status_code", 0) == 200
  2105. and isinstance(response, FileResponse)
  2106. ):
  2107. response = self._inject_runtime_config(response)
  2108. if is_html:
  2109. response.headers["Cache-Control"] = (
  2110. "no-cache, no-store, must-revalidate"
  2111. )
  2112. response.headers["Pragma"] = "no-cache"
  2113. response.headers["Expires"] = "0"
  2114. elif (
  2115. "/assets/" in path
  2116. ): # Assets (JS, CSS, images, fonts) generated by Vite with hash in filename
  2117. response.headers["Cache-Control"] = (
  2118. "public, max-age=31536000, immutable"
  2119. )
  2120. # Add other rules here if needed for non-HTML, non-asset files
  2121. # Ensure correct Content-Type
  2122. if path.endswith(".js"):
  2123. response.headers["Content-Type"] = "application/javascript"
  2124. elif path.endswith(".css"):
  2125. response.headers["Content-Type"] = "text/css"
  2126. return response
  2127. def _inject_runtime_config(self, response: FileResponse) -> Response:
  2128. """Replace the runtime-config placeholder in index.html.
  2129. Returns the original FileResponse if the placeholder is absent
  2130. (older build, or a non-index HTML file) — avoids breaking
  2131. previously-working bundles during upgrades.
  2132. """
  2133. try:
  2134. content = Path(response.path).read_bytes()
  2135. except OSError as e:
  2136. logger.warning(
  2137. "Could not read %s for runtime config injection: %s",
  2138. response.path,
  2139. e,
  2140. )
  2141. return response
  2142. if self.RUNTIME_CONFIG_PLACEHOLDER not in content:
  2143. return response
  2144. new_content = content.replace(
  2145. self.RUNTIME_CONFIG_PLACEHOLDER,
  2146. runtime_config_script.encode("utf-8"),
  2147. )
  2148. return Response(content=new_content, media_type="text/html")
  2149. # Mount Swagger UI static files for offline support
  2150. swagger_static_dir = Path(__file__).parent / "static" / "swagger-ui"
  2151. if swagger_static_dir.exists():
  2152. app.mount(
  2153. "/static/swagger-ui",
  2154. StaticFiles(directory=swagger_static_dir),
  2155. name="swagger-ui-static",
  2156. )
  2157. # Conditionally mount WebUI only if assets exist
  2158. if webui_assets_exist:
  2159. static_dir = Path(__file__).parent / "webui"
  2160. static_dir.mkdir(exist_ok=True)
  2161. app.mount(
  2162. webui_path,
  2163. SmartStaticFiles(
  2164. directory=static_dir, html=True, check_dir=True
  2165. ), # Use SmartStaticFiles
  2166. name="webui",
  2167. )
  2168. logger.info(f"WebUI assets mounted at {webui_path}")
  2169. else:
  2170. logger.info("WebUI assets not available, WebUI route not mounted")
  2171. # Add redirect for WebUI path when assets are not available
  2172. @app.get(webui_path)
  2173. @app.get(f"{webui_path}/")
  2174. async def webui_redirect_to_docs(request: Request):
  2175. """Redirect WebUI path to /docs when WebUI is not available."""
  2176. root = request.scope.get("root_path", "")
  2177. return RedirectResponse(url=f"{root}/docs")
  2178. return app
  2179. def get_application(args=None):
  2180. """Factory function for creating the FastAPI application"""
  2181. if args is None:
  2182. args = global_args
  2183. return create_app(args)
  2184. def configure_logging():
  2185. """Configure logging for uvicorn startup"""
  2186. # Reset any existing handlers to ensure clean configuration
  2187. for logger_name in ["uvicorn", "uvicorn.access", "uvicorn.error", "lightrag"]:
  2188. logger = logging.getLogger(logger_name)
  2189. logger.handlers = []
  2190. logger.filters = []
  2191. # Get log directory path from environment variable
  2192. log_dir = os.getenv("LOG_DIR", os.getcwd())
  2193. log_file_path = os.path.abspath(os.path.join(log_dir, DEFAULT_LOG_FILENAME))
  2194. print(f"\nLightRAG log file: {log_file_path}\n")
  2195. os.makedirs(os.path.dirname(log_dir), exist_ok=True)
  2196. # Get log file max size and backup count from environment variables
  2197. log_max_bytes = get_env_value("LOG_MAX_BYTES", DEFAULT_LOG_MAX_BYTES, int)
  2198. log_backup_count = get_env_value("LOG_BACKUP_COUNT", DEFAULT_LOG_BACKUP_COUNT, int)
  2199. logging.config.dictConfig(
  2200. {
  2201. "version": 1,
  2202. "disable_existing_loggers": False,
  2203. "formatters": {
  2204. "default": {
  2205. "format": "%(levelname)s: %(message)s",
  2206. },
  2207. "detailed": {
  2208. "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
  2209. },
  2210. },
  2211. "handlers": {
  2212. "console": {
  2213. "formatter": "default",
  2214. "class": "logging.StreamHandler",
  2215. "stream": "ext://sys.stderr",
  2216. },
  2217. "file": {
  2218. "formatter": "detailed",
  2219. "class": "logging.handlers.RotatingFileHandler",
  2220. "filename": log_file_path,
  2221. "maxBytes": log_max_bytes,
  2222. "backupCount": log_backup_count,
  2223. "encoding": "utf-8",
  2224. },
  2225. },
  2226. "loggers": {
  2227. # Configure all uvicorn related loggers
  2228. "uvicorn": {
  2229. "handlers": ["console", "file"],
  2230. "level": "INFO",
  2231. "propagate": False,
  2232. },
  2233. "uvicorn.access": {
  2234. "handlers": ["console", "file"],
  2235. "level": "INFO",
  2236. "propagate": False,
  2237. "filters": ["path_filter"],
  2238. },
  2239. "uvicorn.error": {
  2240. "handlers": ["console", "file"],
  2241. "level": "INFO",
  2242. "propagate": False,
  2243. },
  2244. "lightrag": {
  2245. "handlers": ["console", "file"],
  2246. "level": "INFO",
  2247. "propagate": False,
  2248. "filters": ["path_filter"],
  2249. },
  2250. },
  2251. "filters": {
  2252. "path_filter": {
  2253. "()": "lightrag.utils.LightragPathFilter",
  2254. },
  2255. },
  2256. }
  2257. )
  2258. def check_and_install_dependencies():
  2259. """Check and install required dependencies"""
  2260. required_packages = [
  2261. "uvicorn",
  2262. "tiktoken",
  2263. "fastapi",
  2264. # Add other required packages here
  2265. ]
  2266. for package in required_packages:
  2267. if not pm.is_installed(package):
  2268. print(f"Installing {package}...")
  2269. pm.install(package)
  2270. print(f"{package} installed successfully")
  2271. def main():
  2272. # On Windows, ProactorEventLoop (default since Python 3.8) has known
  2273. # race conditions with uvicorn's socket binding that can cause the server
  2274. # to report it's running while the port is never actually bound.
  2275. # Using SelectorEventLoop resolves this issue.
  2276. # See: https://github.com/HKUDS/LightRAG/issues/2438
  2277. if sys.platform == "win32":
  2278. import asyncio
  2279. asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
  2280. # Explicitly initialize configuration for clarity
  2281. # (The proxy will auto-initialize anyway, but this makes intent clear)
  2282. from .config import initialize_config
  2283. initialize_config()
  2284. # Check if running under Gunicorn
  2285. if "GUNICORN_CMD_ARGS" in os.environ:
  2286. # If started with Gunicorn, return directly as Gunicorn will call get_application
  2287. print("Running under Gunicorn - worker management handled by Gunicorn")
  2288. return
  2289. # Check .env file
  2290. if not check_env_file():
  2291. sys.exit(1)
  2292. # Check and install dependencies
  2293. check_and_install_dependencies()
  2294. from multiprocessing import freeze_support
  2295. freeze_support()
  2296. # Configure logging before parsing args
  2297. configure_logging()
  2298. update_uvicorn_mode_config()
  2299. display_splash_screen(global_args)
  2300. # Note: Signal handlers are NOT registered here because:
  2301. # - Uvicorn has built-in signal handling that properly calls lifespan shutdown
  2302. # - Custom signal handlers can interfere with uvicorn's graceful shutdown
  2303. # - Cleanup is handled by the lifespan context manager's finally block
  2304. # Create application instance directly instead of using factory function
  2305. app = create_app(global_args)
  2306. # Start Uvicorn in single process mode. Do not pass root_path here;
  2307. # the prefix lives only on FastAPI's app.root_path. See
  2308. # docs/MultiSiteDeployment.md.
  2309. uvicorn_config = {
  2310. "app": app, # Pass application instance directly instead of string path
  2311. "host": global_args.host,
  2312. "port": global_args.port,
  2313. "log_config": None, # Disable default config
  2314. }
  2315. if global_args.ssl:
  2316. uvicorn_config.update(
  2317. {
  2318. "ssl_certfile": global_args.ssl_certfile,
  2319. "ssl_keyfile": global_args.ssl_keyfile,
  2320. }
  2321. )
  2322. print(
  2323. f"Starting Uvicorn server in single-process mode on {global_args.host}:{global_args.port}"
  2324. )
  2325. uvicorn.run(**uvicorn_config)
  2326. if __name__ == "__main__":
  2327. main()