This guide will walk you through building a state-of-the-art Retrieval-Augmented Generation (RAG) chatbot using Jac Cloud, Jac-Streamlit, LangChain, ChromaDB, and modern LLMs. You’ll learn to:
- Upload and index your own documents (PDFs)
- Chat with an AI assistant that uses both your documents and LLMs
- Add advanced dialogue routing for smarter conversations
Document Upload & Ingestion: Upload PDFs, which are processed and indexed for semantic search.
Retrieval-Augmented Generation: Combines LLMs with document retrieval for context-aware answers.
Web Search Integration: Optionally augments responses with real-time web search results.
Streamlit Frontend: User-friendly chat interface.
Dialogue Routing: Classifies queries and routes them to the best model (RAG or QA).
Session Management: Maintains chat history and user sessions.
Project Structure:
- client.jac: Streamlit frontend for chat and document upload
- server.jac: Jac Cloud API server, session, LLM, and web search logic
- rag.jac: RAG engine for document loading, splitting, embedding, and vector search
- docs/: Example PDFs for testing
importstreamlitasst;importrequests;importbase64;defbootstrap_frontend(token:str){st.set_page_config(layout="wide");st.title("Welcome to your RAG Chatbot!");# Initialize chat historyif"messages"notinst.session_state{st.session_state.messages=[];}uploaded_file=st.file_uploader('Upload PDF');ifuploaded_file{file_b64=base64.b64encode(uploaded_file.read()).decode('utf-8');response=requests.post("http://localhost:8000/walker/upload_pdf",json={"file_name":uploaded_file.name,"file_data":file_b64},headers={"Authorization":f"Bearer {token}"});ifresponse.status_code==200{st.success(f"Uploaded {uploaded_file.name}");}else{st.error(f"Failed to upload {uploaded_file.name}");}}ifprompt:=st.chat_input("What is up?"){# Add user message to chat historyst.session_state.messages.append({"role":"user","content":prompt});# Display user message in chat message containerwithst.chat_message("user"){st.markdown(prompt);}# Display assistant response in chat message containerwithst.chat_message("assistant"){# Call walker APIresponse=requests.post("http://localhost:8000/walker/interact",json={"message":prompt,"session_id":"123"},headers={"Authorization":f"Bearer {token}"});ifresponse.status_code==200{response=response.json();print("response is",response);st.write(response["reports"][0]["response"]);# Add assistant response to chat historyst.session_state.messages.append({"role":"assistant","content":response["reports"][0]["response"]});}}}}withentry{INSTANCE_URL="http://localhost:8000";TEST_USER_EMAIL="test@mail.com";TEST_USER_PASSWORD="password";response=requests.post(f"{INSTANCE_URL}/user/login",json={"email":TEST_USER_EMAIL,"password":TEST_USER_PASSWORD});ifresponse.status_code!=200{# Try registering the user if login failsresponse=requests.post(f"{INSTANCE_URL}/user/register",json={"email":TEST_USER_EMAIL,"password":TEST_USER_PASSWORD});assertresponse.status_code==201;response=requests.post(f"{INSTANCE_URL}/user/login",json={"email":TEST_USER_EMAIL,"password":TEST_USER_PASSWORD});assertresponse.status_code==200;}token=response.json()["token"];print("Token:",token);bootstrap_frontend(token);}
importos;importfromlangchain_community.document_loaders{PyPDFDirectoryLoader,PyPDFLoader}importfromlangchain_text_splitters{RecursiveCharacterTextSplitter}importfromlangchain.schema.document{Document}importfromlangchain_openai{OpenAIEmbeddings}importfromlangchain_community.vectorstores.chroma{Chroma}objRagEngine{hasfile_path:str="docs";haschroma_path:str="chroma";defpostinit{ifnotos.path.exists(self.file_path){os.makedirs(self.file_path);}documents:list=self.load_documents();chunks:list=self.split_documents(documents);self.add_to_chroma(chunks);print("Documents loaded from",self.file_path);}defload_documents{document_loader=PyPDFDirectoryLoader(self.file_path);print("Loading documents from",document_loader);print("Document loader is",document_loader.load());returndocument_loader.load();}defload_document(file_path:str){loader=PyPDFLoader(file_path);returnloader.load();}defadd_file(file_path:str){documents=self.load_document(file_path);chunks=self.split_documents(documents);self.add_to_chroma(chunks);}defsplit_documents(documents:list[Document]){text_splitter=RecursiveCharacterTextSplitter(chunk_size=800,chunk_overlap=80,length_function=len,is_separator_regex=False);returntext_splitter.split_documents(documents);}defget_embedding_function{embeddings=OpenAIEmbeddings();returnembeddings;}defadd_chunk_id(chunks:str){last_page_id=None;current_chunk_index=0;forchunkinchunks{source=chunk.metadata.get('source');page=chunk.metadata.get('page');current_page_id=f'{source}:{page}';ifcurrent_page_id==last_page_id{current_chunk_index+=1;}else{current_chunk_index=0;}chunk_id=f'{current_page_id}:{current_chunk_index}';last_page_id=current_page_id;chunk.metadata['id']=chunk_id;}returnchunks;}defadd_to_chroma(chunks:list[Document]){db=Chroma(persist_directory=self.chroma_path,embedding_function=self.get_embedding_function());chunks_with_ids=self.add_chunk_id(chunks);existing_items=db.get(include=[]);existing_ids=set(existing_items['ids']);new_chunks=[];forchunkinchunks_with_ids{ifchunk.metadata['id']notinexisting_ids{new_chunks.append(chunk);}}iflen(new_chunks){print('adding new documents');new_chunk_ids=[chunk.metadata['id']forchunkinnew_chunks];db.add_documents(new_chunks,ids=new_chunk_ids);}else{print('no new documents to add');}}defget_from_chroma(query:str,chunck_nos:int=5){db=Chroma(persist_directory=self.chroma_path,embedding_function=self.get_embedding_function());results=db.similarity_search_with_score(query,k=chunck_nos);returnresults;}}
importfrommtllm.llms{OpenAI}importfromrag{RagEngine}importos;importbase64;importrequests;globrag_engine:RagEngine=RagEngine();globllm=OpenAI(model_name='gpt-4o');globSERPER_API_KEY:str=os.getenv('SERPER_API_KEY','');objWebSearch{hasapi_key:str=SERPER_API_KEY;hasbase_url:str="https://google.serper.dev/search";defsearch(query:str){headers={"X-API-KEY":self.api_key,"Content-Type":"application/json"};payload={"q":query};resp=requests.post(self.base_url,headers=headers,json=payload);ifresp.status_code==200{data=resp.json();summary="";results=data.get("organic",[])ifisinstance(data,dict)else[];forrinresults[:3]{summary+=f"{r.get('title','')}: {r.get('link','')}\n";ifr.get('snippet'){summary+=f"{r['snippet']}\n";}}returnsummary;}returnf"Serper request failed: {resp.status_code}";}}globweb_search:WebSearch=WebSearch();nodeSession{hasid:str;haschat_history:list[dict];hasstatus:int=1;defrespond(message:str,chat_history:str,agent_role:str,context:str)->strbyllm();}walkerinteract{hasmessage:str;hassession_id:str;caninit_sessionwith`rootentry{visit[-->](`?Session)(?id==self.session_id)else{session_node=here++>Session(id=self.session_id,chat_history=[],status=1);print("Session Node Created");visitsession_node;}}canchatwithSessionentry{here.chat_history.append({"role":"user","content":self.message});docs=rag_engine.get_from_chroma(query=self.message);web=web_search.search(query=self.message);context={"docs":docs,"web":web};response=here.respond(message=self.message,chat_history=here.chat_history,agent_role="You are a conversation agent designed to help users with their queries based on the documents provided and web search results",context=context);here.chat_history.append({"role":"assistant","content":response});report{"response":response};}}walkerupload_pdf{hasfile_name:str;hasfile_data:str;cansave_docwith`rootentry{ifnotos.path.exists(rag_engine.file_path){os.makedirs(rag_engine.file_path);}file_path=os.path.join(rag_engine.file_path,self.file_name);data=base64.b64decode(self.file_data.encode('utf-8'));withopen(file_path,'wb')asf{f.write(data);}rag_engine.add_file(file_path);report{"status":"uploaded"};}}
The frontend is built with Jac-Streamlit and handles authentication, PDF upload, and chat. Here’s how it works:
Authentication and Token Handling:
response=requests.post(f"{INSTANCE_URL}/user/login",json={"email":TEST_USER_EMAIL,"password":TEST_USER_PASSWORD});ifresponse.status_code!=200{# Try registering the user if login failsresponse=requests.post(f"{INSTANCE_URL}/user/register",json={"email":TEST_USER_EMAIL,"password":TEST_USER_PASSWORD});...}token=response.json()["token"];
- The app tries to log in a test user. If not found, it registers and logs in, then retrieves the token for API calls.
- Lets users upload PDFs, which are base64-encoded and sent to the backend for processing.
Chat Logic:
ifprompt:=st.chat_input("What is up?"){st.session_state.messages.append({"role":"user","content":prompt});...response=requests.post("http://localhost:8000/walker/interact",...);...st.session_state.messages.append({"role":"assistant","content":response["reports"][0]["response"]});}
- Captures user input, sends it to the backend, and displays both user and assistant messages in the chat UI.
- Each user session has a unique ID and chat history. The respond method uses an LLM to generate answers, optionally using context from documents and web search.