1+ import os
2+ import time
3+ import uuid
4+ from pathlib import Path
5+ from typing import List , Optional
6+ from typing_extensions import TypedDict
7+
8+ from dotenv import load_dotenv
9+ from fastapi import FastAPI
10+ from pydantic import BaseModel
11+ from tqdm import tqdm
12+
13+ from langchain_openai import OpenAIEmbeddings , ChatOpenAI
14+ from langchain_chroma import Chroma
15+ from langchain_community .document_loaders import PyMuPDFLoader , Docx2txtLoader , TextLoader
16+ from langchain_text_splitters import RecursiveCharacterTextSplitter
17+ from langchain_core .prompts import ChatPromptTemplate
18+ from langchain_core .documents import Document
19+ from langgraph .graph import START , StateGraph
20+
21+ load_dotenv ()
22+ LLM_MODEL = os .getenv ("MODEL_LLM" )
23+ EMB_MODEL = os .getenv ("MODEL_EMBEDDING" )
24+ DOCS_DIR = 'exports'
25+ PERSIST_DIR = 'vector-store'
26+
27+ class State (TypedDict ):
28+ question : str
29+ context : List [Document ]
30+ answer : str
31+
32+ class RAGCore :
33+ def __init__ (self ):
34+ self .embeddings = OpenAIEmbeddings (model = EMB_MODEL )
35+ self .llm = ChatOpenAI (model = LLM_MODEL , temperature = 0.2 )
36+ self .vector_store = Chroma (
37+ collection_name = "collection_test" ,
38+ embedding_function = self .embeddings ,
39+ persist_directory = PERSIST_DIR ,
40+ )
41+ self .graph = self ._build_graph ()
42+
43+ def _build_graph (self ):
44+ prompt = ChatPromptTemplate .from_template (
45+ "You are an assistant for question-answering tasks. Use the following context to answer.\n "
46+ "Context: {context}\n Question: {question}\n Answer:"
47+ )
48+
49+ def retrieve (state : State ):
50+ # Optimized search: fetch top 5 relevant chunks
51+ docs = self .vector_store .similarity_search (state ["question" ], k = 5 )
52+ return {"context" : docs }
53+
54+ def generate (state : State ):
55+ docs_content = "\n \n " .join (doc .page_content for doc in state ["context" ])
56+ sources = list (set ([os .path .basename (doc .metadata .get ('source' , 'Unknown' )) for doc in state ["context" ]]))
57+ messages = prompt .invoke ({"question" : state ["question" ], "context" : docs_content })
58+ response = self .llm .invoke (messages )
59+ source_text = "\n \n **Sources:**\n - " + "\n - " .join (sources )
60+ final_answer = response .content + source_text
61+ return {"answer" : final_answer }
62+
63+ workflow = StateGraph (State )
64+ workflow .add_node ("retrieve" , retrieve )
65+ workflow .add_node ("generate" , generate )
66+ workflow .add_edge (START , "retrieve" )
67+ workflow .add_edge ("retrieve" , "generate" )
68+ return workflow .compile ()
69+
70+ def query (self , text : str ):
71+ return self .graph .invoke ({"question" : text })
72+
73+
74+ app = FastAPI (title = "AnythingLLM Custom Bridge" )
75+ engine = RAGCore ()
76+
77+ class ChatMessage (BaseModel ):
78+ role : str
79+ content : str
80+
81+ class ChatCompletionRequest (BaseModel ):
82+ messages : List [ChatMessage ]
83+ model : Optional [str ] = "custom-rag"
84+
85+ @app .post ("/v1/chat/completions" )
86+ async def chat_endpoint (request : ChatCompletionRequest ):
87+ user_msg = request .messages [- 1 ].content
88+ result = engine .query (user_msg )
89+
90+ return {
91+ "id" : f"chatcmpl-{ uuid .uuid4 ()} " ,
92+ "object" : "chat.completion" ,
93+ "created" : int (time .time ()),
94+ "model" : request .model ,
95+ "choices" : [{
96+ "message" : {"role" : "assistant" , "content" : result ["answer" ]},
97+ "finish_reason" : "stop" ,
98+ "index" : 0
99+ }]
100+ }
101+
102+ def run_ingestion ():
103+ if not os .path .exists (DOCS_DIR ): return
104+
105+ paths = list (Path (DOCS_DIR ).rglob ("*" ))
106+ new_docs = []
107+ for p in tqdm (paths , desc = "Processing files" ):
108+ if p .suffix .lower () == ".pdf" : new_docs .extend (PyMuPDFLoader (str (p )).load ())
109+ elif p .suffix .lower () == ".docx" : new_docs .extend (Docx2txtLoader (str (p )).load ())
110+ elif p .suffix .lower () in {".txt" , ".md" }: new_docs .extend (TextLoader (str (p )).load ())
111+
112+ if new_docs :
113+ splitter = RecursiveCharacterTextSplitter (chunk_size = 1000 , chunk_overlap = 150 )
114+ splits = splitter .split_documents (new_docs )
115+ engine .vector_store .add_documents (splits )
116+ print (f"Successfully indexed { len (splits )} chunks." )
117+
118+ if __name__ == "__main__" :
119+ import uvicorn
120+ run_ingestion ()
121+ uvicorn .run (app , host = "0.0.0.0" , port = 8000 )
0 commit comments