This step-by-step guide will walk you through building a modern chatbot that can chat with your documents, images, and videos. By the end, you'll have a working multimodal AI assistant and understand how to use Jac's unique programming features to build intelligent applications.
importos;importrequests;importfromlangchain_community.document_loaders{PyPDFDirectoryLoader,PyPDFLoader}importfromlangchain_text_splitters{RecursiveCharacterTextSplitter}importfromlangchain.schema.document{Document}importfromlangchain_openai{OpenAIEmbeddings}importfromlangchain_chroma{Chroma}globSERPER_API_KEY:str=os.getenv('SERPER_API_KEY','');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);}defload_documents{document_loader=PyPDFDirectoryLoader(self.file_path);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;}defsearch(query:str,chunck_nos:int=5){results=self.get_from_chroma(query=query,chunck_nos=chunck_nos);summary="";for(doc,score)inresults{page=doc.metadata.get('page');source=doc.metadata.get('source');chunk_txt=doc.page_content[:400];summary+=f"{source} page {page}: {chunk_txt}\n";}returnsummary;}}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}";}}
This engine is the foundation of your chatbot. It processes your uploaded documents, splits them into chunks, creates embeddings, and stores them for efficient search. Let's break down what it does:
Document Processing: Reads PDFs and text files, extracting their content
Text Chunking: Splits large documents into smaller, searchable pieces
Vector Embeddings: Converts text into numerical representations for semantic search
Storage: Uses ChromaDB to store and index your documents
importsys;importos;importfromtools{RagEngine,WebSearch}importfrommcp.server.fastmcp.tools{Tool}importfrommcp.server.fastmcp{FastMCP}importtyping;globrag_engine:RagEngine=RagEngine();globweb_search:WebSearch=WebSearch();withentry{mcp=FastMCP(name="RAG-MCP",port=8899);}defresolve_hints(fn:typing.Callable)->typing.Callable{fn.__annotations__=typing.get_type_hints(fn,include_extras=True);returnfn;}@mcp.tool(name="search_docs")@resolve_hintsasyncdeftool_search_docs(query:str)->str{returnrag_engine.search(query);}@mcp.tool(name="search_web")@resolve_hintsasyncdeftool_search_web(query:str)->str{web_search_results=web_search.search(query);ifnotweb_search_results{return"Mention No results found for the web search";}returnweb_search_results;}withentry{mcp.run("streamable-http");}
This server exposes two tools: one for searching your uploaded documents and another for web search. The FastMCP framework makes it easy to create these modular tools that your main application can use.
importanyio;importlogging;importmcp;importos;importfrommcp.client{streamable_http}withentry{logger=logging.getLogger(__name__);logger.setLevel(logging.INFO);logging.basicConfig(level=logging.INFO);}globMCP_SERVER_URL=os.getenv('MCP_SERVER_URL','http://localhost:8899/mcp');deflist_mcp_tools()->list[dict]{asyncdef_list()->list{asyncwithstreamable_http.streamablehttp_client(MCP_SERVER_URL)as(read,write,_){asyncwithmcp.ClientSession(read,write)assess{awaitsess.initialize();tools=awaitsess.list_tools();structured_tools=[];logger.info(f"available tools 1:{tools.tools}");tool_names=[tool.namefortoolintools.tools];logger.info(f"tool list of names:{tool_names}");returntool_names;}}}returnanyio.run(_list);}defcall_mcp_tool(name:str,arguments:dict)->str{asyncdef_call()->str{asyncwithstreamable_http.streamablehttp_client(MCP_SERVER_URL)as(read,write,_){asyncwithmcp.ClientSession(read,write)assess{awaitsess.initialize();result=awaitsess.call_tool(name=name,arguments=arguments);ifresult.isError{returnf"'MCP error: '{result.error.message}";}if(result.structuredContentand('result'inresult.structuredContent)){returnresult.structuredContent['result'];}if(result.contentand(len(result.content)>0)){returnresult.content[0].text;}}}}returnanyio.run(_call);}
This client handles the communication between your main application and the tools.
Step 5: Create the Main Application with Object Spatial Programming#
Now for the core application logic. Create server.jac:
importsys;importfrommtllm.llm{Model}importfrommtllm.types{Image,Video,Text}importfromtools{RagEngine}importos;importbase64;importrequests;importanyio;importmcp_client;globrag_engine:RagEngine=RagEngine();globllm=Model(model_name='gpt-4o-mini',verbose=True);globMCP_SERVER_URL:str=os.getenv('MCP_SERVER_URL','http://localhost:8899/mcp');"""ChatType enum defines the types of chat interactions. ChatType must be one of:- RAG: For interactions that require document retrieval.- QA: For interactions that does not require document retrieval, or image-video-related questions.- IMAGE: For interactions involving image analysis or anything related to images, and follow up questions.- VIDEO: For interactions involving video analysis or video-related questions."""enumChatType{RAG="RAG",QA="QA",IMAGE="IMAGE",VIDEO="VIDEO"}nodeRouter{"Classify the message as RAG, QA, or VIDEO. If classification fails, default to QA."defclassify(message:str)->ChatTypebyllm(method="Reason",temperature=0.8);}nodeChat{haschat_type:ChatType;}"""Get available MCP tool names."""deflist_mcp_tools()->list[str]{returnmcp_client.list_mcp_tools();}"""Use MCP tool to perform actions.name must be one of available tools from list_mcp_tools(), do not make up any tool names.Example input for `use_mcp_tool`:{"name": "tool_name", "arguments": {"query": "your query"}}"""defuse_mcp_tool(name:str,arguments:dict[str,str])->str{returnmcp_client.call_mcp_tool(name=name,arguments=arguments);}walkerinfer{hasmessage:str;haschat_history:list[dict];hasfile_path:str="";caninit_routerwith`rootentry{visit[-->](`?Router)else{router_node=here++>Router();router_node++>RagChat();router_node++>QAChat();router_node++>ImageChat();router_node++>VideoChat();visitrouter_node;}}canroutewithRouterentry{classification=here.classify(message=self.message);print("Routing message:",self.message,"to chat type:",classification);visit[-->](`?Chat)(?chat_type==classification);}}nodeImageChat(Chat){haschat_type:ChatType=ChatType.IMAGE;"""Answer the user's message(text) by referring to the provided image. Always refer to the given image, answer relevant to the given image."""defrespond_with_image(img:Image,text:Text,chat_history:list[dict])->strbyllm(tools=([use_mcp_tool,list_mcp_tools]));canchatwithinferentry{img_path=visitor.file_path;response=self.respond_with_image(img=Image(img_path),text=visitor.message,chat_history=visitor.chat_history);visitor.chat_history.append({"role":"assistant","content":response});self.chat_history=visitor.chat_history;visitor.response=response;report{"response":response,"chat_history":visitor.chat_history};}}nodeVideoChat(Chat){haschat_type:ChatType=ChatType.VIDEO;"""Answer the user's message using the provided video and text. Always refer to the given video, answer relevant to the given video."""defrespond_with_video(video:Video,text:Text,chat_history:list[dict])->strbyllm(method="Chain-of-Thoughts");canchatwithinferentry{video_path=visitor.file_path;response=self.respond_with_video(video=Video(video_path),text=visitor.message,chat_history=visitor.chat_history);visitor.chat_history.append({"role":"assistant","content":response});self.chat_history=visitor.chat_history;visitor.response=response;report{"response":response,"chat_history":visitor.chat_history};}}nodeRagChat(Chat){haschat_type:ChatType=ChatType.RAG;"""Generate a helpful response to the user's message. Use available mcp tool when needed.Use list_mcp_tools to find out what are the available tools. Always pass arguments as a flat dictionary (e.g., {\"query\": \"Your search query\"}), never as a list or schema_dict_wrapper. """defrespond(message:str,chat_history:list[dict])->strbyllm(method="ReAct",tools=([list_mcp_tools,use_mcp_tool]),messages=chat_history,max_react_iterations=6);canchatwithinferentry{response=self.respond(message=visitor.message,chat_history=visitor.chat_history,);visitor.chat_history.append({"role":"assistant","content":response});self.chat_history=visitor.chat_history;visitor.response=response;report{"response":response,"chat_history":visitor.chat_history};}}nodeQAChat(Chat){haschat_type:ChatType=ChatType.QA;"""Generate a helpful response to the user's message. Use available mcp tool when needed. Always pass arguments as a flat dictionary (e.g., {\"query\": \"Your search query\"}), never as a list or schema_dict_wrapper. """defrespond(message:str,chat_history:list[dict])->strbyllm(method="ReAct",tools=([use_mcp_tool,list_mcp_tools]),messages=chat_history,max_react_iterations=6);canchatwithinferentry{response=self.respond(message=visitor.message,chat_history=visitor.chat_history,);visitor.chat_history.append({"role":"assistant","content":response});self.chat_history=visitor.chat_history;visitor.response=response;report{"response":response,"chat_history":visitor.chat_history};}}walkerinteract{hasmessage:str;hassession_id:str;haschat_history:list[dict]=[];hasfile_path:str="";caninit_sessionwith`rootentry{visit[-->](`?Session)(?id==self.session_id)else{session_node=here++>Session(id=self.session_id,chat_history=[],file_path=self.file_path,status=1);print("Session Node Created");visitsession_node;}}}nodeSession{hasid:str;haschat_history:list[dict];hasstatus:int=1;hasfile_path:str="";canchatwithinteractentry{visitor.chat_history=self.chat_history;visitor.chat_history.append({"role":"user","content":visitor.message});response=infer(message=visitor.message,chat_history=self.chat_history,file_path=visitor.file_path)spawnroot;visitor.chat_history.append({"role":"assistant","content":response.response});self.chat_history=visitor.chat_history;report{"response":response.response};}}walkerupload_file{hasfile_name:str;hasfile_data:str;hassession_id:str;cansave_docwith`rootentry{upload_dir=os.path.join("uploads",self.session_id);ifnotos.path.exists(upload_dir){os.makedirs(upload_dir);}file_path=os.path.join(upload_dir,self.file_name);data=base64.b64decode(self.file_data.encode('utf-8'));withopen(file_path,'wb')asf{f.write(data);}# Only add text-based documents to rag_enginelower_name=self.file_name.lower();iflower_name.endswith(".pdf")orlower_name.endswith(".txt"){rag_engine.add_file(file_path);}report{"status":"uploaded","file_path":file_path,"added_to_rag":lower_name.endswith(".pdf")orlower_name.endswith(".txt")};}}
Let's break down what we just built:
Router Node: This is the brain of your application. It uses Mean Typed Programming (MTP) to automatically classify user questions and route them to the right specialist.
Specialized Chat Nodes: Each type of question gets its own expert:
RagChat: Handles document-based questions
QAChat: Manages general questions and web search
ImageChat: Processes image-related conversations
VideoChat: Handles video discussions
Session Management: The Session node keeps track of each user's conversation history and uploaded files.
Walkers: These handle the flow of your application:
infer: Routes questions to the right chat node
interact: Manages conversations and maintains session state
importstreamlitasst;importrequests;importbase64;defbootstrap_frontend(token:str){st.set_page_config(layout="wide");st.title("Welcome to your Jac MCP Chatbot!");# Initialize session stateif"messages"notinst.session_state{st.session_state.messages=[];}if"session_id"notinst.session_state{st.session_state.session_id="user_session_123";}uploaded_file=st.file_uploader('Upload File (PDF, TXT, Image, or Video)');ifuploaded_file{file_b64=base64.b64encode(uploaded_file.read()).decode('utf-8');file_extension=uploaded_file.name.lower().split('.')[-1];file_type=uploaded_file.typeor'';supported_types=['pdf','txt','png','jpg','jpeg','webp','mp4','avi','mov'];iffile_extensionnotinsupported_typesandnot(file_type.startswith('image')orfile_type.startswith('video')){st.error(f"Unsupported file type: {file_typeor'unknown'}. Please upload PDF, TXT, Image, or Video files.");return;}# Use upload_pdf walker endpoint for all uploads, saving in uploads/{session_id}payload={"file_name":uploaded_file.name,"file_data":file_b64,"session_id":st.session_state.session_id};response=requests.post("http://localhost:8000/walker/upload_file",json=payload,headers={"Authorization":f"Bearer {token}"});ifresponse.status_code==200{st.success(f"File '{uploaded_file.name}' uploaded and saved to uploads/{st.session_state.session_id}.");# Track last uploaded file path in session statest.session_state.last_uploaded_file_path=f"uploads/{st.session_state.session_id}/{uploaded_file.name}";}else{st.error(f"Failed to process {uploaded_file.name}: {response.text}");}}# Display chat messages from history on app rerunformessageinst.session_state.messages{withst.chat_message(message["role"]){st.markdown(message["content"]);}}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"){withst.spinner("Thinking..."){# Call walker APIpayload={"message":prompt,"session_id":st.session_state.session_id};# If a file was uploaded, include its pathif"last_uploaded_file_path"inst.session_state{payload["file_path"]=st.session_state.last_uploaded_file_path;}response=requests.post("http://localhost:8000/walker/interact",json=payload,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);}
This creates a clean, intuitive interface where users can register, log in, upload files, and chat with the AI.
POST /walker/upload_file — Upload files (requires authentication)
POST /walker/interact — Chat with the AI (requires authentication)
Visit http://localhost:8000/docs to see the full API documentation.
You now have the foundation to build sophisticated AI applications using Jac's unique programming paradigms. The combination of Object Spatial Programming, Mean Typed Programming, and modular tool architecture gives you a solid base for creating intelligent, scalable applications.