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





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.