Tags: RAG, Agentic AI, LangGraph, LangChain, Legal NLP, Chroma, FAISS
License: MIT
Version: 0.1.0
JusticeGPT β IPC Legal Copilot is a multi-agent Retrieval-Augmented Generation (RAG) assistant for the Indian Penal Code (IPC). The IPC corpus is split into two halves and indexed into two vector stores (Chroma + FAISS). A condense node converts follow-up questions (e.g., βIs it bailable?β) into standalone queries using a single shared memory, then two retrieval agents query their respective stores and a synthesizer merges their outputs into one answer. The project includes a Flask UI with a βThinkingβ¦β spinner and an auto-build step that creates vector stores on first run and skips rebuilding thereafter.
Single-index chatbots often fail when users ask follow-ups that rely on conversation context. Legal queries frequently use pronouns (βitβ, βthat sectionβ), so retrieval must resolve these references before searching. JusticeGPT demonstrates a practical pattern:
Use cases: quick legal reference for students/educators, legal researchers, and developers learning agentic RAG.
pypdf
.RecursiveCharacterTextSplitter
with chunk_size=1500
, chunk_overlap=200
.chroma_db/ipc_part1
)faiss_db/ipc_part2
)docs/
and builds both stores. Later runs detect existing stores and skip rebuild.%%{init: {'securityLevel': 'loose', 'flowchart': {'htmlLabels': true}} }%% flowchart LR U[User]:::user --> C[[Condense = follow-up β standalone]]:::condense C --> A1[Agent 1<br/>Retriever = Chroma /ipc_part1]:::agent C --> A2[Agent 2 Retriever = FAISS /ipc_part2]:::agent A1 --> S[[Synthesizer LLM merge]]:::synth A2 --> S S --> R((Final Answer)):::answer C -. uses history .- M[(Shared Memory = ConversationBufferMemory)]:::memory A1 -. context .- M A2 -. context .- M classDef user fill:#E3F2FD,stroke:#1E88E5,stroke-width:1.5px,color:#0D47A1; classDef condense fill:#FFF3E0,stroke:#FB8C00,stroke-width:1.5px,color:#E65100; classDef agent fill:#E8F5E9,stroke:#43A047,stroke-width:1.5px,color:#1B5E20; classDef synth fill:#F3E5F5,stroke:#8E24AA,stroke-width:1.5px,color:#4A148C; classDef answer fill:#E0F7FA,stroke:#0097A7,stroke-width:1.5px,color:#006064; classDef memory fill:#FFFDE7,stroke:#FBC02D,stroke-width:1.5px,color:#7F6F00; linkStyle 0 stroke:#1E88E5,stroke-width:1.5px; linkStyle 1 stroke:#43A047,stroke-width:1.2px; linkStyle 2 stroke:#43A047,stroke-width:1.2px; linkStyle 3 stroke:#8E24AA,stroke-width:1.2px; linkStyle 4 stroke:#8E24AA,stroke-width:1.2px; linkStyle 5 stroke:#0097A7,stroke-width:1.5px;
Flow:
User β Condense (rewrite follow-up β standalone) β Agent 1 (Chroma, part-1) + Agent 2 (FAISS, part-2) β Synthesizer (LLM merge) β Final answer
Shared memory (one ConversationBufferMemory
) is used by condense and both agents to keep context.
Components
--CONDENSE--
prompt (prompts/promt.txt
) to rewrite the latest message into a standalone query.RetrievalQA(stuff)
over ipc_part1
, top-k=4.RetrievalQA(stuff)
over ipc_part2
, top-k=4.--SYNTH--
prompt to produce a concise, statute-grounded final.{condense, agent1, agent2, synth}
; state includes query
, standalone_query
, ans1
, ans2
, final
.ChatOpenAI
(default gpt-4o
, temperature=0
), OpenAIEmbeddings
(default text-embedding-3-small
).%%{init: {'securityLevel': 'loose', 'flowchart': {'htmlLabels': true}} }%% flowchart LR S{First Run?}:::decision S -- No --> Load[Load Existing Stores<br/>Chroma + FAISS]:::load Load --> Ready((Ready to Serve)):::ready S -- Yes --> DL[Download IPC PDF]:::download --> EX[Extract Text]:::extract --> CH[Chunk & Split]:::chunk CH --> CBuild[Build Chroma = ipc_part1]:::chroma CH --> FBuild[Build FAISS = ipc_part2]:::faiss CBuild --> PS[Persist Indexes]:::persist FBuild --> PS PS --> Ready classDef decision fill:#FFFDE7,stroke:#FBC02D,stroke-width:1.5px,color:#6D4C00; classDef load fill:#DCEDC8,stroke:#7CB342,stroke-width:1.5px,color:#33691E; classDef ready fill:#E0F7FA,stroke:#00838F,stroke-width:1.5px,color:#005662; classDef download fill:#E3F2FD,stroke:#1E88E5,stroke-width:1.5px,color:#0D47A1; classDef extract fill:#F3E5F5,stroke:#8E24AA,stroke-width:1.5px,color:#4A148C; classDef chunk fill:#FFF3E0,stroke:#FB8C00,stroke-width:1.5px,color:#E65100; classDef chroma fill:#E8F5E9,stroke:#43A047,stroke-width:1.5px,color:#1B5E20; classDef faiss fill:#E8EAF6,stroke:#3949AB,stroke-width:1.5px,color:#1A237E; classDef persist fill:#ECEFF1,stroke:#607D8B,stroke-width:1.5px,color:#263238; linkStyle 0 stroke:#7CB342,stroke-width:1.2px; linkStyle 1 stroke:#00838F,stroke-width:1.5px; linkStyle 2 stroke:#1E88E5,stroke-width:1.2px; linkStyle 3 stroke:#8E24AA,stroke-width:1.2px; linkStyle 4 stroke:#FB8C00,stroke-width:1.2px; linkStyle 5 stroke:#43A047,stroke-width:1.2px; linkStyle 6 stroke:#3949AB,stroke-width:1.2px; linkStyle 7 stroke:#607D8B,stroke-width:1.2px; linkStyle 8 stroke:#00838F,stroke-width:1.5px;
Logic:
If stores exist β load & serve.
Else β download IPC PDF β extract β chunk β split β build Chroma & FAISS β persist β serve.
This release emphasizes functionality and reproducibility. Suggested evaluation protocol:
Expected behavior: After warm-up, retrieval is local/fast; latency dominated by LLM calls. Condense significantly improves follow-up accuracy vs. naive chat.
allow_dangerous_deserialization=True
for your own locally-saved index; never load untrusted pickles.Pinned in requirements.txt
(notable pins: numpy<2.0
, langgraph==0.5.4
, langchain==0.2.*
, langchain-openai
, langchain-chroma
, chromadb==0.4.*
, faiss-cpu
, pypdf
, flask
).
python3.13 -m venv .venv && source .venv/bin/activate pip install --upgrade pip pip install -r requirements.txt cp .env.example .env # Edit .env: # OPENAI_API_KEY=sk-... # (optional) OPENAI_CHAT_MODEL=gpt-4o # (optional) OPENAI_EMBED_MODEL=text-embedding-3-small # (optional) CHROMA_TELEMETRY=FALSE
You can watch the video here: