← All posts
How-ToAugust 31, 2026 · 8 min

Build a rag legal research assistant that drafts briefs in under 10 minutes

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.


What you need

ToolPlan / Price*Role
Python 3.11Free (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.340Open-source, freeChaining retrieval, LLM, and prompts
Pinecone (or Chroma locally)Free tier 1 M vectors, then $0.048 / 1 k vectorsVector store for case embeddings
CourtListener API (Free Law Project)Free (rate-limited)Pulls full-text opinions from the public database
Docker (optional)FreeIsolates the environment for reproducibility
Git (optional)FreeVersion-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.

1. Set up the Python environment

Create an isolated virtual environment and install the required libraries.

bash
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.

2. Obtain API credentials

OpenAI: Sign up at https://platform.openai.com/account/api-keys and copy the secret key. Pinecone: Register at https://app.pinecone.io and create a new index (e.g., legal-cases). Record the API key and environment (e.g., us-west1-gcp). CourtListener: No key is required for the free endpoint, but you must respect the rate limit of 1 request / second*; use the User-Agent header as recommended in the API docs.

Store the secrets in a .env file (never commit it).

dotenv
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.

3. Build the vector index of case law

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.

4. Create the RAG retrieval-to-generation chain

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:

  1. Retrieval - vector similarity search returns the five most relevant opinions.
  2. Summarization - each opinion is sent to gpt-3.5-turbo with a focused prompt, producing a concise 120-word synopsis.
  3. Generation - a second LLM call stitches the snippets into a coherent brief, citing each source.

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.

5. Optional: expose the workflow via a simple Flask API

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:

bash
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.


Where this breaks

Failure modeSymptomFix / mitigation
Pinecone quota exhaustionAPI returns 429 Too Many Requests after ~1 M vectorsMonitor 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 ingestionImplement a time.sleep(1) between page fetches (already in the script) and consider exponential back-off for retries.
OpenAI token overageUnexpected $ charge on billing pageLimit 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 driftRetrieved cases are irrelevant after a few weeksRe-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 errorsSkip records without plain_text (as in the code) or fall back to the HTML case_body field and strip tags with BeautifulSoup.
Prompt injectionMalicious user input in question manipulates the LLM outputSanitize 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.

FAQ

How much does this cost per brief? 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.

Can I replace Pinecone with an on-prem vector DB? 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.

What if I need to work offline or behind a strict firewall? Run the entire stack inside Docker without external network access after the initial ingestion. Use the text-embedding-ada-002 model via the OpenAI Azure private endpoint, or replace it with a locally hosted embedding model such as sentence-transformers/all-mpnet-base-v2.


Building a rag legal research assistant that pulls case law from public APIs and drafts briefs in about ten minutes is entirely feasible with openly available tools. By structuring the workflow with LangChain, a vector store, and OpenAI's generation models, you get a reproducible pipeline that law firms can internalize, brand, and sell as a productivity-boosting service.

If you're hungry for more ready-made automations you can offer to clients, check out our guide to AI automations you can sell. And for a deeper dive into prompt engineering and RAG best practices, grab the free guide.

Happy building, and remember: the real value comes from the curation of the right cases, not from a flashier model.

Get the full toolkit

Grab the free guide with the node-by-node build for all 10 automations.

No spam. Unsubscribe anytime. Just the good stuff.