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

Search for a command to run...

No comments yet. Be the first to comment.
There's a question that most people feel but almost nobody says out loud. Not because it's complicated. Because saying it threatens everything built on top of not saying it. The question is simple. Wh

Public discourse around personal finance in India increasingly relies on lifestyle narratives, absolute numbers, and loosely imported benchmarks. Concepts such as middle class, financial security, high income, or wealthy are often used without refere...

By Ahmad W Khan (interactive at https://ahmadwkhan.com/india-work-landscape) Summary (TL;DR): India’s labour market is vast, diversified, and uneven. Government roles remain tenure‑rich but hard to enter; healthcare and licensed professional practice...

Audience: Intermediate PHP devs (comfortable with OOP, Composer, basic MVC) who are new/rusty with SymfonyOS Assumptions: macOS/Linux primary; Windows notes included (PowerShell + WSL2)Target PHP & Symfony: PHP 8.2+ and Symfony 7.3.x (current stable ...

Money, unlike geography, is invisible.We know where a country starts and ends on a map. But wealth and income? They’re like air currents, everywhere, yet hard to see. Percentiles help make them visible: where do you sit compared to your neighbors, yo...

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

| Component | Tool / Framework |
| UI | Streamlit |
| LLM Inference | Ollama |
| Embeddings | HuggingFace MiniLM |
| RAG Framework | LangChain |
| Vector Store | ChromaDB |
Python 3.9+ installed
Ollama installed and running
A terminal and a code editor like VS Code
mkdir friday-ai && cd friday-ai
python3 -m venv venv
source venv/bin/activate
pip install streamlit langchain langchain-community langchain-huggingface chromadb
pip install unstructured openai-whisper watchdog chardet
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
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)
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()
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