In the modern enterprise, exposing Large Language Models (LLMs) via REST APIs is no longer just about sending a prompt and getting a response. It requires strict identity management, data isolation, auditability, and highly reliable answer generation.
This article provides an end-to-end guide to designing and implementing a secure REST API for an Enterprise Multi-Agent Retrieval-Augmented Generation (RAG) system. We will use AWS API Gateway and Amazon Cognito for the secure perimeter, JWT for identity propagation, and LangGraph to orchestrate a multi-agent RAG backend.
1. Real-Time Use Case: "FinSecure" Internal Analyst Portal
The Scenario: A global financial institution needs an internal Q&A tool for analysts to query sensitive SEC filings, internal risk reports, and market data.
The Requirements:
Strict Access Control: Analysts in the "Equities" department must not see "Fixed Income" restricted documents.
High Accuracy: Simple RAG fails on complex financial queries. We need a multi-agent system to route, retrieve, and grade answers to prevent hallucinations.
Enterprise Security: The API must be protected by enterprise-grade IAM (Cognito) and rate-limited (API Gateway).
2. Phase 1: AWS Infrastructure (Cognito & API Gateway)
Before writing application code, we must establish the secure perimeter.
Step 1: Amazon Cognito (Identity Provider)
Create a Cognito User Pool. Crucially, add a Custom Attribute called department (e.g., custom:department). This will be embedded in the JWT and used later for data isolation in our RAG pipeline.
Step 2: API Gateway (The Secure Perimeter)
Create an HTTP API or REST API in API Gateway.
Create a Cognito Authorizer. Point it to your User Pool.
Attach the Authorizer to your
/queryroute.Mapping Template (Crucial): Configure the Integration Request to pass the Cognito claims to the backend. In API Gateway, you map
$context.authorizer.claimsto the payload sent to your Lambda/FastAPI backend.

3. Phase 2: Designing the Multi-Agent LangGraph RAG
Why multi-Agent? A single "Retrieve and Generate" prompt often fails in enterprise settings. It doesn't know when to stop retrieving, doesn't check its own work, and ignores user permissions.
We will design a LangGraph with four agents:
Router Agent: Analyzes the query. Is it a financial question? Does the user's department (from JWT) have permission to ask it?
Retriever Agent: Fetches documents from the Vector DB, applying strict metadata filtering based on the user's JWT claims.
Generator Agent: Synthesizes the answer.
Grader Agent: Evaluates the generated answer against the retrieved context. If it's a hallucination, it routes back to the Generator.
4. Phase 3: End-to-End Code Implementation
We will use FastAPI (which can be deployed on AWS Lambda via Mangum or on ECS) to handle the REST API, parse the JWT, and invoke the LangGraph.
Prerequisites
pip install fastapi uvicorn langgraph langchain langchain-openai boto3 mangumStep 1: Define the LangGraph State and Agents
# graph_agents.py
from typing import TypedDict, Sequence, List, Literal
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langgraph.graph import StateGraph, END
import operator
# 1. Define the State
class AgentState(TypedDict):
messages: Sequence[BaseMessage]
user_context: dict # Injected from JWT via API Gateway
documents: List[str]
next_step: str
# Initialize LLM
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# 2. Define the Nodes (Agents)
def router_agent(state: AgentState):
"""Checks if query is valid and within user's department scope."""
user_dept = state["user_context"].get("department", "unknown")
query = state["messages"][0].content
prompt = ChatPromptTemplate.from_template(
"You are a security router. User is in department: {dept}. "
"Query: {query}. "
"If the query is about finance, reply 'RETRIEVE'. If it's out of scope or asks for other departments' data, reply 'DENY'."
)
chain = prompt | llm
response = chain.invoke({"dept": user_dept, "query": query})
if "DENY" in response.content.upper():
return {"messages": [AIMessage(content="Access denied or out of scope.")], "next_step": "end"}
return {"next_step": "retrieve"}
def retriever_agent(state: AgentState):
"""Simulates Vector DB retrieval with metadata filtering."""
user_dept = state["user_context"].get("department")
query = state["messages"][0].content
# In production, pass user_dept as metadata filter to your Vector DB (e.g., OpenSearch/Pinecone)
# docs = vector_db.similarity_search(query, filter={"department": user_dept})
# Simulated retrieval
mock_docs = [
f"Document 1: Q3 Financial Report for {user_dept} division. Revenue up 10%.",
f"Document 2: Risk assessment for {user_dept} assets."
]
return {"documents": mock_docs, "next_step": "generate"}
def generator_agent(state: AgentState):
"""Generates the answer based on retrieved documents."""
docs = "\n".join(state["documents"])
query = state["messages"][0].content
prompt = ChatPromptTemplate.from_template(
"Context:\n{context}\n\nQuestion: {query}\n\nAnswer the question using only the context."
)
chain = prompt | llm
response = chain.invoke({"context": docs, "query": query})
return {"messages": [AIMessage(content=response.content)], "next_step": "grade"}
def grader_agent(state: AgentState):
"""Grades the answer. If hallucinated, loops back to generator."""
# Simulated grading logic
last_message = state["messages"][-1].content
docs = " ".join(state["documents"])
# In production, use an LLM to grade: "Does the answer strictly follow the context?"
is_hallucination = "Q4" in last_message and "Q4" not in docs # Dummy logic
if is_hallucination:
return {"next_step": "generate"} # Loop back
return {"next_step": "end"}
# 3. Build the Graph
def create_rag_graph():
workflow = StateGraph(AgentState)
workflow.add_node("router", router_agent)
workflow.add_node("retrieve", retriever_agent)
workflow.add_node("generate", generator_agent)
workflow.add_node("grade", grader_agent)
workflow.set_entry_point("router")
# Conditional edges
workflow.add_conditional_edges(
"router",
lambda x: x["next_step"],
{"retrieve": "retrieve", "end": END, "deny": END}
)
workflow.add_edge("retrieve", "generate")
workflow.add_conditional_edges(
"grade",
lambda x: x["next_step"],
{"generate": "generate", "end": END}
)
return workflow.compile()
rag_graph = create_rag_graph()
Join the conversation! Your thoughts help the community grow.