Author: Utkrisht Sharma (utkrishs)
An AI research assistant that combines an AI agent (LangGraph), a RAG pipeline, a translation service (MCP), and a vector database (Milvus). The system is fully containerized and deployed on Google Kubernetes Engine (GKE). Users can upload academic papers, ask questions in English, Spanish, French, or Italian, and receive context-aware answers grounded in the indexed papers, along with two recommended research papers.
- Demo
- System Architecture Diagram
- Component Details
- Index Algorithm Benchmarking
- Environment Variables
- How to Run the Application
Project Demo: https://drive.google.com/file/d/1bT9SAvU8ZZji8duW0Jvqjn1pnRYn960f/view?usp=sharing
Code Walkthrough: https://drive.google.com/file/d/17Nxqd4vo_jzPHlVcfJmmYk2kSOUWt-1J/view?usp=sharing
Indexing Results:
The system consists of 3 separate services deployed on a GKE cluster.
| Source | Target | Protocol | Purpose |
|---|---|---|---|
| User (Browser) | Streamlit App | HTTP (Port 8080) | Upload papers, submit queries, view results |
| Streamlit App | LangGraph Agent | HTTP REST POST /invoke (Port 8000) |
Send upload/query requests |
| LangGraph AI Agent | Translator MCP Server | MCP over HTTP (Port 9000) | Language detection & translation |
| LangGraph Agent | Milvus Vector DB | gRPC (Port 19530) | Store/retrieve document embeddings |
| LangGraph Agent | LLM Model | HTTPS | LLM inference & embedding generation |
┌─────────────────────────────────────────────────────────────────────────┐
│ High Level Workflow │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ [Upload Path] │
│ User → Streamlit (PDF upload + domain) → Agent /invoke │
│ → index_node (chunk → embed → store) → Milvus │
│ → Response: "Indexed N chunks" │
│ │
│ [Query Path] │
│ User → Streamlit (query text) → Agent /invoke │
│ → init_query → agent (LLM) │
│ → detect_language (MCP) → translate to EN (MCP, if needed) │
│ → search_papers (Milvus) → generate answer (Gemini LLM) │
│ → translate back (MCP, if needed) → format_response │
│ → Response: {answer, 2 recommended papers} │
| |
└─────────────────────────────────────────────────────────────────────────┘
Directory: streamlit/
The Streamlit web application serves as the frontend for the research assistant system.
| Feature | Description |
|---|---|
| Paper Upload | Upload academic papers in PDF format for RAG indexing |
| Domain Selection | Select the paper's research domain: AI, Security, or Other |
| Language Support | Submit queries in English, Spanish, French, or Italian |
| Answers | Receive answers generated strictly from retrieved paper content |
| Paper Recommendations | Get 2 relevant research papers with each answer |
| Language Matching | Response language always matches the query language (e.g., Spanish input → Spanish output) |
-
Upload Papers (Sidebar):
- Select one or more PDF files using the file uploader.
- Choose the research domain from the dropdown (AI / Security / Other).
- Click "Index Documents" to send papers to the LangGraph Agent for chunking, embedding, and storage in Milvus.
-
Ask Questions (Main Area):
- Type a query in the text input field (in any supported language).
- Click "Send" to invoke the agent.
-
View Results:
- The answer is displayed under "Answer".
- Two recommended research papers are listed under "Recommended Research Papers" with clickable source links.
Directory: langgraph-rag/
This AI agent is the central orchestrator of the entire system, implemented using LangGraph. It coordinates between the frontend(streamlit), the translator service(MCP), and the vector database.
The agent uses an agentic tool-calling workflow where the LLM decides which tools to call and in what order. This is implemented via LangGraph's bind_tools mechanism.
Design choices to minimize hallucinations:
- A strong system prompt ensures that the agent must call
search_papersand base answers strictly on retrieved content. - The prompt instructs: "Base your answer STRICTLY on retrieved content. Do NOT hallucinate."
- If no relevant results are found, the agent responds with: "The indexed research papers do not contain information on this topic."
- The agent is instructed to call
search_papersexactly once — preventing retry loops that could lead to inconsistent behavior and potential timeouts. - The
format_responsenode guarantees a consistent output structure regardless of LLM behavior.
| Node | Type | Description |
|---|---|---|
_route_entry |
Conditional Entry | Routes to index_node (if file_content present) or init_query (if question present) |
index_node |
Processing | Handles paper upload: chunks text using RecursiveCharacterTextSplitter (800/120), generates embeddings, and stores in Milvus with metadata (domain, title, source) |
init_query |
Processing | Prepares the initial HumanMessage with the research question and instructs LLM to detect language first |
agent |
LLM | The core LLM node: uses ChatGoogleGenerativeAI with bound tools (MCP + local). Decides autonomously which tool to call next |
tools |
Tool Execution | ToolNode that executes tool calls — MCP tools for translation, local search_papers tool for vector search |
format_response |
Processing | Extracts the final answer from the last AIMessage and the top 2 unique recommended papers from search_papers results |
- Language Detection: Calls
detect_language(text)via MCP server to detect the query language. - Language Validation: Check if the language is supported. If it is not supported then it will return an error message immediately and ends the workflow instantly.
- Translation to English (if needed): Calls
translate_text(text, source_lang, "en")via MCP server. - Domain-Filtered Search: Calls
search_papers(query, domain, top_k)to retrieve relevant chunks from the milvus vector databse with cosine similarity. - Answer Generation: Uses the LLM to write an answer based strictly on retrieved content. Mentions relevant paper titles.
- Translation (if needed): Calls
translate_text(answer, "en", target_lang)via MCP. - Format Response: Extracts answer + 2 recommended paper titles/sources into a consistent JSON format.
The agent always returns a consistent JSON structure via the /invoke endpoint:
{
"answer": "The translated answer text based on retrieved papers...",
"recommended_papers": [
{"title": "Paper Title 1", "source": ""},
{"title": "Paper Title 2", "source": ""}
]
}The agent communicates with the Translator MCP Server via a standardized MCP (Model Context Protocol) HTTP interface:
- Endpoint:
http://<mcpserver_ip>:80/mcp - Protocol: JSON-RPC 2.0
- Tools exposed by MCP:
detect_language,translate_text - The MCP client is initialized once during app startup and tools are bound to the LLM via
bind_tools. - Cleanup is handled via
cleanup_mcp()on shutdown.
| Endpoint | Method | Description |
|---|---|---|
/ok |
GET | Health check — returns {"status": "ok"} |
/invoke |
POST | Main endpoint. Accepts upload or query payloads invokes the LangGraph state machine, and returns {answer, recommended_papers} |
Directory: mcp-server/
The translation service is a standardized service deployed on GKE. It uses the FastMCP framework to expose translation tools via the Model Context Protocol (MCP) over HTTP.
The service uses deep-translator (GoogleTranslator) for actual translation. This provides high-quality, production-grade neural machine translation without requiring GPUs or custom models.
| Code | Language |
|---|---|
en |
English |
es |
Spanish |
fr |
French |
it |
Italian |
| Tool | Parameters | Return Value |
|---|---|---|
detect_language(text) |
text: str |
{language_code, language_name, supported: bool} |
translate_text(text, source_lang, target_lang) |
text: str, source_lang: str, target_lang: str |
{translated_text, source_lang, target_lang} |
// Input
{"text": "¿Cuáles son los avances recientes?", "source_lang": "es", "target_lang": "en"}
// Output
{"translated_text": "What are the recent advances?", "source_lang": "es", "target_lang": "en"}Unsupported language handling:
// Input
{"text": "something", "source_lang": "zh", "target_lang": "en"}
// Output
{"error": "Unsupported source language: zh"}Terraform Directory: milvus-gke/
Benchmarking Directory: index_data/
30 academic papers are indexed through the Streamlit application, organized by domain:
| Domain | Count | Source |
|---|---|---|
| AI | 10 | Google Scholar — AI papers |
| Security | 10 | Google Scholar — Security papers |
| Other | 10 | Google Scholar — General papers |
All 30 papers are uploaded through the Streamlit web application with their respective domain labels selected during upload.
| Setting | Value |
|---|---|
| Embedding Model | gemini-embedding-001 |
| Chunk Size | 800 characters |
| Chunk Overlap | 120 characters |
| Similarity Metric | Cosine Similarity (for all searches) |
| Collection Name | research_papers |
| Index Type (Production) | IVF_PQ (nlist=128, m=8, nbits=8) |
| Search Parameters | nprobe=10 for IVF_PQ |
Papers are indexed and embedded in their original language (primarily English for academic papers). The embeddings are stored in a single Milvus collection with domain metadata for filtering.
When a user submits a query, the search_papers tool performs:
- Cosine similarity search on the embedding vectors.
- Optional domain filtering via Milvus expression:
domain == "AI"(orSecurity,Other). - Returns top-k results (default: 4) with chunk content and metadata.
Auto-scaling is configured via kubectl after Terraform deployment:
kubectl autoscale deployment milvus --min=1 --max=5 --cpu-percent=70 -n milvus| Parameter | Value |
|---|---|
| Minimum Pods | 1 |
| Maximum Pods | 5 |
| Scaling Trigger | CPU utilization ≥ 70% |
Verify auto-scaling:
kubectl get hpa -n milvusScript: index_data/benchmark.py
The benchmark compares three indexing algorithms on the same set of academic papers using cosine similarity for all similarity calculations.
- HNSW
- IVF_PQ
- DiskANN
The benchmark script (benchmark.py) performs:
- Load: Reads all PDF/TXT papers from a directory.
- Chunk: Splits all documents (800/120). Same chunks for all index types.
- Embed: Generates embeddings via Gemini API with rate limiting (batch size 50, 2s delay). Embeddings are computed once and reused for all index types via
PrecomputedEmbeddings. - Index: For each index type (HNSW, IVF_PQ, DiskANN):
- Creates a new Milvus collection.
- Inserts all chunks with pre-computed embeddings.
- Measures indexing time and collection storage size.
- Report: Prints a comparison table.
| Criterion | Best | Observation |
|---|---|---|
| Fastest Indexing | IVF_PQ | Product quantization with inverted file index is the fastest to build |
| Smallest Storage | IVF_PQ | PQ compression significantly reduces storage footprint |
| Highest Recall | HNSW | Graph-based ANN provides the best recall quality at the cost of higher memory |
| Variable | Default | Where to Set | Description |
|---|---|---|---|
MILVUS_URI |
http://34.46.107.62:19530 |
Agent, Benchmark | Milvus vector DB connection URI |
MCP_SERVER_URL |
http://35.224.9.106:80/mcp |
Agent | Translator MCP server endpoint |
LANGGRAPH_API_BASE |
http://34.132.103.45:80 |
Streamlit | LangGraph Agent API base URL |
Upload the milvus-gke folder to Google Cloud Shell
cd milvus-gke
terraform init
terraform apply
kubectl autoscale deployment milvus --min=1 --max=5 --cpu-percent=70 -n milvus
# Verify auto-scaling:
kubectl get hpa -n milvusNote: Save the Milvus external IP
Update the MILVUS_URI as given in the milvus service endpoint
Upload the mcp-server folder to Google Cloud Shell
cd mcp-server
gcloud auth configure-docker
gcloud builds submit --tag gcr.io/<YOUR_PROJECT_ID>/mcpserver:1.0 .Deploy to GKE (via Google Cloud Console):
- Go to Artifact Registry → find translator-mcp:1.0
- Click "Deploy to GKE"
- Deployment name: mcpserver
- Node type: User-managed nodes
- Expose: Port 80 to Target Port 9000
- Wait for green checkmark
Verify the translator service:
# Test
curl -X POST http://35.224.9.106:80/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: 41567d76ca6c4cc880226e9ee0505cff" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"translate_text","arguments":{"text":"Hello world","source_lang":"en","target_lang":"es"}},"id":1}'
Note: Save the Translator external IP
Update the MILVUS_URI as given in the milvus service endpoint
Upload the mcp-server folder to Google Cloud Shell
cd langgraph-rag
gcloud builds submit --tag gcr.io/<YOUR_PROJECT_ID>/langgraph:1.0 .Deploy to GKE (via Google Cloud Console):
- Go to Artifact Registry in langgraph:1.0
- Click "Deploy to GKE"
- Deployment name: langgraph
- Node type: User-managed nodes
- Environment variables (Container Details section):
- MCP_SERVER_URL = http://<mcpserver_ip>:80/mcp
- Expose: Port 80 → Target Port 8000
- Wait for green checkmark
Verify the agent service:
curl http://<AGENT_IP>:80/ok
# Expected: {"status":"ok"}
# Test a query (English)
curl -X POST http://<AGENT_IP>:80/invoke \
-H "Content-Type: application/json" \
-d '{"input":{"question":"What are neural network architectures?","top_k":4}}'Note: Save the Agent external IP
Upload the streamlit folder to Google Cloud Shell
cd streamlit
gcloud builds submit --tag gcr.io/<YOUR_PROJECT_ID>/streamlit:1.0 .Deploy to GKE (via Google Cloud Console):
- Go to Artifact Registry and find streamlit:1.0
- Click "Deploy to GKE"
- Deployment name: streamlit
- Node type: User-managed nodes
- Environment variables (Container Details section): - LANGGRAPH_API_BASE = http://<langgraphservice_ip>:80
- Expose: Port 80 → Target Port 8080
- Wait for green checkmark
Access the Streamlit app: In services, click the service endpoint IP of the streamlit service to spin up the frontend and access the whole project
cd index_data
pip install -r requirements.txt
python benchmark.py --papers_dir ../papersThe benchmark:
- Loads all PDF papers from the directory.
- Chunks all documents (800/120 split).
- Embeds all chunks via Gemini API (with rate limiting).
- Creates separate Milvus collections for HNSW, IVF_PQ, and DiskANN.
- Prints a comparison table of indexing time and storage size.



