How to
How do I build a company knowledge base for AI from docs, Slack, meeting recordings and videos?
A company knowledge base for AI stores documents, chat exports, meeting recordings, and videos, splits them into passages, transcript slices, or frames, embeds those units, and answers from an agent or RAG route. New files should update only their own chunks and indexes. The usual stack is a parser, a vector database, and an orchestrator.
Updated · Part of How do I ... in Python?
Before you start
About 45 min.
- Python 3.11+
- pip install -U 'pixeltable[serve]'
- pxt init in the project root
- spacy and tiktoken for sentence,token_limit splitting
- sentence-transformers for all-MiniLM-L6-v2
- torch for CLIP
- ffmpeg
- openai-whisper
- markitdown for Word files
1. Store documents, Slack, recordings, and videos as typed tables
Keep each source as rows the engine understands: a document file, a chat message as text, a meeting as audio, a clip as video. A file path fills a Document, Audio, or Video column. Word files need markitdown installed; the engine calls it. Slack is an export of messages, not a chat iterator.
import pixeltable as pxtfrom pixeltable.functions.audio import audio_splitterfrom pixeltable.functions.document import document_splitterfrom pixeltable.functions.huggingface import clip, sentence_transformerfrom pixeltable.functions.video import extract_audio, frame_iteratorfrom pixeltable.functions.whisper import transcribeTableModel = pxt.model_base()text_embed = sentence_transformer.using(model_id='all-MiniLM-L6-v2')clip_embed = clip.using(model_id='openai/clip-vit-base-patch32')class Docs(TableModel, name='docs'):title: pxt.Stringdocument: pxt.Documentclass SlackMessages(TableModel, name='slack'):channel: pxt.Stringuser: pxt.Stringts: pxt.Stringtext: pxt.String__indexes__ = [pxt.EmbeddingIndex(text, string_embed=text_embed)]class Meetings(TableModel, name='meetings'):title: pxt.Stringaudio: pxt.Audioclass Videos(TableModel, name='videos'):title: pxt.Stringvideo: pxt.Videoaudio = extract_audio(video)transcript = transcribe(audio, model='base').text.astype(pxt.String)__indexes__ = [pxt.EmbeddingIndex(transcript, string_embed=text_embed)]Docs.insert([{'title': 'Handbook', 'document': 'docs/handbook.docx'}])SlackMessages.insert([{'channel': 'general', 'user': 'ana', 'ts': '1731000000.000100', 'text': 'Release is at 3 pm'}])Meetings.insert([{'title': 'Weekly sync', 'audio': 'recordings/weekly-sync.mp3'}])Videos.insert([{'title': 'Dock camera', 'video': 'clips/dock-0930.mp4'}])
2. Chunk documents, slice meetings, sample video frames
Split each document into sentence windows with a token limit. Split each meeting into timestamped audio slices and transcribe them. Walk each clip at one frame per second. Slack rows are already retrieval units: index the message text as it is.
class Chunks(TableModel,name='chunks',base=Docs,iterator=document_splitter(document=Docs.document,separators='sentence,token_limit',limit=300,),):__indexes__ = [pxt.EmbeddingIndex(text, string_embed=text_embed)]class MeetingSegments(TableModel,name='meeting_segments',base=Meetings,iterator=audio_splitter(audio=Meetings.audio, duration=30.0),):transcript = transcribe(audio_segment, model='base').text.astype(pxt.String)__indexes__ = [pxt.EmbeddingIndex(transcript, string_embed=text_embed)]class Frames(TableModel,name='frames',base=Videos,iterator=frame_iterator(video=Videos.video, fps=1),):__indexes__ = [pxt.EmbeddingIndex(frame, embedding=clip_embed)]
3. Query the indexes from an agent or a RAG route
A query ranks passages, Slack lines, transcript slices, or stills. An agent calls those queries as tools. An HTTP route can expose the same search. Inserting a new file updates only that file: new chunks, new slices, new frames, and their indexes.
from pixeltable.serving import FastAPIRouter@pxt.querydef search_passages(question: str, limit: int = 5):sim = Chunks.text.similarity(string=question)return (Chunks.order_by(sim, asc=False).limit(limit).select(title=Docs.title, text=Chunks.text, score=sim))@pxt.querydef search_slack(question: str, limit: int = 5):sim = SlackMessages.text.similarity(string=question)return (SlackMessages.order_by(sim, asc=False).limit(limit).select(channel=SlackMessages.channel,user=SlackMessages.user,text=SlackMessages.text,score=sim,))@pxt.querydef search_meetings(question: str, limit: int = 5):sim = MeetingSegments.transcript.similarity(string=question)return (MeetingSegments.order_by(sim, asc=False).limit(limit).select(title=Meetings.title,segment_start=MeetingSegments.segment_start,text=MeetingSegments.transcript,score=sim,))@pxt.querydef search_video(question: str, limit: int = 5):sim = Frames.frame.similarity(string=question)return (Frames.order_by(sim, asc=False).limit(limit).select(title=Videos.title, frame_attrs=Frames.frame_attrs, score=sim))api = FastAPIRouter(name='knowledge')api.add_query_route(path='/search/docs', query=search_passages)api.add_query_route(path='/search/slack', query=search_slack)api.add_query_route(path='/search/meetings', query=search_meetings)api.add_query_route(path='/search/video', query=search_video)# pxt init# pxt schema update app.py knowledge -f# pxt service update app.py knowledge -f
A parser plus a vector database plus an orchestrator, versus one backend
| Layer | Usual stack | One backend |
|---|---|---|
| Ingest | Unstructured, LlamaParse, or a Slack export job | Document, String, Audio, and Video columns |
| Chunk | A splitter in LlamaIndex or LangChain | document_splitter, audio_splitter, frame_iterator views |
| Index | Qdrant, pgvector, or another vector database | EmbeddingIndex on the source view |
| Orchestrate | n8n, LangChain, or a custom worker | Insert runs that row; HTTP in the same file |
| Query | A retriever wired to an agent | @pxt.query the agent or RAG route calls |
Where Pixeltable fits
Pixeltable is the multimodal backend for this knowledge base: Document and Slack String tables, a document splitter view, audio splitter plus Whisper, a frame iterator plus CLIP, and embedding indexes that update on insert. Keep Qdrant, LlamaIndex, Unstructured, or n8n when those are already the contract. A parser plus a vector database plus an orchestrator is three systems to keep in sync; one table is the other shape.
import pixeltable as pxtfrom pixeltable.functions.document import document_splitterfrom pixeltable.functions.huggingface import clip, sentence_transformerfrom pixeltable.functions.video import frame_iteratorTableModel = pxt.model_base()text_embed = sentence_transformer.using(model_id='all-MiniLM-L6-v2')clip_embed = clip.using(model_id='openai/clip-vit-base-patch32')class Docs(TableModel, name='docs'):document: pxt.Documentclass Chunks(TableModel,name='chunks',base=Docs,iterator=document_splitter(document=Docs.document, separators='sentence,token_limit', limit=300),):__indexes__ = [pxt.EmbeddingIndex(text, string_embed=text_embed)]class SlackMessages(TableModel, name='slack'):text: pxt.String__indexes__ = [pxt.EmbeddingIndex(text, string_embed=text_embed)]class Videos(TableModel, name='videos'):video: pxt.Videoclass Frames(TableModel,name='frames',base=Videos,iterator=frame_iterator(video=Videos.video, fps=1),):__indexes__ = [pxt.EmbeddingIndex(frame, embedding=clip_embed)]
Questions
Do I need Qdrant, LlamaIndex, and Unstructured to build this knowledge base?
How do Slack exports become searchable rows?
Do Word files need a separate parser?
What happens when a new PDF, recording, or video arrives?
Can an agent query this without a separate vector database?
Next step
Copy the commands into an agent, or run them locally with pip install pixeltable.