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.
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);}
semChatType="""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.""";semRouter.classify="Classify the message as RAG, QA, or VIDEO. If classification fails, default to QA.";semImageChat.respond_with_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.""";semVideoChat.respond_with_video="""Answer the user's message using the provided video and text. Always refer to the given video, answer relevant to the given video.""";semRagChat.respond="""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. """;semQAChat.respond="""Generate a helpful response to the user's message.""";implImageChat.chat{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};}implVideoChat.chat{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};}implRagChat.chat{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};}implQAChat.chat{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};}implupload_file.save_doc{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")};}"""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);}
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");}
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);}
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="uploads/user_session_123";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);docs=document_loader.load();foriinrange(min(3,len(docs))){doc=docs[i];source=doc.metadata.get('source','unknown');page=doc.metadata.get('page','unknown');}returndocs;}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="";foriinrange(len(results)){doc=results[i][0];score=results[i][1];page=doc.metadata.get('page');source=doc.metadata.get('source');chunk_txt=doc.page_content[0:400]iflen(doc.page_content)>400elsedoc.page_content;preview=chunk_txt[0:100]iflen(chunk_txt)>100elsechunk_txt;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}";}}
Your application uses Jac's Object Spatial Programming to create a clean, modular design:
Nodes represent different parts of your system (Router, Chat types, Sessions). Each node has specific responsibilities and capabilities.
Walkers move through your node network, carrying information and executing logic. They represent the actions your system can perform.
Mean Typed Programming (MTP) lets AI automatically classify and route requests, making your application intelligent without complex rule-based logic.
Implementation Separation: The server.jac file contains the high-level structure and logic, while server.impl.jac provides the detailed function implementations. Jac seamlessly imports the implementation file, allowing for clean separation of concerns.
The application consists of:
Document Processing Engine (tools.jac): Processes and searches documents using vector embeddings
Tool Server (mcp_server.jac): Exposes document and web search as MCP tools
Tool Client (mcp_client.jac): Interfaces with the tool server
Main Application (server.jac + server.impl.jac): Routes queries and manages conversations
Web Interface (client.jac): User-friendly Streamlit interface
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.