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

Ahmad W KhanAhmad W Khan
3 min read

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

ComponentTool / Framework
UIStreamlit
LLM InferenceOllama
EmbeddingsHuggingFace MiniLM
RAG FrameworkLangChain
Vector StoreChromaDB

Prerequisites

  1. Python 3.9+ installed

  2. Ollama installed and running

  3. 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

0
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

Ahmad W Khan
Ahmad W Khan