π Overview
Laravel Markdown RAG is a comprehensive Laravel package that transforms Markdown files into an AI-powered knowledge base. It uses vector embeddings, cosine similarity search, and large language models to answer user questions based on your documentation.
Markdown Indexing
Automatically reads and chunks .md files from a configurable directory with intelligent text splitting.
Vector Embeddings
Generates semantic embeddings for each text chunk powered by laravel/ai package.
AI Chat Interface
Livewire-powered chat where users ask questions and get AI answers grounded in your knowledge base.
Query Rewriting
Optional keyword extraction or AI-powered query rewriting for better search results.
Reranking
Optional AI or keyword-based reranking of search results for improved relevance.
2D Visualization
t-SNE-based embedding visualization via Plotly.js for exploring your knowledge base.
Requirements
| Dependency | Version |
|---|---|
| PHP | ^8.2 |
| Laravel | ^10 | ^11 | ^12 |
| Livewire | ^3 | ^4 |
| laravel/ai | ^0.2.5 |
π Installation
-
Install via Composer
bash
composer require nomanur/laravel-markdown-rag -
Run Migrations
bash
php artisan migrateThis creates three tables:
historiesβ stores conversation messagesknowledge_chunksβ stores text chunks, embeddings, and source metadataknowledge_documentsβ stores metadata for each indexed markdown file
-
Publish Configuration (Optional)
bash
php artisan vendor:publish --provider="Nomanur\LaravelMarkdownRAGServiceProvider" --tag="config" -
Set Environment Variables
env
OPENAI_API_KEY=your_api_key_here MARKDOWN_CHAT_RATE_LIMIT=100 MARKDOWN_RERANKING=false MARKDOWN_QUERY_REWRITE=false AI_PROMPT_REWRITING=false
Note: Despite the OPENAI_API_KEY naming, this package uses laravel/ai which supports multiple AI providers including Gemini. Configure your preferred provider in config/ai.php.
Publishable Components
| Command | What Gets Published |
|---|---|
--tag="config" |
Configuration file to config/laravel-markdown-rag.php |
--tag="views" |
All Blade views for customization |
--tag="models" |
History, KnowledgeChunk, KnowledgeDocument models |
--tag="livewire" |
RagChat, WithoutFlux Livewire components |
--tag="migrations" |
Database migration files |
βοΈ Configuration
The configuration file is published to config/laravel-markdown-rag.php. Here are all available options:
| Key | Default | Description |
|---|---|---|
openai_api_key |
env('OPENAI_API_KEY') |
API key for the AI provider |
markdown_chat_rate_limit |
env('MARKDOWN_CHAT_RATE_LIMIT') |
Maximum chat history records per user |
markdown_reranking |
false |
Enable reranking of search results |
markdown_query_rewrite |
false |
Enable query rewriting before search |
ai_prompt_rewriting |
false |
Enable AI-powered prompt rewriting |
markdown_info_path |
'knowledge-base' |
Path relative to public/ for .md files |
markdown_embedding_batch_size |
50 |
Number of embeddings per batch request |
markdown_ai_retry_max_attempts |
3 |
Maximum retry attempts for AI requests |
markdown_default_agent_prompt |
(default prompt) | Default system prompt for the knowledge agent |
ποΈ System Architecture
The package operates in three distinct phases, creating a complete pipeline from markdown files to AI-powered answers:
Phase 1: Indexing
.md files in public/{markdown_info_path}/ (default: public/knowledge-base/)php artisan markdownrag:indexlaravel/aiknowledge_chunks table; metadata in knowledge_documentsphp artisan queue:work to process background jobsPhase 2: Chat
/markdownraghistories tablehistories β displayed to userPhase 3: Visualization
/vector-embedding (requires auth + verified)Component Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Laravel Markdown RAG β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββ β
β β .md Files ββββΆβ TextSplitter ββββΆβ KnowledgeDoc β β
β β (public/KB) β β (chunking) β β KnowledgeChunk β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββ β
β β β β
β βΌ βΌ β
β ββββββββββββββββ ββββββββββββββββββββ β
β β VectorServiceβββΆβ Database β β
β β (embeddings) β β (chunks/docs) β β
β ββββββββββββββββ ββββββββββββββββββββ β
β β β
β ββββββββββββββββ ββββββββββββββββ β β
β β User Chat ββββΆβKnowledgeAgentββββββββββ β
β β (Livewire) β β+ SearchTool β β
β ββββββββββββββββ ββββββββββββββββ β
β β β β
β β ββββββββββββββββββΌβββββββββββ β
β β βΌ βΌ βΌ β
β β QueryRewrite RerankSvc TSNE β
β β Service (visualization) β
β β βΌ β
β β History Model β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Indexing System
Artisan Commands
php artisan markdownrag:index # Index all .md files
php artisan markdownrag:index --clear # Clear existing chunks first
php artisan queue:work # Start queue worker for background jobs
TextSplitter Strategy
The TextSplitter service intelligently chunks markdown content using a multi-strategy approach:
- Strategy 1: Split by double newlines (
\n\n) β paragraph-based splitting - Strategy 2: Split by single newlines (
\n) β line-based fallback - Strategy 3: Character-based splitting with word boundary awareness
Default Settings: chunkSize: 1000 characters, chunkOverlap: 100 characters. Overlap ensures context from the end of one chunk carries into the next, improving search accuracy.
VectorService Operations
The central service for all vector operations:
// Generate embeddings with retry logic
getEmbeddings(array $texts): array
// Store a chunk with its embedding
storeChunk(string $content, array $embedding, string $source, ?string $documentId, array $metadata): void
// Perform cosine similarity search
search(string $query, int $limit, ?string $documentId): Collection
// Reduce dimensions for visualization
reduceDimensions(array $embeddings): array
// Chunk documents and get statistics
chunkDocuments(array $documents): array
countCharacters(array $documents): int
countTokens(array $documents): int
Similarity Search Process
- Generates an embedding for the query string
- Performs cosine similarity search across all stored chunks
- Optionally filters by
documentId - Returns results sorted by similarity (highest first)
π¬ Chat Interface
Routes
| Route | Middleware | Description |
|---|---|---|
GET /markdownrag |
auth |
Renders the chat interface (WithoutFlux view) |
GET /vector-embedding |
auth, verified |
Renders the embedding visualization page |
Livewire Components
Two chat components are provided:
RagChat (rag-chat)
- Limited to 50 messages in display
- Renders
components.ragview - Custom-styled chat interface with CSS custom properties
- Full dark mode support
WithoutFlux (without-flux)
- No message limit β shows all history
- Renders
components.without-fluxview - Uses Marked.js for markdown rendering of AI responses
- Uses Tailwind CSS via CDN
KnowledgeSearchTool
The tool available to the AI agent for searching the knowledge base:
- Name:
search_knowledge_base - Parameters:
query(required, string) β the search querydocument_id(optional, string) β limit search to a specific document
Search Pipeline
User Query β [Query Rewrite?] β Embedding β Similarity Search β [Rerank?] β Top 3 Results β AI Response
π Embedding Visualization
Access
Visit /vector-embedding while authenticated and verified.
How It Works
- Fetches up to 100
KnowledgeChunkrecords from the database - Extracts their high-dimensional embeddings
- Runs t-SNE (t-Distributed Stochastic Neighbor Embedding) to reduce dimensions to 2D
- Renders a Plotly.js interactive scatter plot
Features
- Interactive hover β shows source file name and content preview
- Displays total chunk count and t-SNE algorithm info
- Dark mode support
- Empty state with instruction to run
markdownrag:indexif no chunks exist
TSNE Service: A PHP implementation of t-SNE, ported from Karpathy's tsne.js. Default perplexity: 30.0, learning rate (eta): 100.0. Uses binary search for optimal beta and gradient descent with momentum.
β‘ Services Deep Dive
QueryRewriteService
Improves search quality by rewriting user queries before embedding generation:
Keyword Mode
Enabled when markdown_query_rewrite=true and ai_prompt_rewriting=false. Removes ~150+ English stop words to extract key terms:
"What is the best way to configure the database?" β "best way configure database"
AI Mode
Enabled when ai_prompt_rewriting=true. Uses the LLM to rephrase follow-up questions into standalone queries, incorporating the last 5 conversation messages for context. Includes exponential backoff retry logic.
RerankService
Reorders search results for improved relevance:
Keyword Mode
Enabled when markdown_reranking=true and ai_prompt_rewriting=false. Scores chunks by keyword occurrence frequency, weighted by term length. Sorts by descending score and returns top 3.
AI Mode
Enabled when ai_prompt_rewriting=true. Sends chunks to the AI with instructions to rank them by relevance. Parses the LLM's response for chunk IDs, reorders the collection, and includes a safety fallback for any missed chunks.
ποΈ Database Models
History Model
Table: histories
| Column | Type | Description |
|---|---|---|
id | bigint | Primary key |
user_id | bigint (FK) | Foreign key to users table |
agent | string | Agent identifier (e.g., 'knowledge') |
role | string | 'user' or 'assistant' |
content | text | Message content |
Relations: belongsTo(User::class)
KnowledgeChunk Model
Table: knowledge_chunks
| Column | Type | Description |
|---|---|---|
id | bigint | Primary key |
content | text | Text chunk content |
embedding | json | Vector embedding array |
source | string | Source filename |
document_id | string (indexed) | Parent document reference |
metadata | json (nullable) | Additional metadata |
Key Method: similaritySearch($queryEmbedding, $limit, $documentId) β performs cosine similarity search
KnowledgeDocument Model
Table: knowledge_documents
| Column | Type | Description |
|---|---|---|
id | bigint | Primary key |
name | string | Document name (filename) |
path | string (nullable) | File path |
system_prompt | text (nullable) | Custom system prompt for this document |
tool_description | text (nullable) | Custom tool description |
metadata | json (nullable) | Additional metadata |
π€ AI Components
KnowledgeAgent
The core AI agent implementing Agent, Conversational, and HasTools interfaces from laravel/ai:
- Constructor: Accepts an authenticatedized user, optional
documentId, and optionalKnowledgeDocumentobject - instructions(): Returns system instructions β either from a custom resolver, the document's
system_promptattribute, or the config default - messages(): Returns conversation history from the
Historymodel, filtered by user andagent='knowledge' - tools(): Returns a single
KnowledgeSearchToolinstance - timeout(): Returns 60000ms (60 seconds)
Static Customization Hooks
// Override instructions globally
KnowledgeAgent::resolveInstructionsUsing(callable $resolver);
// Override message retrieval globally
KnowledgeAgent::resolveMessagesUsing(callable $resolver);
π¨ Customization
Publishing All Components
# Publish everything at once
php artisan vendor:publish --provider="Nomanur\LaravelMarkdownRAGServiceProvider"
Service Provider Auto-Registration
The service provider automatically registers:
- Views from
resources/viewsunder thelaravel-markdown-ragnamespace - Migrations from
src/Database/migrations - Routes from
routes/web.php - Livewire components:
rag-chatandwithout-flux - Singleton binding for
laravel-markdown-rag - Console commands
π§ Agent & Tool Customization
Customizing Agent Instructions
use Nomanur\Ai\Agents\KnowledgeAgent;
// Globally override agent instructions via closure
KnowledgeAgent::resolveInstructionsUsing(function (User $user, ?string $documentId, ?KnowledgeDocument $document) {
return 'Your custom system instructions here...';
});
Customizing Message Retrieval
KnowledgeAgent::resolveMessagesUsing(function (User $user) {
return History::query()
->where('user_id', $user->id)
->where('agent', 'custom_agent')
->latest()
->take(100)
->get();
});
Customizing Search Tool Description
use Nomanur\Ai\Tools\KnowledgeSearchTool;
KnowledgeSearchTool::resolveDescriptionUsing(function () {
return 'Your custom tool description...';
});
Tip: After publishing models or Livewire components via vendor:publish, you can modify them in app/Models/ or app/Livewire/ to add custom relationships, scopes, or business logic.
π οΈ Artisan Commands
# Index all .md files from public/knowledge-base/
php artisan markdownrag:index
# Clear existing chunks before indexing
php artisan markdownrag:index --clear
# Start queue worker for background jobs
php artisan queue:work
# Append package routes to your routes/web.php (optional)
php artisan markdownrag:route
markdownrag:index
Indexes all .md files from public/{markdown_info_path}/. Displays a progress bar during indexing and creates KnowledgeDocument records for each file.
markdownrag:route
Appends the package routes to your application's routes/web.php file. Note: Routes are automatically loaded by the service provider by default, so this command is only needed if you've manually removed the route loading.
β Troubleshooting
No chunks indexed
- Ensure
.mdfiles exist inpublic/{markdown_info_path}/ - Check file permissions on the knowledge-base directory
- Run with
--clearflag if you want to re-index
AI returns empty responses
- Verify your API key is set in
.env - Check
laravel/aiconfiguration inconfig/ai.php - Ensure embeddings were generated successfully during indexing
Chat interface not loading
- Ensure Livewire is installed and properly configured
- Check that
@livewireStylesand@livewireScriptsare included in your layout - Verify the user is authenticated (routes require
authmiddleware)
Visualization page is empty
- Run
php artisan markdownrag:indexto populate chunks - Check that
knowledge_chunkstable has records with valid embeddings
Queue jobs not processing
- Ensure the queue worker is running:
php artisan queue:work - Check your
QUEUE_CONNECTIONsetting in.env(usedatabase,redis, orsync) - For development, you can set
QUEUE_CONNECTION=syncto process jobs immediately - Monitor queue worker logs for errors:
php artisan queue:listen
Need more help? Check the GitHub Issues or create a new issue for support.