PDF Insight Pro’s RAG pipeline operates in two distinct phases: document ingestion — run once at upload time — and query processing — run on every chat turn. What makes the pipeline truly agentic is the second phase: instead of doing a single retrieval pass and feeding the result blindly to the LLM, the system wraps the LLM in a LangChainDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/Jatin-Mehra119/PDF-Insight-Beta/llms.txt
Use this file to discover all available pages before exploring further.
AgentExecutor that is given explicit tools it can call repeatedly. The agent decides on its own whether to search the vector database, invoke the Tavily web-search API, or combine both — before composing a final answer. This tool-calling loop runs for up to Config.AGENT_MAX_ITERATIONS (2) iterations, after which a three-tier fallback strategy guarantees a response is always returned.
Document Ingestion Phase
PDF loading with PyMuPDFLoader
process_pdf_file(file_path) in utils/text_processing.py uses LangChain’s PyMuPDFLoader to load the uploaded PDF. The loader returns a list of Document objects, one per page, each carrying the page text and a metadata dict (page number, source path, etc.).Paragraph-aware chunking with chunk_text()
chunk_text(documents, max_length=Config.DEFAULT_CHUNK_SIZE) walks every Document, splits the page text on double newlines to produce paragraphs, and assembles paragraphs into chunks whose token estimate stays at or below max_length (1000). Paragraphs shorter than Config.MIN_PARAGRAPH_LENGTH (10 characters) are skipped. A chunk is only committed to the output list when its stripped length exceeds Config.MIN_CHUNK_LENGTH (20 characters).Embedding generation with SentenceTransformer
create_embeddings(chunks, model) in utils/text_processing.py uses a SentenceTransformer loaded with Config.EMBEDDING_MODEL (BAAI/bge-large-en-v1.5) to encode all chunk texts in a single batched call. The function returns a (numpy_array, chunks) tuple.FAISS HNSW index construction
build_faiss_index(embeddings) in utils/faiss_utils.py creates a faiss.IndexHNSWFlat index from the embedding array. HNSW (Hierarchical Navigable Small World) provides approximate-nearest-neighbour search in sub-linear time.| Parameter | Value | Effect |
|---|---|---|
FAISS_NEIGHBORS | 32 | Graph connectivity; higher = better recall, slower build |
efConstruction | 200 | Build-time beam width; higher = more accurate graph |
efSearch | 50 | Query-time beam width; higher = better recall, slower search |
Query Processing Phase
Top-k chunk retrieval via retrieve_similar_chunks()
retrieve_similar_chunks(query, index, chunks, model, k=Config.INITIAL_CONTEXT_CHUNKS) for an initial 5-chunk pass. The function encodes the query string with the same SentenceTransformer, runs index.search(), and returns a list of (text, distance, metadata) tuples for every valid index position.Similarity filtering with filter_relevant_chunks()
filter_relevant_chunks(chunks_data, threshold=Config.SIMILARITY_THRESHOLD) removes any tuple whose distance value is ≥ 1.5. Because HNSW distances are L2-style (lower = more similar), chunks that exceed this threshold are considered too dissimilar to be useful.Context assembly with prepare_context_from_chunks()
prepare_context_from_chunks(context_chunks, max_tokens=Config.MAX_CONTEXT_TOKENS) sorts the filtered chunks by distance (ascending), then concatenates chunk text until the running token count reaches MAX_CONTEXT_TOKENS (7000). The resulting string is passed as the {context} variable in the agent prompt.LangChain agent creation
RAGService.execute_agent() in services/rag_service.py calls create_tool_calling_agent(llm, tools, prompt) and wraps it in an AgentExecutor. The executor is configured with handle_parsing_errors=True, max_iterations=Config.AGENT_MAX_ITERATIONS (2), and early_stopping_method="generate".Tool execution: vector_database_search and tavily_search_results_json
vector_database_search, a @tool-decorated closure over the session’s FAISS index, chunks, and embedding model. When use_search=True is set in the ChatRequest, the Tavily tool (tavily_search_results_json) is appended to the tool list.vector_database_search tool internally calls retrieve_similar_chunks() followed by filter_relevant_chunks() and returns a formatted string. Queries shorter than 3 characters are rejected with an explanatory message.Response validation and output extraction
agent_executor.invoke() returns, execute_agent() checks that response_payload["output"] is non-empty and longer than 10 characters, and that it does not match a list of known incomplete-response prefixes (e.g. "I need to", "Let me"). If validation fails, an exception is raised to trigger the fallback chain.Fallback Strategy
The system uses a three-tier fallback to ensure a response is always returned, even when the agent errors or produces an unusable output.Tier 1 — Agent execution (primary)
execute_agent() runs the full AgentExecutor loop with tool-calling and conversation memory. This is the preferred path.Tier 2 — fallback_response() (direct tool use + LLM)
generate_response() calls fallback_response(). This method:- Invokes
vector_database_search.run(query)directly (bypassing the agent loop). - Optionally invokes
tavily_search_results_json.run(query)ifuse_tavily=True. - Appends tool results to the original context string as
"Additional Information". - Calls
llm.invoke()directly with aChatPromptTemplatethat instructs the LLM to use the enriched context.
Tier 3 — Simple LLM prompt (final safety net)
fallback_response() also raises, generate_response() catches the error and returns a static apology string: "I'm sorry, I encountered an error processing your request. Please try again." In practice, fallback_response() itself contains a nested try/except that falls back to an even simpler prompt before giving up.Agentic Prompt
The system prompt is built dynamically byRAGService.create_agent_prompt() based on which tools are available. The tool_instructions string is interpolated at prompt-creation time, not at inference time.
chat_history placeholder is populated from ConversationBufferMemory that the chat handler pre-fills from the session’s persisted chat_history list before each AgentExecutor.invoke() call. This gives the agent full multi-turn context without loading the entire history into the system prompt.RAG Evaluation Metrics
The pipeline was evaluated against theneural-bridge dataset using semantic similarity (cosine) and ROUGE-L F1 as the primary metrics. Results are documented in test_RAG.ipynb and summarised below.
Key Metrics
| Metric | Value |
|---|---|
| Semantic Similarity (Mean) | 0.852 |
| ROUGE-L F1 Score (Mean) | 0.395 |
| Semantic Similarity (Max) | 1.000 |
| ROUGE-L F1 Score (Max) | 1.000 |
| Semantic Similarity (Min) | 0.592 |
| ROUGE-L F1 Score (Min) | 0.099 |
| Standard Deviation (Similarity) | 0.089 |
| Standard Deviation (ROUGE-L F1) | 0.217 |
Quantile Distribution
| Percentile | Semantic Similarity | ROUGE-L F1 Score |
|---|---|---|
| 25th | 0.7946 | 0.2516 |
| 50th | 0.8732 | 0.3256 |
| 75th | 0.9181 | 0.4951 |
Evaluation Status
| Status | Count | Percentage |
|---|---|---|
| PASS | 64 | 85.3% |
| FAIL | 11 | 14.7% |