A conversational AI agent built with LangGraph and RAG (Retrieval-Augmented Generation) that answers questions about your meeting notes. It loads .txt meeting transcripts into a ChromaDB vector store, then lets users query them through a Streamlit chat interface powered by a tool-calling LLM agent.
User: What decisions were made about the database optimization?
Assistant: In the "Database Optimization Discussion" meeting on July 4, 2026,
the team decided to:
1. Add indexes to frequently searched columns
2. Archive old transactional data
3. Enable database query monitoring
Participants included Sarah Chen (DBA), James Wilson (Backend Lead), ...
┌─────────────────────────────────────────────────┐
│ Streamlit UI (main.py) │
│ Chat interface · Session state · History │
└────────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ LangGraph Workflow (runnable.py) │
│ │
│ ┌───────────┐ should_continue ┌───────┐ │
│ │ agent │──────────────────────▶│ tools │ │
│ │ (LLM call)│◀──────────────────────│ (exec)│ │
│ └───────────┘ tools → agent └───────┘ │
│ │
│ Checkpointing: InMemorySaver │
└────────────────────┬────────────────────────────┘
│
┌──────────┴──────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ LLM Provider │ │ Tool Functions │
│ (OpenAI-compat) │ │ (tools.py) │
└──────────────────┘ └───────┬──────────┘
│
┌─────────┴─────────┐
▼ ▼
┌──────────────┐ ┌────────────────┐
│ query_docs │ │ get_current_ │
│ (ChromaDB │ │ time │
│ RAG search) │ └────────────────┘
└──────┬───────┘
│
▼
┌──────────────┐
│ ChromaDB │
│ Vector Store│
│ (./chroma_db)│
└──────────────┘
- Ingestion —
rag_document_loader.pyreads.txtfiles frommeeting_notes/, splits them into 400-character chunks (80-char overlap), computes embeddings withall-MiniLM-L6-v2, and persists them to./chroma_db/. - Runtime —
main.pystarts the Streamlit app;tools.pyloads the existing ChromaDB instance and exposesquery_documentsandget_current_timeas LangGraph tools. - Query — User asks a question → LangGraph agent decides whether to call a tool → tool retrieves top-5 similar chunks from Chroma → LLM synthesizes a grounded answer citing specific meetings.
- Guardrails — Before the agent runs,
input_guardrails.pychecks the latest user message for banned keywords and can short-circuit to a safe refusal response without calling the model.
MeetingNotesAgent/
├── main.py # Streamlit entry point — chat UI, session state
├── runnable.py # LangGraph StateGraph: agent ↔ tools loop
├── input_guardrails.py # Deterministic middleware that blocks banned keywords before the LLM runs
├── tools.py # @tool functions: query_documents, get_current_time
├── rag_document_loader.py # One-shot script: ingest meeting notes → ChromaDB
├── rag_evaluation.ipynb # Jupyter notebook for RAG evaluation
├── meeting_notes/ # Source .txt meeting transcripts
│ ├── 1.txt
│ └── 2.txt
├── chroma_db/ # Persisted Chroma vector store (generated)
├── requirements.txt # Python dependencies
└── .env # Environment variables (not committed)
- Python 3.10+
- An OpenAI-compatible LLM endpoint — any provider that exposes an OpenAI-compatible API (OpenAI, Azure OpenAI, OmniRouter, Ollama, etc.)
- ~500 MB disk for the embedding model download (first run only)
git clone <your-repo-url>
cd MeetingNotesAgent
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtCreate a .env file in the project root:
# LLM provider (OpenAI-compatible API)
OMNIROUTER_API_KEY=your-api-key-here
OMNIROUTER_BASE_URL=https://your-provider.com/v1
LLM_MODEL=gpt-4o-mini
# Optional: LangSmith tracing
LANGSMITH_API_KEY=your-langsmith-key
# Optional: custom document directory (default: meeting_notes)
RAG_DIRECTORY=meeting_notesPlace .txt files in the meeting_notes/ directory (or set RAG_DIRECTORY to a custom path). Each file should contain the transcript or summary of a single meeting.
Run this once, and again whenever you add or update meeting notes:
python rag_document_loader.pyThis loads your .txt files, chunks them, computes embeddings, and saves the vector store to ./chroma_db/.
streamlit run main.pyThe app opens at http://localhost:8501.
The agent has access to two tools:
| Tool | Description |
|---|---|
query_documents(question) |
Searches ChromaDB for the top-5 most relevant document chunks and returns them with source filenames. This is the core RAG retrieval tool. |
get_current_time() |
Returns the current local timestamp. |
To add a new tool, decorate a function with @tool in tools.py and add it to the available_functions dict.
The agent is built on a LangGraph StateGraph with two nodes:
agent— Calls the LLM with the current conversation history and bound tools. If the LLM issues tool calls, the graph routes to thetoolsnode.tools— Executes each tool call, wraps results asToolMessages, and routes back toagentfor synthesis.
The should_continue conditional edge decides whether to loop back to tools or end the turn. InMemorySaver provides checkpointing for multi-turn conversations within a session.
The project also includes a deterministic guardrail layer in input_guardrails.py. It uses a ContentFilterMiddleware to inspect the latest human message before the agent runs. If a banned keyword is detected (for example, hack, exploit, or bypass), the middleware returns a short refusal message and stops the workflow early. This prevents blocked requests from reaching the LLM and avoids unnecessary model cost.
You can customize the banned keywords in runnable.py by adjusting the ContentFilterMiddleware configuration.
The agent uses a system prompt that enforces strict retrieval-grounded behavior:
- Always query documents before answering — no hallucination from memory.
- Ground answers in retrieved documents — no fabrication.
- Synthesize across all returned chunks — a single query may span multiple meetings.
- Cite specific meeting names from source metadata.
- List decisions, action items, and participants when present.
- Admit uncertainty when documents don't contain the answer.
- Embedding model:
sentence-transformers/all-MiniLM-L6-v2(384-dimensional, ~80 MB, runs locally via PyTorch) - Chunk size: 400 characters with 80-character overlap
- Splitter:
RecursiveCharacterTextSplitterwith["\n\n", "\n", " "]separators — respects paragraph and sentence boundaries - Retrieval: Cosine similarity search, top-5 results
The rag_evaluation.ipynb notebook provides an interactive environment for evaluating RAG retrieval quality. Use it to:
- Test queries against the vector store
- Inspect retrieved chunks for relevance
- Iterate on chunking parameters and retrieval settings
| Variable | Required | Default | Description |
|---|---|---|---|
OMNIROUTER_API_KEY |
Yes | — | API key for your LLM provider |
OMNIROUTER_BASE_URL |
Yes | — | Base URL for the OpenAI-compatible API |
LLM_MODEL |
No | gpt-4o-mini |
Model name to use |
LANGSMITH_API_KEY |
No | — | Enable LangSmith tracing/evaluation |
RAG_DIRECTORY |
No | meeting_notes |
Directory containing .txt meeting notes |
"No documents found" / empty responses
- Make sure you've run
python rag_document_loader.pyafter adding notes - Verify
./chroma_db/exists and is not empty - Check that
RAG_DIRECTORYpoints to the correct folder
LLM connection errors
- Verify
OMNIROUTER_BASE_URLandOMNIROUTER_API_KEYin.env - Ensure the endpoint is OpenAI-compatible (supports
/chat/completions) - Check that
LLM_MODELmatches a model available at your provider
Embedding model download is slow
- The first run downloads
all-MiniLM-L6-v2(~80 MB) from HuggingFace - Subsequent runs use the cached model in
~/.cache/huggingface/
Streamlit caching issues
- If tools seem stale after code changes, restart the Streamlit server
- The ChromaDB instance is loaded at module import time in
tools.py
This project is for educational and internal use.