How to Build a Personal, Private, File-Aware AI Assistant from Scratch


Imagine a digital companion that lives on your machine, understands your documents, remembers past conversations, and answers your questions intelligently—all without sending data to the cloud.
This is FRIDAY: your offline, private, file-aware AI assistant.
In this post we’ll learn how to build it step by step, using only open-source tools.
What You'll Build
A fully local AI assistant that:
Chats with you based on your files (notes, books, logs)
Remembers conversations by thread
Summarizes long texts chapter by chapter
Works entirely offline
Embeds files intelligently and reuses them
Tech Stack
Component | Tool / Framework |
UI | Streamlit |
LLM Inference | Ollama |
Embeddings | HuggingFace MiniLM |
RAG Framework | LangChain |
Vector Store | ChromaDB |
Prerequisites
Python 3.9+ installed
Ollama installed and running
A terminal and a code editor like VS Code
Step 1: Project Setup
Create folder structure:
mkdir friday-ai && cd friday-ai
python3 -m venv venv
source venv/bin/activate
Install dependencies:
pip install streamlit langchain langchain-community langchain-huggingface chromadb
pip install unstructured openai-whisper watchdog chardet
Step 2: File Embedding Utility
Create embed_utils.py
:
import hashlib, os, json
from pathlib import Path
from langchain.document_loaders import TextLoader, UnstructuredPDFLoader
from langchain.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
UPLOAD_DIR = "my_context/uploads"
HASH_FILE = "file_hash_cache.json"
embedding = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
def get_hash(path):
return hashlib.md5(path.read_bytes()).hexdigest()
def load_hashes():
return json.load(open(HASH_FILE)) if os.path.exists(HASH_FILE) else {}
def save_hashes(h):
with open(HASH_FILE, "w") as f:
json.dump(h, f, indent=2)
def embed_uploaded_documents():
docs, updated, cache = [], False, load_hashes()
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
for file in Path(UPLOAD_DIR).glob("*.*"):
ext = file.suffix.lower()
if ext not in [".txt", ".pdf"]: continue
h = get_hash(file)
if cache.get(str(file)) == h: continue
loader = TextLoader(str(file)) if ext == ".txt" else UnstructuredPDFLoader(str(file))
chunks = splitter.split_documents(loader.load())
for c in chunks:
c.metadata["source"] = str(file.name)
docs.extend(chunks)
cache[str(file)] = h
updated = True
if docs:
Chroma.from_documents(docs, embedding=embedding, persist_directory="db").persist()
save_hashes(cache)
return updated
Step 3: Chat Memory and Summarizer
Create summarizer_utils.py
:
import re
from langchain.llms import Ollama
def split_chapters(text):
return re.split(r"\nChapter\s+\d+.*\n", text, flags=re.IGNORECASE)
def summarize_chunk(text):
llm = Ollama(model="phi3:mini")
prompt = f"Summarize this chapter in 3 bullet points:\n\n{text[:3000]}"
return llm(prompt)
Step 4: Build the UI
Create app.py
:
import os, json
from pathlib import Path
import streamlit as st
from datetime import datetime
from langchain.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.llms import Ollama
from langchain_huggingface import HuggingFaceEmbeddings
from embed_utils import embed_uploaded_documents
from summarizer_utils import summarize_chunk, split_chapters
UPLOAD_DIR = "my_context/uploads"
VECTOR_DB_PATH = "db"
THREADS_FILE = "chat_threads.json"
st.set_page_config(page_title="Friday AI", layout="wide")
if "chat_sessions" not in st.session_state:
st.session_state.chat_sessions = json.load(open(THREADS_FILE)) if os.path.exists(THREADS_FILE) else {}
with st.sidebar:
st.title("Friday AI")
uploaded = st.file_uploader("Upload PDF/TXT", type=["pdf", "txt"], accept_multiple_files=True)
if uploaded:
os.makedirs(UPLOAD_DIR, exist_ok=True)
for file in uploaded:
with open(os.path.join(UPLOAD_DIR, file.name), "wb") as f:
f.write(file.read())
st.success("Uploaded")
if embed_uploaded_documents():
st.success("Files embedded.")
threads = list(st.session_state.chat_sessions.keys())
selected_thread = st.selectbox("Chat Thread", threads + ["➕ New Thread"])
if selected_thread == "➕ New Thread":
name = st.text_input("Name new thread")
if st.button("Create") and name:
st.session_state.chat_sessions[name] = []
selected_thread = name
current = selected_thread
if st.button("🧹 Clear Thread"):
st.session_state.chat_sessions[current] = []
json.dump(st.session_state.chat_sessions, open(THREADS_FILE, "w"))
st.rerun()
summarize_file = st.selectbox("Summarize File", [f.name for f in Path(UPLOAD_DIR).glob("*.*")])
if st.button("Summarize Chapters"):
with open(Path(UPLOAD_DIR)/summarize_file, encoding="utf-8", errors="replace") as f:
text = f.read()
for i, ch in enumerate(split_chapters(text)):
st.markdown(f"### Chapter {i+1}")
st.markdown(summarize_chunk(ch))
retriever = Chroma(persist_directory=VECTOR_DB_PATH, embedding_function=HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")).as_retriever(search_kwargs={"k": 5})
qa_chain = RetrievalQA.from_chain_type(llm=Ollama(model="phi3:mini"), retriever=retriever)
st.title(f"🧵 Chat: {current}")
chat = st.session_state.chat_sessions.get(current, [])
for msg in chat:
st.markdown(f"**{msg['role'].capitalize()}**: {msg['text']}")
query = st.text_input("Ask a question")
if query:
past = "\n".join(f"{m['role'].capitalize()}: {m['text']}" for m in chat[-4:])
prompt = f"You are Friday, my assistant.\n\n{past}\nUser: {query}"
answer = qa_chain.run(prompt)
chat.append({"role": "user", "text": query})
chat.append({"role": "ai", "text": answer})
st.session_state.chat_sessions[current] = chat
json.dump(st.session_state.chat_sessions, open(THREADS_FILE, "w"))
st.rerun()
Final Result
You now have:
A UI for uploading and embedding your personal files
Multi-threaded chat assistant that remembers conversations
Summarizer that can extract insights from chapters
Smart local vector search
All 100% private and offline
Subscribe to my newsletter
Read articles from Ahmad W Khan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
