Skip to content

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 pxt
from pixeltable.functions.audio import audio_splitter
from pixeltable.functions.document import document_splitter
from pixeltable.functions.huggingface import clip, sentence_transformer
from pixeltable.functions.video import extract_audio, frame_iterator
from pixeltable.functions.whisper import transcribe
TableModel = 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.String
document: pxt.Document
class SlackMessages(TableModel, name='slack'):
channel: pxt.String
user: pxt.String
ts: pxt.String
text: pxt.String
__indexes__ = [pxt.EmbeddingIndex(text, string_embed=text_embed)]
class Meetings(TableModel, name='meetings'):
title: pxt.String
audio: pxt.Audio
class Videos(TableModel, name='videos'):
title: pxt.String
video: pxt.Video
audio = 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.query
def 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.query
def 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.query
def 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.query
def 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

A parser plus a vector database plus an orchestrator, versus one backend
LayerUsual stackOne backend
IngestUnstructured, LlamaParse, or a Slack export jobDocument, String, Audio, and Video columns
ChunkA splitter in LlamaIndex or LangChaindocument_splitter, audio_splitter, frame_iterator views
IndexQdrant, pgvector, or another vector databaseEmbeddingIndex on the source view
Orchestraten8n, LangChain, or a custom workerInsert runs that row; HTTP in the same file
QueryA 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 pxt
from pixeltable.functions.document import document_splitter
from pixeltable.functions.huggingface import clip, sentence_transformer
from pixeltable.functions.video import frame_iterator
TableModel = 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.Document
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 SlackMessages(TableModel, name='slack'):
text: pxt.String
__indexes__ = [pxt.EmbeddingIndex(text, string_embed=text_embed)]
class Videos(TableModel, name='videos'):
video: pxt.Video
class 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?

Not if one backend stores the files, chunks them, indexes embeddings, and serves the query. That is the Pixeltable shape. The usual answer engines still name a parser (Unstructured), a vector database (Qdrant or pgvector), and an orchestrator (LlamaIndex, LangChain, or n8n). Keep those when they are already the contract.

How do Slack exports become searchable rows?

Export messages as JSON (channel, user, ts, text) and insert them as String rows. There is no Slack iterator. Index the text column. New messages are new rows; the embedding index updates on insert.

Do Word files need a separate parser?

Yes for DOCX. Pixeltable stores the file as a Document. Pixeltable calls MarkItDown itself for Office files, so install markitdown and insert the file. PDFs split with document_splitter using sentence,token_limit. heading does nothing on a PDF.

What happens when a new PDF, recording, or video arrives?

Insert that row. The splitter, transcription, frame view, and embedding indexes run for that file. Unchanged rows stay cached. That is incremental computation, not a corpus rebuild.

Can an agent query this without a separate vector database?

Yes. Expose @pxt.query as HTTP with FastAPIRouter, or wrap the table as a retriever (the Agno local RAG post). A separate vector database is optional when the index already lives on the source row.

Next step

Copy the commands into an agent, or run them locally with pip install pixeltable.