Build a rag legal research assistant that drafts briefs in under 10 minutes A developer has published a step-by-step recipe for building a Retrieval-Augmented Generation (RAG) legal research assistant that retrieves relevant case law from the CourtListener API, embeds opinions with OpenAI's text-embedding-ada-002, stores them in Pinecone or Chroma, and uses LangChain with gpt-3.5-turbo to draft a 300-word legal brief. The command-line tool, brief.py, is estimated to take four to six hours to build and can be wrapped in a Flask app or n8n workflow for internal law-firm use. You can spin up a Retrieval-Augmented Generation RAG legal research assistant in a few hours, hook it up to public case-law APIs, and have it return a concise brief in roughly ten minutes of work. The system combines a vector store of recent opinions, LangChain orchestration, and OpenAI's text-creation model so you retrieve the most relevant cases, summarize them, and let the LLM draft a brief - all with a single click. | Tool | Plan / Price | Role | |---|---|---| | Python 3.11 | Free system install | Runtime for LangChain script | | OpenAI API gpt-3.5-turbo | Pay-as-you-go, $0.002 / 1 k tokens free-tier available | Generates summaries and briefs | | LangChain ≄ 0.0.340 | Open-source, free | Chaining retrieval, LLM, and prompts | | Pinecone or Chroma locally | Free tier 1 M vectors, then $0.048 / 1 k vectors | Vector store for case embeddings | | CourtListener API Free Law Project | Free rate-limited | Pulls full-text opinions from the public database | | Docker optional | Free | Isolates the environment for reproducibility | | Git optional | Free | Version-control of the codebase | Pricing is accurate as of August 2026; verify on the provider's pricing page before you start. Estimated build time: 4-6 hours for a developer comfortable with Python and basic HTTP auth. The following recipe creates a command-line tool brief.py that accepts a legal question, retrieves the top-5 relevant opinions from CourtListener, summarizes each, and asks OpenAI to write a 300-word brief. Every step is reproducible; you can later wrap it in a Flask app or n8n workflow for internal law-firm software. Create an isolated virtual environment and install the required libraries. python -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install langchain openai pinecone-client tqdm requests If you prefer a fully local vector store, replace pinecone-client with chromadb . Tip: Keep the environment file requirements.txt in version control so you can rebuild the stack on a new machine with pip install -r requirements.txt . legal-cases . Record the us-west1-gcp . User-Agent header as recommended in the API docs. Store the secrets in a .env file never commit it . OPENAI API KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXX PINECONE API KEY=YOUR PINECONE KEY PINECONE ENV=us-west1-gcp Load them in code with python-dotenv install pip install python-dotenv or use os.getenv . The script below fetches the latest 200 opinions from CourtListener via the /search/ endpoint , extracts the plain text , embeds each with OpenAI's text-embedding-ada-002 , and upserts into Pinecone. The process runs once and can be scheduled weekly. python ingest cases.py - populates Pinecone with case embeddings import os, time, json, requests from dotenv import load dotenv from langchain.embeddings import OpenAIEmbeddings import pinecone load dotenv pinecone.init api key=os.getenv "PINECONE API KEY" , environment=os.getenv "PINECONE ENV" index name = "legal-cases" if index name not in pinecone.list indexes : pinecone.create index name=index name, dimension=1536, metric="cosine" index = pinecone.Index index name embeddings = OpenAIEmbeddings openai api key=os.getenv "OPENAI API KEY" def fetch cases page=1, page size=20 : url = "https://www.courtlistener.com/api/rest/v3/search/" params = { "type": "opinion", "page": page, "page size": page size, "order by": "-date filed" } headers = {"User-Agent": "YourLawFirmRAG/1.0"} resp = requests.get url, params=params, headers=headers resp.raise for status return resp.json "results" vectors = for page in range 1, 11 : 10 pages Ɨ 20 = 200 cases cases = fetch cases page=page, page size=20 for case in cases: text = case.get "plain text", "" if not text: continue embed = embeddings.embed query text 1536-dim vector vectors.append case "id" , embed, {"title": case "case name" , "date": case "date filed" } time.sleep 1 respect 1 req/s limit Batch upsert max 100 vectors per request batch size = 100 for i in range 0, len vectors , batch size : batch = vectors i:i+batch size index.upsert vectors=batch print f"Upserted {len vectors } case embeddings." What this does: pulls 200 recent opinions, turns each into a 1536-dim embedding, and stores them in a Pinecone index named legal-cases . After the initial run you have a searchable knowledge base that can be refreshed on a cron schedule. Now write the core assistant in brief.py . It takes a user query, retrieves the top-5 most similar cases, asks OpenAI to summarize each, and finally composes a brief. python brief.py - one-shot legal brief generator import os, sys, json from dotenv import load dotenv from langchain.vectorstores import Pinecone from langchain.embeddings import OpenAIEmbeddings from langchain.llms import OpenAI from langchain.chains import LLMChain from langchain.prompts import PromptTemplate from tqdm import tqdm load dotenv embeddings = OpenAIEmbeddings openai api key=os.getenv "OPENAI API KEY" vectorstore = Pinecone.from existing index index name="legal-cases", embedding=embeddings, namespace=None Prompt to summarize a single case SUMMARIZE PROMPT = PromptTemplate input variables= "case text", "question" , template= "You are a seasoned legal analyst. Summarize the following case excerpt " "in 120 words, focusing on how it answers the question: '{question}'.\n\n" "{case text}" Prompt to draft a brief from the collection of summaries BRIEF PROMPT = PromptTemplate input variables= "question", "summaries" , template= "Write a short ā‰ˆ300-word legal brief that answers the question:\n" "\"{question}\"\n" "Use only the information from the following case summaries. " "Cite each summary with its title and date in parentheses.\n\n" "{summaries}" def retrieve and summarize question: str, top k: int = 5 : docs = vectorstore.similarity search question, k=top k llm = OpenAI model="gpt-3.5-turbo", temperature=0.2, openai api key=os.getenv "OPENAI API KEY" summarize chain = LLMChain llm=llm, prompt=SUMMARIZE PROMPT summaries = for doc in tqdm docs, desc="Summarizing cases" : summary = summarize chain.run {"case text": doc.page content, "question": question} meta = doc.metadata header = f" {meta.get 'title', 'Unknown' } {meta.get 'date', 'N/A' } " summaries.append f"{header}\n{summary}\n" return "\n".join summaries def draft brief question: str, summaries: str : llm = OpenAI model="gpt-3.5-turbo", temperature=0.3, openai api key=os.getenv "OPENAI API KEY" brief chain = LLMChain llm=llm, prompt=BRIEF PROMPT return brief chain.run {"question": question, "summaries": summaries} if name == " main ": if len sys.argv < 2: print "Usage: python brief.py \"Legal question here\"" sys.exit 1 user question = sys.argv 1 print "šŸ”Ž Retrieving relevant opinions..." case summaries = retrieve and summarize user question print "\nāœļø Drafting brief..." result = draft brief user question, case summaries print "\n=== GENERATED BRIEF ===\n" print result What this does: gpt-3.5-turbo with a focused prompt, producing a concise 120-word synopsis. Running python brief.py "When does the doctrine of laches apply in patent infringement?" typically finishes in under 10 seconds of compute time, leaving you with a ready-to-send draft. Law-firm software often prefers HTTP endpoints. The snippet below wraps the above logic in a /brief endpoint that accepts JSON { "question": "..." } and returns the brief. python api.py - Flask wrapper install with pip install flask from flask import Flask, request, jsonify from brief import retrieve and summarize, draft brief app = Flask name @app.route "/brief", methods= "POST" def generate brief : payload = request.get json if not payload or "question" not in payload: return jsonify {"error": "Missing 'question' field"} , 400 question = payload "question" summaries = retrieve and summarize question brief = draft brief question, summaries return jsonify {"brief": brief} if name == " main ": app.run host="0.0.0.0", port=8000 Deploy this container with Docker for sandboxed execution inside your firm's DMZ: docker build -t rag-legal-assistant . docker run -d -p 8000:8000 --env-file .env rag-legal-assistant Now any internal tool can POST a legal question and receive a polished brief in under a minute. | Failure mode | Symptom | Fix / mitigation | |---|---|---| | Pinecone quota exhaustion | API returns 429 Too Many Requests after ~1 M vectors | Monitor usage in the Pinecone dashboard; split the index by jurisdiction or use the free-tier for prototypes. | | CourtListener rate limit 1 req/s | HTTP 429 from /search/ during ingestion | Implement a time.sleep 1 between page fetches already in the script and consider exponential back-off for retries. | | OpenAI token overage | Unexpected $ charge on billing page | Limit max tokens in the LLM calls max tokens=500 for summarization, max tokens=800 for briefs and enable budgeting alerts in the OpenAI console. | | Embedding drift | Retrieved cases are irrelevant after a few weeks | Re-run ingest cases.py weekly; you can add a cron job 0 2 0 to keep the vector store fresh. | | Missing plain text | Some cases return empty strings, causing zero-length embeddings and errors | Skip records without plain text as in the code or fall back to the HTML case body field and strip tags with BeautifulSoup. | | Prompt injection | Malicious user input in question manipulates the LLM output | Sanitize the incoming question: remove newlines, enforce a maximum length e.g., 200 characters , and optionally whitelist legal terms. | Warning: The OpenAI API does not guarantee that generated citations are accurate. Always run a secondary check e.g., a quick search on the original case IDs before filing any document. For a deeper technical reference, see OpenAI's docs https://platform.openai.com/docs . A single brief typically uses ~1 500 tokens for retrieval-summaries and ~3 000 tokens for the final draft. At $0.002 / 1 k tokens, the API cost is roughly $0.009 per request, plus negligible Pinecone read-costs in the free tier. Yes. Chroma , Weaviate , or FAISS are all compatible with LangChain. Swap the Pinecone import for langchain.vectorstores.Chroma and change the vectorstore initialization accordingly; no other code changes are required. CourtListener data is released under the Creative Commons Zero CC0 license, allowing unrestricted commercial use. However, you should still attribute the source per the API's terms of service: include "Data sourced from CourtListener https://www.courtlistener.com