main.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import requests
  2. import logging
  3. import ftfy
  4. import sys
  5. from langchain_community.document_loaders import (
  6. AzureAIDocumentIntelligenceLoader,
  7. BSHTMLLoader,
  8. CSVLoader,
  9. Docx2txtLoader,
  10. OutlookMessageLoader,
  11. PyPDFLoader,
  12. TextLoader,
  13. UnstructuredEPubLoader,
  14. UnstructuredExcelLoader,
  15. UnstructuredMarkdownLoader,
  16. UnstructuredPowerPointLoader,
  17. UnstructuredRSTLoader,
  18. UnstructuredXMLLoader,
  19. YoutubeLoader,
  20. )
  21. from langchain_core.documents import Document
  22. from open_webui.retrieval.loaders.external_document import ExternalDocumentLoader
  23. from open_webui.retrieval.loaders.mistral import MistralLoader
  24. from open_webui.retrieval.loaders.datalab_marker import DatalabMarkerLoader
  25. from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
  26. logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
  27. log = logging.getLogger(__name__)
  28. log.setLevel(SRC_LOG_LEVELS["RAG"])
  29. known_source_ext = [
  30. "go",
  31. "py",
  32. "java",
  33. "sh",
  34. "bat",
  35. "ps1",
  36. "cmd",
  37. "js",
  38. "ts",
  39. "css",
  40. "cpp",
  41. "hpp",
  42. "h",
  43. "c",
  44. "cs",
  45. "sql",
  46. "log",
  47. "ini",
  48. "pl",
  49. "pm",
  50. "r",
  51. "dart",
  52. "dockerfile",
  53. "env",
  54. "php",
  55. "hs",
  56. "hsc",
  57. "lua",
  58. "nginxconf",
  59. "conf",
  60. "m",
  61. "mm",
  62. "plsql",
  63. "perl",
  64. "rb",
  65. "rs",
  66. "db2",
  67. "scala",
  68. "bash",
  69. "swift",
  70. "vue",
  71. "svelte",
  72. "ex",
  73. "exs",
  74. "erl",
  75. "tsx",
  76. "jsx",
  77. "hs",
  78. "lhs",
  79. "json",
  80. ]
  81. class TikaLoader:
  82. def __init__(self, url, file_path, mime_type=None, extract_images=None):
  83. self.url = url
  84. self.file_path = file_path
  85. self.mime_type = mime_type
  86. self.extract_images = extract_images
  87. def load(self) -> list[Document]:
  88. with open(self.file_path, "rb") as f:
  89. data = f.read()
  90. if self.mime_type is not None:
  91. headers = {"Content-Type": self.mime_type}
  92. else:
  93. headers = {}
  94. if self.extract_images == True:
  95. headers["X-Tika-PDFextractInlineImages"] = "true"
  96. endpoint = self.url
  97. if not endpoint.endswith("/"):
  98. endpoint += "/"
  99. endpoint += "tika/text"
  100. r = requests.put(endpoint, data=data, headers=headers)
  101. if r.ok:
  102. raw_metadata = r.json()
  103. text = raw_metadata.get("X-TIKA:content", "<No text content found>").strip()
  104. if "Content-Type" in raw_metadata:
  105. headers["Content-Type"] = raw_metadata["Content-Type"]
  106. log.debug("Tika extracted text: %s", text)
  107. return [Document(page_content=text, metadata=headers)]
  108. else:
  109. raise Exception(f"Error calling Tika: {r.reason}")
  110. class DoclingLoader:
  111. def __init__(self, url, file_path=None, mime_type=None, params=None):
  112. self.url = url.rstrip("/")
  113. self.file_path = file_path
  114. self.mime_type = mime_type
  115. self.params = params or {}
  116. def load(self) -> list[Document]:
  117. with open(self.file_path, "rb") as f:
  118. files = {
  119. "files": (
  120. self.file_path,
  121. f,
  122. self.mime_type or "application/octet-stream",
  123. )
  124. }
  125. params = {
  126. "image_export_mode": "placeholder",
  127. "table_mode": "accurate"
  128. }
  129. if self.params:
  130. if self.params.get("do_picture_description"):
  131. params["do_picture_description"] = self.params.get(
  132. "do_picture_description"
  133. )
  134. if self.params.get("ocr_engine") and self.params.get("ocr_lang"):
  135. params["ocr_engine"] = self.params.get("ocr_engine")
  136. params["ocr_lang"] = [
  137. lang.strip()
  138. for lang in self.params.get("ocr_lang").split(",")
  139. if lang.strip()
  140. ]
  141. endpoint = f"{self.url}/v1alpha/convert/file"
  142. r = requests.post(endpoint, files=files, data=params)
  143. if r.ok:
  144. result = r.json()
  145. document_data = result.get("document", {})
  146. text = document_data.get("md_content", "<No text content found>")
  147. metadata = {"Content-Type": self.mime_type} if self.mime_type else {}
  148. log.debug("Docling extracted text: %s", text)
  149. return [Document(page_content=text, metadata=metadata)]
  150. else:
  151. error_msg = f"Error calling Docling API: {r.reason}"
  152. if r.text:
  153. try:
  154. error_data = r.json()
  155. if "detail" in error_data:
  156. error_msg += f" - {error_data['detail']}"
  157. except Exception:
  158. error_msg += f" - {r.text}"
  159. raise Exception(f"Error calling Docling: {error_msg}")
  160. class Loader:
  161. def __init__(self, engine: str = "", **kwargs):
  162. self.engine = engine
  163. self.kwargs = kwargs
  164. def load(
  165. self, filename: str, file_content_type: str, file_path: str
  166. ) -> list[Document]:
  167. loader = self._get_loader(filename, file_content_type, file_path)
  168. docs = loader.load()
  169. return [
  170. Document(
  171. page_content=ftfy.fix_text(doc.page_content), metadata=doc.metadata
  172. )
  173. for doc in docs
  174. ]
  175. def _is_text_file(self, file_ext: str, file_content_type: str) -> bool:
  176. return file_ext in known_source_ext or (
  177. file_content_type and file_content_type.find("text/") >= 0
  178. )
  179. def _get_loader(self, filename: str, file_content_type: str, file_path: str):
  180. file_ext = filename.split(".")[-1].lower()
  181. if (
  182. self.engine == "external"
  183. and self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_URL")
  184. and self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_API_KEY")
  185. ):
  186. loader = ExternalDocumentLoader(
  187. file_path=file_path,
  188. url=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_URL"),
  189. api_key=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_API_KEY"),
  190. mime_type=file_content_type,
  191. )
  192. elif self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"):
  193. if self._is_text_file(file_ext, file_content_type):
  194. loader = TextLoader(file_path, autodetect_encoding=True)
  195. else:
  196. loader = TikaLoader(
  197. url=self.kwargs.get("TIKA_SERVER_URL"),
  198. file_path=file_path,
  199. mime_type=file_content_type,
  200. extract_images=self.kwargs.get("PDF_EXTRACT_IMAGES"),
  201. )
  202. elif (
  203. self.engine == "datalab_marker"
  204. and self.kwargs.get("DATALAB_MARKER_API_KEY")
  205. and file_ext
  206. in [
  207. "pdf",
  208. "xls",
  209. "xlsx",
  210. "ods",
  211. "doc",
  212. "docx",
  213. "odt",
  214. "ppt",
  215. "pptx",
  216. "odp",
  217. "html",
  218. "epub",
  219. "png",
  220. "jpeg",
  221. "jpg",
  222. "webp",
  223. "gif",
  224. "tiff",
  225. ]
  226. ):
  227. loader = DatalabMarkerLoader(
  228. file_path=file_path,
  229. api_key=self.kwargs["DATALAB_MARKER_API_KEY"],
  230. langs=self.kwargs.get("DATALAB_MARKER_LANGS"),
  231. use_llm=self.kwargs.get("DATALAB_MARKER_USE_LLM", False),
  232. skip_cache=self.kwargs.get("DATALAB_MARKER_SKIP_CACHE", False),
  233. force_ocr=self.kwargs.get("DATALAB_MARKER_FORCE_OCR", False),
  234. paginate=self.kwargs.get("DATALAB_MARKER_PAGINATE", False),
  235. strip_existing_ocr=self.kwargs.get(
  236. "DATALAB_MARKER_STRIP_EXISTING_OCR", False
  237. ),
  238. disable_image_extraction=self.kwargs.get(
  239. "DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION", False
  240. ),
  241. output_format=self.kwargs.get(
  242. "DATALAB_MARKER_OUTPUT_FORMAT", "markdown"
  243. ),
  244. )
  245. elif self.engine == "docling" and self.kwargs.get("DOCLING_SERVER_URL"):
  246. if self._is_text_file(file_ext, file_content_type):
  247. loader = TextLoader(file_path, autodetect_encoding=True)
  248. else:
  249. loader = DoclingLoader(
  250. url=self.kwargs.get("DOCLING_SERVER_URL"),
  251. file_path=file_path,
  252. mime_type=file_content_type,
  253. params={
  254. "ocr_engine": self.kwargs.get("DOCLING_OCR_ENGINE"),
  255. "ocr_lang": self.kwargs.get("DOCLING_OCR_LANG"),
  256. "do_picture_description": self.kwargs.get(
  257. "DOCLING_DO_PICTURE_DESCRIPTION"
  258. ),
  259. "picture_description_local": (
  260. '{\n'
  261. ' "repo_id": "HuggingFaceTB/SmolVLM-256M-Instruct",\n'
  262. ' "prompt": "Analyze the image and provide a comprehensive, detailed description. Identify all visible objects, their attributes, actions taking place, spatial relationships, and any contextual or inferred connections. Use clear, structured, and informative language suitable for downstream retrieval or knowledge extraction tasks."\n'
  263. '}'
  264. )
  265. },
  266. )
  267. elif (
  268. self.engine == "document_intelligence"
  269. and self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT") != ""
  270. and self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY") != ""
  271. and (
  272. file_ext in ["pdf", "xls", "xlsx", "docx", "ppt", "pptx"]
  273. or file_content_type
  274. in [
  275. "application/vnd.ms-excel",
  276. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  277. "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  278. "application/vnd.ms-powerpoint",
  279. "application/vnd.openxmlformats-officedocument.presentationml.presentation",
  280. ]
  281. )
  282. ):
  283. loader = AzureAIDocumentIntelligenceLoader(
  284. file_path=file_path,
  285. api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"),
  286. api_key=self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY"),
  287. )
  288. elif (
  289. self.engine == "mistral_ocr"
  290. and self.kwargs.get("MISTRAL_OCR_API_KEY") != ""
  291. and file_ext
  292. in ["pdf"] # Mistral OCR currently only supports PDF and images
  293. ):
  294. loader = MistralLoader(
  295. api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path
  296. )
  297. elif (
  298. self.engine == "external"
  299. and self.kwargs.get("MISTRAL_OCR_API_KEY") != ""
  300. and file_ext
  301. in ["pdf"] # Mistral OCR currently only supports PDF and images
  302. ):
  303. loader = MistralLoader(
  304. api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path
  305. )
  306. else:
  307. if file_ext == "pdf":
  308. loader = PyPDFLoader(
  309. file_path, extract_images=self.kwargs.get("PDF_EXTRACT_IMAGES")
  310. )
  311. elif file_ext == "csv":
  312. loader = CSVLoader(file_path, autodetect_encoding=True)
  313. elif file_ext == "rst":
  314. loader = UnstructuredRSTLoader(file_path, mode="elements")
  315. elif file_ext == "xml":
  316. loader = UnstructuredXMLLoader(file_path)
  317. elif file_ext in ["htm", "html"]:
  318. loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
  319. elif file_ext == "md":
  320. loader = TextLoader(file_path, autodetect_encoding=True)
  321. elif file_content_type == "application/epub+zip":
  322. loader = UnstructuredEPubLoader(file_path)
  323. elif (
  324. file_content_type
  325. == "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
  326. or file_ext == "docx"
  327. ):
  328. loader = Docx2txtLoader(file_path)
  329. elif file_content_type in [
  330. "application/vnd.ms-excel",
  331. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  332. ] or file_ext in ["xls", "xlsx"]:
  333. loader = UnstructuredExcelLoader(file_path)
  334. elif file_content_type in [
  335. "application/vnd.ms-powerpoint",
  336. "application/vnd.openxmlformats-officedocument.presentationml.presentation",
  337. ] or file_ext in ["ppt", "pptx"]:
  338. loader = UnstructuredPowerPointLoader(file_path)
  339. elif file_ext == "msg":
  340. loader = OutlookMessageLoader(file_path)
  341. elif self._is_text_file(file_ext, file_content_type):
  342. loader = TextLoader(file_path, autodetect_encoding=True)
  343. else:
  344. loader = TextLoader(file_path, autodetect_encoding=True)
  345. return loader