{"slug": "build-a-multi-agent-rag-legal-assistant-with-langgraph-fastapi-and-streamlit", "title": "Build a Multi-Agent RAG Legal Assistant with LangGraph, FastAPI, and Streamlit (Beginner Guide)", "summary": "A developer published a beginner tutorial for building an end-to-end multi-agent RAG legal assistant tailored to UAE Federal Law, using LangGraph, FastAPI, Streamlit, Pinecone, and OpenRouter. The architecture replaces naive single-pass RAG with a cyclic system in which a synthesizer node drafts answers from retrieved statutory text and a fact-checker node verifies each claim, looping back for rewrites when unsupported claims are detected. The guide covers PDF ingestion into 384-dimension vectors, a pinned dependency stack, and Docker-based deployment.", "body_md": "Retrieval-Augmented Generation (RAG) sounds complex, but the core concept is straightforward: instead of asking an AI model to answer purely from memory, you hand it specific reference documents and tell it to answer using only that text.\n\nIn this guide, you will build an end-to-end legal assistant tailored for UAE Federal Law. Although we use UAE legal documents in this tutorial, the same architecture can be applied to company policies, research papers, medical guidelines, knowledge bases, or any custom document collection. We will walk through every layer, from turning raw PDFs into searchable vectors to forcing an AI agent to fact-check its own answers.\n\n*(Above: The final Streamlit UI showing a verified answer and expandable sources)*\n\n**What this step does:** Explains the foundational concept behind our application.\n\n**Why we need it:** To understand why we aren't just using ChatGPT out of the box.\n\nTraditional LLMs answer using their training data. Because that data is frozen in time, they often hallucinate or invent fake legal clauses.\n\nRAG allows an LLM to retrieve information from external documents before generating a response.\n\nIn this project, the assistant will:\n\nMost beginner tutorials teach \"naive RAG,\" which follows a single straight line: `Question ➔ Search ➔ Answer`. If the model hallucinates a fake legal clause, the user receives false information.\n\nWe are building a **cyclic multi-agent system** that verifies its own output before sending it back:\n\n```\nUser Question\n      │\n      ▼\nVector Search (Pinecone) ──────────► Retrieves statutory text chunks\n      │\n      ▼\nSynthesizer Node ──────────────────► Drafts an answer using ONLY retrieved text\n      │\n      ▼\nFact-Checker Node (Gatekeeper) ────► Compares draft against raw legal text\n      │\n      ├── [FALSE: Unsupported claims] ──► Loops back to Synthesizer to rewrite\n      │\n      └── [TRUE: 100% Supported] ───────► Sends final response to user\n```\n\nBefore starting, you should be familiar with basic Python syntax, virtual environments, and basic HTTP requests. No prior experience with LangGraph or Docker is required.\n\nCreate a root directory named `uae-legal-rag` and organize your files like this:\n\n```\nuae-legal-rag/\n├── data/\n│   └── uae_labor_law.pdf # Place your legal document here\n├── backend/\n│   ├── __init__.py\n│   ├── schemas.py        # Request/Response data models\n│   ├── agent.py          # Multi-agent LangGraph state machine\n│   └── server.py         # FastAPI application\n├── frontend/\n│   └── app.py            # Streamlit user interface\n├── ingest.py             # Script to chunk & upload PDFs to Pinecone\n├── .env                  # Secret API keys\n├── requirements.txt      # Pinned dependencies\n└── Dockerfile            # Container definition\n```\n\n**What this step does:** Configures our zero-cost infrastructure and installs libraries.\n\n**Pinecone:** Create a free starter account at pinecone.io. Create an index named `uae-law` with **384 dimensions** (matching our open-source embedding model) and the **cosine** similarity metric.\n\n**OpenRouter:** Sign up at openrouter.ai and generate an API key. We will use their `openrouter/free` endpoint.\n\nCreate a `.env` file in the project root:\n\n```\nPINECONE_API_KEY=\"your_pinecone_api_key\"\nOPENROUTER_API_KEY=\"your_openrouter_api_key\"\n```\n\nCreate `requirements.txt` and lock these exact versions to avoid breaking changes:\n\n```\nfastapi==0.110.0\nuvicorn==0.29.0\npydantic==2.6.4\nlanggraph==0.0.30\nlangchain==0.1.13\nlangchain-community==0.0.29\nlangchain-pinecone==0.0.3\nlangchain-huggingface==0.0.2\nlangchain-openai==0.1.1\npinecone-client==3.2.2\nstreamlit==1.32.2\npython-dotenv==1.0.1\npypdf==4.1.0\nrequests==2.31.0\n```\n\nInstall them:\n\n```\npython -m venv venv\nsource venv/bin/activate  # Windows: venv\\Scripts\\activate\npip install -r requirements.txt\n```\n\n`ingest.py`)\n**What this step does:** Converts a human-readable PDF into machine-searchable numbers (vectors).\n\n**Why we need it:** A vector database starts empty. Without this, the AI has no law to search.\n\nThe flow is: `PDF ➔ Chunking ➔ Embeddings ➔ Pinecone`.\n\nPlace a PDF in the `data/` directory, then create `ingest.py`:\n\n``` python\nimport os\nfrom dotenv import load_dotenv\nfrom langchain_community.document_loaders import PyPDFLoader\nfrom langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_huggingface import HuggingFaceEmbeddings\nfrom langchain_pinecone import PineconeVectorStore\n\nload_dotenv()\n\ndef ingest_documents():\n    pdf_path = \"data/uae_labor_law.pdf\"\n    if not os.path.exists(pdf_path):\n        raise FileNotFoundError(f\"Missing PDF at {pdf_path}. Please place a document there.\")\n\n    print(\"1. Loading PDF...\")\n    loader = PyPDFLoader(pdf_path)\n    raw_documents = loader.load()\n\n    print(\"2. Chunking text...\")\n    text_splitter = RecursiveCharacterTextSplitter(\n        chunk_size=1000,\n        chunk_overlap=150\n    )\n    docs = text_splitter.split_documents(raw_documents)\n    print(f\"Created {len(docs)} text chunks.\")\n\n    print(\"3. Generating embeddings & uploading to Pinecone...\")\n    # Runs locally on CPU at zero cost (384 dimensions)\n    embeddings = HuggingFaceEmbeddings(model_name=\"all-MiniLM-L6-v2\")\n\n    PineconeVectorStore.from_documents(\n        documents=docs,\n        embedding=embeddings,\n        index_name=\"uae-law\"\n    )\n    print(\"Ingestion complete. Documents are now indexed in Pinecone.\")\n\nif __name__ == \"__main__\":\n    ingest_documents()\n```\n\n**How to test:** Run `python ingest.py` in your terminal.\n\n**Expected outcome:** You will see \"Ingestion complete.\" Check your Pinecone dashboard to verify the vectors are there.\n\n**Common error:** `IndexNotFoundError` means you forgot to create the `uae-law` index in the Pinecone console first.\n\n`backend/schemas.py`)\n**What this step does:** Sets up strict rules for what data can enter and leave our API.\n\n**Why we need it:** To keep the application decoupled, the API validates user input before it ever touches the agent workflow.\n\nCreate `backend/schemas.py`:\n\n``` python\nfrom pydantic import BaseModel, Field\nfrom typing import List\n\nclass ChatRequest(BaseModel):\n    query: str = Field(..., min_length=5, max_length=500, description=\"Legal question\")\n\nclass ChatResponse(BaseModel):\n    verified_answer: str\n    sources: List[str]\n```\n\n`backend/agent.py`)\n**What this step does:** Creates the \"brain\" of our application using three specific agents.\n\nThe Retriever Agent performs semantic search. Its job is simple: Receive a user question, search Pinecone, and return relevant legal passages. These passages are then passed to the Synthesizer Agent.\n\nThe Synthesizer drafts an initial answer grounded strictly in those excerpts.\n\nThe Fact-Checker compares the draft against the raw legal text. If it detects assumptions, it loops back to the Synthesizer to rewrite. (We loop back to the Synthesizer rather than the Retriever because retrieval is usually correct; the LLM simply needs to be forced to write a more conservative answer).\n\nCreate `backend/agent.py`:\n\n``` python\nimport os\nfrom typing import TypedDict, List\nfrom dotenv import load_dotenv\nfrom langgraph.graph import StateGraph, END\nfrom langchain_huggingface import HuggingFaceEmbeddings\nfrom langchain_pinecone import PineconeVectorStore\nfrom langchain_openai import ChatOpenAI\n\nload_dotenv()\n\nembeddings = HuggingFaceEmbeddings(model_name=\"all-MiniLM-L6-v2\")\nvectorstore = PineconeVectorStore(index_name=\"uae-law\", embedding=embeddings)\n\nllm = ChatOpenAI(\n    base_url=\"https://openrouter.ai/api/v1\",\n    api_key=os.getenv(\"OPENROUTER_API_KEY\"),\n    model=\"openrouter/free\"\n)\n\nclass GraphState(TypedDict):\n    query: str\n    context: List[str]\n    draft: str\n    verified_answer: str\n    cycle_count: int\n\ndef retriever_node(state: GraphState):\n    docs = vectorstore.similarity_search(state[\"query\"], k=3)\n    return {\"context\": [doc.page_content for doc in docs]}\n\ndef synthesizer_node(state: GraphState):\n    context_block = \"\\n\\n\".join(state[\"context\"])\n    prompt = (\n        f\"You are a strict UAE legal assistant. Answer the question using ONLY the provided text.\\n\"\n        f\"Context:\\n{context_block}\\n\\n\"\n        f\"Question: {state['query']}\\n\"\n        f\"Answer:\"\n    )\n    response = llm.invoke(prompt)\n    return {\"draft\": response.content}\n\ndef fact_checker_node(state: GraphState):\n    context_block = \"\\n\\n\".join(state[\"context\"])\n    prompt = (\n        f\"Evaluate if the following Answer is 100% supported by the Context.\\n\"\n        f\"Context:\\n{context_block}\\n\\n\"\n        f\"Answer:\\n{state['draft']}\\n\\n\"\n        f\"If completely supported without assumptions, reply ONLY with 'TRUE'.\\n\"\n        f\"If unsupported, reply ONLY with 'FALSE'.\"\n    )\n    result = llm.invoke(prompt).content.strip().upper()\n    if \"TRUE\" in result:\n        return {\"verified_answer\": state[\"draft\"]}\n    return {\"cycle_count\": state.get(\"cycle_count\", 0) + 1}\n\ndef routing_gate(state: GraphState):\n    if state.get(\"verified_answer\"):\n        return \"approved\"\n    if state.get(\"cycle_count\", 0) >= 5:\n        return \"limit_reached\"\n    return \"rejected\"\n\nworkflow = StateGraph(GraphState)\nworkflow.add_node(\"retriever\", retriever_node)\nworkflow.add_node(\"synthesizer\", synthesizer_node)\nworkflow.add_node(\"fact_checker\", fact_checker_node)\n\nworkflow.set_entry_point(\"retriever\")\nworkflow.add_edge(\"retriever\", \"synthesizer\")\nworkflow.add_edge(\"synthesizer\", \"fact_checker\")\n\nworkflow.add_conditional_edges(\n    \"fact_checker\",\n    routing_gate,\n    {\n        \"approved\": END,\n        \"limit_reached\": END,\n        \"rejected\": \"synthesizer\"\n    }\n)\n\nlegal_graph = workflow.compile()\n\ndef run_agent(query: str) -> dict:\n    return legal_graph.invoke({\"query\": query, \"cycle_count\": 0})\n```\n\n`backend/server.py`)\n**What this step does:** Wraps our LangGraph brain in a web server.\n\n**Why we need it:** So our frontend UI (or any other app) can communicate with the AI securely over HTTP.\n\nCreate `backend/server.py`:\n\n``` python\nfrom fastapi import FastAPI, HTTPException\nfrom backend.schemas import ChatRequest, ChatResponse\nfrom backend.agent import run_agent\n\napp = FastAPI(title=\"UAE Legal RAG API\")\n\n@app.post(\"/chat\", response_model=ChatResponse)\nasync def chat_endpoint(request: ChatRequest):\n    try:\n        result = run_agent(request.query)\n        if not result.get(\"verified_answer\"):\n            raise HTTPException(\n                status_code=500,\n                detail=\"Safety check: Agent could not reach a verified answer within 5 retries.\"\n            )\n        return ChatResponse(\n            verified_answer=result[\"verified_answer\"],\n            sources=result.get(\"context\", [])\n        )\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=str(e))\n```\n\n**How to test:** Run `uvicorn backend.server:app --reload` in your terminal. Navigate to `http://127.0.0.1:8000/docs` in your browser. You will see the Swagger UI where you can test the `/chat` endpoint directly.\n\n*(Above: FastAPI Swagger UI running locally)*\n\n`frontend/app.py`)\n**What this step does:** Creates a visual chat window for the user.\n\n**Why we need it:** To provide an interactive web UI with expandable source citations so users can verify the AI's claims.\n\nCreate `frontend/app.py`:\n\n``` python\nimport streamlit as st\nimport requests\n\nst.set_page_config(page_title=\"UAE Legal Assistant\", page_icon=\"⚖️\")\nst.title(\"⚖️ UAE Legal Assistant\")\nst.caption(\"Multi-Agent Fact-Checked Legal Q&A (LangGraph + FastAPI)\")\n\nquery = st.text_input(\"Enter your statutory inquiry:\", placeholder=\"e.g., What is the probation period limit under UAE Labor Law?\")\n\nif st.button(\"Submit Query\", type=\"primary\"):\n    if not query.strip():\n        st.warning(\"Please provide a question.\")\n    else:\n        with st.spinner(\"Retrieving clauses, drafting answer, and running fact-checker...\"):\n            try:\n                response = requests.post(\n                    \"http://localhost:8000/chat\",\n                    json={\"query\": query},\n                    timeout=60\n                )\n                if response.status_code == 200:\n                    data = response.json()\n                    st.success(\"Verification Passed\")\n                    st.markdown(f\"**Answer:**\\n{data['verified_answer']}\")\n\n                    with st.expander(\"Inspect Referenced Statutory Clauses\"):\n                        for idx, source in enumerate(data[\"sources\"], start=1):\n                            st.info(f\"**Clause Chunk {idx}:**\\n{source}\")\n                else:\n                    st.error(f\"Error {response.status_code}: {response.text}\")\n            except requests.exceptions.ConnectionError:\n                st.error(\"Cannot connect to backend. Ensure FastAPI is running on port 8000.\")\n```\n\n**How to test:** Open a *new* terminal window (keep FastAPI running in the first one) and run `streamlit run frontend/app.py`. Your browser will open the app automatically.\n\n*(Above: The Streamlit interface querying the backend)*\n\n`Dockerfile`)\n**What this step does:** Packages the entire backend into a standardized container.\n\n**Why we need it:** So the application runs exactly the same way on any machine or cloud server, without dependency errors.\n\nCreate a `Dockerfile` in the root directory:\n\n```\nFROM python:3.10-slim\n\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\nCOPY . .\nEXPOSE 8000\n\nCMD [\"uvicorn\", \"backend.server:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]\n```\n\nBuild and run your container:\n\n```\ndocker build -t uae-legal-api .\ndocker run -p 8000:8000 --env-file .env uae-legal-api\n```\n\nYou now have a fully decoupled, multi-agent RAG application. By separating the retrieval, synthesis, and fact-checking steps, you drastically reduce hallucinations.\n\nAt the time of writing, all services used in this guide have free tiers that are sufficient for learning and experimentation.\n\nYou can view the complete source code and run the project yourself here: [MalaikaJunaid/multi-agent-rag-assistant](https://github.com/MalaikaJunaid/multi-agent-rag-assistant)\n\nIn this tutorial you:\n\nThese are the same building blocks used in production RAG systems.\n\nHappy coding!", "url": "https://wpnews.pro/news/build-a-multi-agent-rag-legal-assistant-with-langgraph-fastapi-and-streamlit", "canonical_source": "https://dev.to/malaikajunaid/build-a-multi-agent-rag-legal-assistant-with-langgraph-fastapi-and-streamlit-beginner-guide-54ka", "published_at": "2026-09-22 10:40:02+00:00", "updated_at": "2026-09-22 10:52:57.206685+00:00", "lang": "en", "topics": ["ai-agents", "large-language-models", "ai-tools", "developer-tools", "ai-infrastructure"], "entities": ["LangGraph", "FastAPI", "Streamlit", "Pinecone", "OpenRouter", "LangChain", "Hugging Face", "UAE Federal Law"], "alternates": {"html": "https://wpnews.pro/news/build-a-multi-agent-rag-legal-assistant-with-langgraph-fastapi-and-streamlit", "markdown": "https://wpnews.pro/news/build-a-multi-agent-rag-legal-assistant-with-langgraph-fastapi-and-streamlit.md", "text": "https://wpnews.pro/news/build-a-multi-agent-rag-legal-assistant-with-langgraph-fastapi-and-streamlit.txt", "jsonld": "https://wpnews.pro/news/build-a-multi-agent-rag-legal-assistant-with-langgraph-fastapi-and-streamlit.jsonld"}}