Giving My Portfolio a Memory: Building an Automated RAG Assistant#
If you’ve poked around my site lately, you might have noticed a small chat bubble popping up in the bottom right corner.
Instead of letting this portfolio just be a static collection of markdown files and devlogs sitting on GitHub Pages, I wanted to try something a bit more interactive: an embedded AI assistant that actually knows what’s on this site and can answer questions about my background, devlogs, and projects in real time.
Naturally, I didn’t want to manually copy-paste every blog post or devlog into an LLM platform every time I write an update. The goal was simple: push a new markdown file to GitHub, let CI handle the heavy lifting, and automatically update a Retrieval-Augmented Generation (RAG) knowledge base.
Sounds straightforward in theory. In practice? Well, let’s just say I bumped my toes on a few sharp corners before getting it right. Here is how the whole setup works, how it fits together, and the lessons learned from debugging the pipeline.
A Quick Note on Process#
Before diving in, a bit of transparency: this project is part of a school assignment about “AI Driven Application” or “AIDA” for short, so I leaned on Google’s Gemini CLI to do the actual code writing — including the Python sync script that talks to Dify’s API. My own role was steering the ship: defining the implementation plan, deciding on the architecture, and doing the troubleshooting whenever something broke (which, as you’ll see below, was often). So when I say “I fixed X” below, read that as “I diagnosed the problem and directed Gemini CLI to the fix” rather than me hand-typing every line myself.
The Architecture: How It Works#
The entire pipeline connects three main pieces:
- Hugo (GitHub Pages): Where all the content lives as plain Markdown files (
content/**/*.md). - GitHub Actions: An automated workflow that watches for changes in the
content/folder, detects new or edited files, and talks to the API. - Dify (Knowledge Base & Chatbot): An open-source LLM app platform that manages the document chunking, hybrid vector search + reranking, and hosts the chatbot widget.
[ Git Push to main ]
│
▼
[ GitHub Action: sync-dify.yml ]
│
├── Calculates SHA-256 Hashes (skips unchanged posts)
└── Uploads new/modified .md files via Multipart API
│
▼
[ Dify Knowledge Base ]
(Parent-Child Chunking + Hybrid Search)
│
▼
[ Floating Web Widget on Hugo ]The Roadblocks (and How I Fixed Them)#
Setting up an API pipeline between GitHub Actions and a specialized RAG platform like Dify turned out to be a great exercise in troubleshooting. Here are the main hurdles I ran into:
1. The “Zero Words Found” Mystery#
When I first wrote the sync script, I used Dify’s plain-text upload option. The files showed up in the dataset, but every single document listed 0 words and had empty segments — Dify had accepted the files but hadn’t actually processed them into searchable text.
- Why: I’d set my Dify dataset up to use a more advanced chunking style, where each document is split into a “parent” section with smaller “child” pieces nested inside it (useful for keeping context together during search). The plain-text upload option doesn’t know how to build that structure on its own — it needs to be told explicitly how to do it.
- The Fix: I switched to Dify’s file-upload option instead, and told it directly how to build that parent/child structure — roughly how big the parent chunks should be, and how big the smaller pieces inside them should be. Once I specified that, Dify’s parser kicked in properly and all the text segments lit up.
2. Picky API Requirements#
Dify is strict about exactly what information it expects in each request, and that expectation changes depending on how your dataset is set up. Over a couple of runs, I hit errors telling me things like: a required processing setting was missing, the format I was sending didn’t match the format my dataset expected, or a filename contained characters it didn’t like (this last one happened because I was sending full folder paths like content/posts/... instead of just the filename).
- The Fix: I cleaned up filenames so only the plain filename got sent (no folder paths), filled in every setting Dify expected — including a small “clean up whitespace” instruction — and made sure I was explicitly telling it to expect the parent/child format I’d configured. Once the request matched what Dify expected, the errors disappeared.
3. Rate Limits & Timeouts#
My repository has 20+ markdown files across the devlog series and blog posts. Uploading all of them back-to-back triggered Dify’s rate limiter — it started rejecting requests for coming in too fast — and a few uploads also timed out while Dify was verifying things on its end.
- The Fix: I added a short pause (a few seconds) between each upload, and built in a retry system: if a request gets rejected for being too fast, it waits a bit and tries again, waiting longer each time it fails. That way a temporary hiccup doesn’t crash the whole script.
4. Smart Caching (No More Redundant Uploads)#
Re-uploading all 20+ files on every single commit was wasteful, and it also made the rate-limit problem above worse.
- The Fix: I added a caching step that fingerprints each file (a hash, basically a unique code generated from the file’s contents) and remembers it between CI runs. When the workflow runs, it compares each file’s current fingerprint against the saved one. If nothing changed, it just prints
→ Skipping unchanged: [filename]and moves on. Only genuinely new or edited posts get pushed to Dify.
Embedding the Chatbot into Hugo#
Once the knowledge base was syncing reliably, getting the chatbot onto the site was surprisingly clean thanks to Hugo’s templating system and the Blowfish theme.
Blowfish has a built-in partial hook called extend-footer.html. By creating layouts/partials/extend-footer.html, I injected Dify’s lightweight embed script:
<script>
window.difyChatbotConfig = {
token: 'YOUR_DIFY_TOKEN',
baseUrl: 'https://udify.app'
}
</script>
<script
src="https://udify.app/embed.min.js"
id="YOUR_DIFY_TOKEN"
defer>
</script>
<style>
#dify-chatbot-bubble-button {
background-color: #1C64F2 !important;
}
#dify-chatbot-bubble-window {
width: 24rem !important;
height: 40rem !important;
}
</style>Whenever Hugo compiles the site for GitHub Pages, this footer partial is automatically included across all pages without having to touch theme files directly.
Tuning the System Prompt for Broad vs. Specific Questions#
One interesting thing about RAG is how it handles different types of questions:
- Specific queries (“What did Jesper do in week 9?”) work amazingly well out of the box, because the search only needs to find and pull one or two matching paragraphs.
- Broad synthesis queries (“What programming languages does Jesper know?”) can struggle if the answer is scattered across 15 different files and the search only grabs a handful of small chunks.
To solve this, I:
- Told Dify to pull more chunks per query (6–8 instead of the default few), so it has a wider net to draw an answer from.
- Wrote a system prompt that gives the bot solid background info upfront (basically my about me section and a techstack), while still leaning on the retrieved devlog chunks for specific technical details.
Wrap Up#
It took a few iterations to get the API requirements, chunking rules, and CI caching aligned, but there’s something rewarding about watching a self-updating RAG pipeline just work, especially after watching it fail in four different ways first.
Whenever I push a new devlog entry to GitHub, it’s live on the website in minutes, and the assistant can immediately discuss the new concepts or code changes with anyone visiting the site.
Feel free to try clicking the bubble in the corner and ask it a question!