knowledge base search
in daily useagentic automation · python · vector search
- problem
- Support agents searched a large knowledge base by hand while customers waited.
- what i built
- An automation that keeps the knowledge base in a vector database, synced from HubSpot, and finds the right article for a question asked in plain words. To make it easy for everyone to use, I shared it as one simple command: an agent types
/kband a question, and gets the article's steps and when to escalate. - result
- Used every day by 12 full-time support agents. Finding an article by hand took about 20 seconds; with
/kb, the answer arrives in about 3.
demo
>/kb screen went black after last night's update
kb · searched the knowledge base · 3 matches
$ python scripts/search.py "screen went black after last night's update"
- 0.56Black screen after a firmware update
- 0.14Touchscreen not responding to taps
- 0.14Screen flickers or shows lines
vector search over 10 articles · confident match · returned the steps for #1007
Black screen after a firmware update (article #1007)
- Check the player's status LED. Blinking amber means the update is still installing: wait 15 minutes (updates on X1 firmware 4.2+ take longer).
- If the LED is solid green, reboot the player from the management console. Content should appear within 3 minutes.
- Confirm a playlist is assigned to the device in the console.
- Re-push the current playlist.
Escalate if still black after re-pushing the playlist, or the player shows offline for more than 30 minutes.
>/kb kiosk shows E-620, taps do nothing
kb · exact match on error code E-620
$ python scripts/search.py "kiosk shows E-620, taps do nothing"
- 1.00Touchscreen not responding to taps
- 0.08Display shows 'No signal'
- 0.07New content not showing, old content still playing
an exact error code outranks similarity · returned the steps for #1011
Touchscreen not responding to taps (article #1011)
- Ask site staff to wipe the screen; moisture or grease blocks touch input.
- Reboot the kiosk from the management console.
- In the console, confirm the touch driver shows as connected.
- Run the touch calibration from the kiosk's maintenance menu.
Escalate if touch is still unresponsive after calibration, or the driver shows as disconnected.
>
how it works
- knowledge base. Articles live in HubSpot, where the support team writes and edits them.
- sync job. A scheduled job compares HubSpot with the index and re-processes only what was added, edited or removed.
- vector database. Each article is stored as vectors, so a question can be matched by meaning and not only by exact words.
- /kb. An agent types
/kband a question in Claude Code. The skill tells Claude how to search and how to answer. - python search. The script looks for an exact error code first, then ranks articles by similarity to the question.
- answer. Claude replies with the article's steps and when to escalate, and names the article it used.
code
def sync(conn: sqlite3.Connection, articles: list[Article],
source: str) -> SyncResult:
"""Make the index match `articles`, re-embedding only new or changed articles."""
existing = {aid: (h, title) for aid, h, title in conn.execute(
"SELECT article_id, content_hash, title FROM articles")}
incoming = {a.article_id: a for a in articles}
res = SyncResult([], [], [], 0)
with conn:
for aid in existing.keys() - incoming.keys():
conn.execute("DELETE FROM chunks WHERE article_id = ?", (aid,))
conn.execute("DELETE FROM articles WHERE article_id = ?", (aid,))
res.removed.append(existing[aid][1])
for aid, art in incoming.items():
h = art.content_hash
if aid not in existing:
res.added.append(art.title)
elif existing[aid][0] == h:
res.unchanged += 1
continue
else:
conn.execute("DELETE FROM chunks WHERE article_id = ?", (aid,))
res.updated.append(art.title)
row = {**asdict(art), "content_hash": h}
conn.execute(f"INSERT OR REPLACE INTO articles ({', '.join(row)}) "
f"VALUES ({', '.join(':' + c for c in row)})", row)
conn.executemany(
"INSERT INTO chunks (article_id, section, text, vector) VALUES (?,?,?,?)",
[(aid, sec, txt, embed(txt).tobytes()) for sec, txt in art.chunks()])
conn.execute(
"INSERT INTO sync_log (source, added, updated, removed, unchanged) "
"VALUES (?,?,?,?,?)",
(source, len(res.added), len(res.updated), len(res.removed), res.unchanged))
return res
def search(conn: sqlite3.Connection, query: str, k: int = 3) -> list[Hit]:
"""Top k articles: an exact error code first, then by best chunk similarity."""
qvec = embed(query)
codes = {re.sub(r"^[Ee]-?", "E-", c) for c in ERROR_CODE_RE.findall(query)}
# brute force; fine for a few hundred articles (see README for timings)
best: dict[int, tuple[float, str]] = {}
for aid, section, blob in conn.execute(
"SELECT article_id, section, vector FROM chunks"):
vec = array.array("f")
vec.frombytes(blob)
score = cosine(qvec, vec)
if aid not in best or score > best[aid][0]:
best[aid] = (score, section)
if codes: # exact error-code match beats similarity
marks = ",".join("?" * len(codes))
for (aid,) in conn.execute(
f"SELECT article_id FROM articles WHERE error_code IN ({marks})",
sorted(codes)):
best[aid] = (1.0, "error code")
top = sorted(best.items(), key=lambda item: item[1][0], reverse=True)[:k]
if not top:
return []
rows = {r[0]: r for r in conn.execute(
f"SELECT article_id, title, product, category, error_code, body "
f"FROM articles WHERE article_id IN ({','.join('?' * len(top))})",
[aid for aid, _ in top])}
return [Hit(*rows[aid], score=max(score, 0.0), matched=section)
for aid, (score, section) in top]
technical breakdown
keeping the index in sync
The first version loaded a routine bulk CSV export from HubSpot. Once the HubSpot API was available, I replaced that with a cron job that cross-references the HubSpot knowledge base for changes and updates the internal vector database to match.
In the demo version, the sync compares a content hash per article, so only new or edited articles are processed again. Applying a month of changes to the sample knowledge base adds one article, updates one, removes one and leaves the other eight alone.
search
The demo version ranks in two passes. If the question contains an error code such as E-620, the article with that code goes straight to the top, because exact codes are where similarity search is weakest. Otherwise each article is split into two chunks, one holding the symptoms and one holding the fix, and the question is compared against both.
Below a confidence floor, the skill says it found no confident match and recommends escalating. The demo uses a small built-in embedder so it runs anywhere with no API keys; a production setup swaps in a real embedding model.
safeguards
- The skill can only read the index. Updating it is the sync job's work.
- Claude answers only from the steps the search returned, and names the article.
- When nothing matches confidently, it says so and tells the agent to escalate.
shared as one simple command
To make the pipeline and search easy for the whole team to use, I packaged them in a simplified form: a Claude Code skill, so an agent only types /kb and a question. I set up release control for it, so every agent automatically stays on the latest version.
about the demo
The demo above replays real output from a version I rebuilt from scratch on made-up articles. It follows a similar workflow to the original and shares none of the company's code or data. It has 10 automated tests covering sync and search. The full rebuild is public on GitHub: vuln7/KBsearch, with instructions to run it in a few seconds using Python alone.
stack
- Python
- vector database
- HubSpot (CSV export, then API)
- cron
- Claude Code (how agents use it)
status
In use every day by 12 full-time support agents at a digital-signage company. It became the team's search engine for the knowledge base, and a lookup that took about 20 seconds by hand now takes about 3, from asking /kb to getting the answer.
what i learned
How to build workflows and pipelines in HubSpot, how to automate HubSpot in general, and how to automate processes a company depends on every day.
more projects
quoting automation
built, not rolled outsecure web search
red-teaming in progressan AI-ready knowledge base
300+ articlesai customer support agent
live on the client siteportal402
design and whitepaper