Skip to content
Back to all work
AI Systems2026

AI Marketing RAG Assistant

Marketing copy generated only from verified company documents, scored for grounding and brand voice before anything is allowed to send.

Role
Architect & sole engineer
FastAPIPython 3.11ChromaDBsentence-transformersNext.js 14TypeScriptBM25

The application

Screens, captured from the running app

Booted against its seeded database and walked route by route — these are the real pages, not mockups.

Marketing RAG dashboard showing indexed documents, campaigns and generation quality scores.
Grounding and brand-voice scores sit on the dashboard next to campaign volume.
Knowledge base view listing indexed company documents and their chunk counts.
The knowledge base is the whole constraint — nothing gets generated from anything that isn't in here.
Grounded assistant screen with suggested prompts and the note that answers come from the indexed knowledge base.
The assistant, before a question is asked. Answers cite the passages they came from rather than asserting.
Campaign list with channel, status and generated content.
Campaigns hold the generated copy and the score it earned before dispatch.
Trigger engine screen with cron schedules and event triggers dispatching to email, Slack and webhooks.
The trigger engine — deliberately the dullest part of the system. Cron, events, dispatch.

The problem

A model asked to write marketing copy will produce something fluent, on-topic, and containing a product claim nobody at the company has ever made. In marketing that is not a curiosity — it is a compliance problem and a brand problem, and it is the reason most teams that try this end up hand-checking every output and concluding the tool saved nothing.

The fix is not a better prompt. It is refusing to generate from anything except retrieved, verified company material, and then measuring whether the output actually stayed inside it.

Hybrid retrieval

Two retrievers over the same knowledge base:

  • BM25 — lexical, catches exact product names, SKUs and terms of art that embeddings blur together.
  • Semantic — dense vectors in ChromaDB, catches paraphrase.

Their results are merged and re-ranked. Pure semantic search is confidently wrong about product names in a way that lexical search is not, and pure lexical misses anything phrased differently from the source. Running both is the cheapest reliability win available here.

Embeddings come from OpenAI's text-embedding-3-small when a key is present and local sentence-transformers when it isn't — so the whole system, retrieval included, runs offline.

Scoring the output, not just producing it

Every generation is scored on two axes before it goes anywhere:

  • Grounding — how much of the output is supported by the retrieved passages. Unsupported claims are what this whole architecture exists to catch.
  • Brand voice — how closely the register matches the reference material.

Low-scoring generations are surfaced rather than silently sent. This is the difference between a RAG system and a RAG demo: the demo shows you the good output, the system tells you when the output is bad.

The trigger engine

Campaign dispatch runs on cron schedules and event triggers, fanning out to email, Slack and webhooks. It is deliberately the least clever part of the system — a queue, a schedule, and a dispatcher — because the value is in what gets sent, not in the sending.

Swappable at every layer

Vector store, LLM, and embedding model each sit behind an interface: ChromaDB swaps for Pinecone or Qdrant, GPT-4o-mini for Anthropic or Ollama, hosted embeddings for local. Not for its own sake — a marketing team's constraint here is usually procurement or data residency, and those constraints arrive after the build, not before it.

FastAPI and SQLAlchemy on the backend, Next.js 14 with TypeScript and shadcn/ui on the front, background tasks in-process with Celery hooks in place.

You have a process thatshould be a system.

Tell me what arrives, who has to act on it, and where it currently falls over. That conversation is usually enough to scope the build.