✨Introduction:
Large Language Models (LLMs) have made it incredibly easy to build intelligent chatbots for internal tools, customer support, and personal productivity apps. In this blog, we’ll walk through how to build a production-ready LLM chatbot using Streamlit for UI, Amazon Bedrock for model inference, and LangChain for orchestration.
❓What is LLM ?
LLM (Large Language Model) is an artificial intelligence model, trained on massive internet datasets to understand text and generate some new texts, images and many more. An application powered by LLM model talks as if a person is talking to another person, sharing data, images or videos. This has a capability to understand the language of text and emotions of user. LLMs serve as the backbone of modern AI applications such as chatbots, virtual assistants, content generators, and intelligent search systems. They bridge the gap between human intent and machine intelligence, making interactions more natural, contextual, and meaningful.
❓What is Langchain and Why ?
Langchain is a open source framework, designed to develop an application, powered by LLM (Large Language Models). Langchain provides a standard interface to connect to LLM providers, ideally each LLMs are having APIs with different format for a particular purpose, now from end under perspective, this would be difficult to switch from one model to another model for fulfillment of that purpose. Switching LLMs require a change in backend API configuration as well which should not be an expected solution in real world scenario. Langchain comes up with a solution for this where it provides a standard structure to provide minimum inputs from user end which automatically changes the backend API configuration if switching LLM is triggered.
At its core, LangChain enables seamless integration between LLMs and external systems such as databases, APIs, file systems, and cloud services. This allows applications to go beyond simple question-answering and perform complex reasoning, decision-making, and multi-step workflows. LangChain also supports multiple LLM providers, including OpenAI, AWS Bedrock, Azure, and open-source models, making it flexible and cloud-agnostic. This allows developers to switch models or providers without rewriting the entire application.
📌Key Features of LangChain
- Prompt Templates – Create dynamic and reusable prompts for consistent LLM responses
- Chains – Combine multiple LLM calls and logic into a single workflow
- Memory – Maintain conversation context across interactions
- Agents – Enable LLMs to decide which tools or actions to use dynamically
- Retrievers & Vector Stores – Connect LLMs with private or enterprise data for accurate responses
🎯Objective:
In this blog, we are going to develop a streamlit UI application with advantages of Langchain to connect AWS Bedrock service to leverage LLMs.
User → Streamlit UI → LangChain → Amazon Bedrock → LLM Response → UI
🧠Architecture
🧰Components Involved
- AWS Bedrock
- Langchain
- Streamlit UI
🛠️Prerequisites:
Ensure below prerequisites are followed -
- AWS account with Amazon Bedrock access enabled
- Install below python packages -
boto3
langchain_classic
langchain_community
langchain_aws
langchain_core
streamlit
🧩Application Components
1️⃣ Sidebar Configuration of UI
import streamlit as st
def typing_indicator():
return st.markdown("""
<div class="typing">
<span>🤖 Bot is typing</span>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
""", unsafe_allow_html=True)
def autoscroll():
st.markdown("""
<script>
var chatBox = window.parent.document.querySelector('.main');
chatBox.scrollTo({ top: chatBox.scrollHeight, behavior: 'smooth' });
</script>
""", unsafe_allow_html=True)
def typing_css():
st.markdown("""
<style>
.typing {
display: flex;
align-items: center;
gap: 6px;
color: #ccc;
font-size: 15px;
font-style: italic;
opacity: 0.9;
margin: 8px 0;
}
.dot {
height: 6px;
width: 6px;
background: #ccc;
border-radius: 50%;
animation: blink 1.4s infinite both;
}
.dot:nth-child(2) { animation-delay: .2s; }
.dot:nth-child(3) { animation-delay: .4s; }
@keyframes blink {
0% { opacity: .2; }
20% { opacity: 1; }
100% { opacity: .2; }
}
/* Remove red background from buttons with stronger selectors */
div[data-testid="column"] .stButton > button,
.stButton > button,
button[kind="secondary"] {
background-color: transparent !important;
background: transparent !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
color: inherit !important;
box-shadow: none !important;
}
div[data-testid="column"] .stButton > button:hover,
.stButton > button:hover,
button[kind="secondary"]:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
background: rgba(255, 255, 255, 0.1) !important;
border: 1px solid rgba(255, 255, 255, 0.3) !important;
}
div[data-testid="column"] .stButton > button:focus,
.stButton > button:focus,
button[kind="secondary"]:focus {
background-color: transparent !important;
background: transparent !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
box-shadow: none !important;
}
</style>
""", unsafe_allow_html=True)
def apply_sidebar():
st.markdown("""
<style>
/* Sidebar container */
[data-testid="stSidebar"] {
background: linear-gradient(180deg, #141414, #1d1d1d);
padding: 2rem 1.2rem;
border-right: 1px solid #333;
animation: fadeIn 0.8s ease-out;
}
/* Fade-in animation */
@keyframes fadeIn {
0% { opacity: 0; transform: translateX(-20px); }
100% { opacity: 1; transform: translateX(0); }
}
/* Section headers */
[data-testid="stSidebar"] h1,
[data-testid="stSidebar"] h2,
[data-testid="stSidebar"] h3 {
color: #fff !important;
letter-spacing: .3px;
animation: slideIn 0.6s ease-in;
}
@keyframes slideIn {
0% { opacity: 0; transform: translateY(-10px); }
100% { opacity: 1; transform: translateY(0); }
}
/* Slider animation + glow */
.stSlider input:focus + div .thumb {
box-shadow: 0 0 12px #ff3e3e;
transition: 0.3s;
}
/* Hover animation on dropdown */
.stSelectbox > div > div:hover {
transform: scale(1.02);
transition: 0.25s ease-in-out;
}
/* Animated button style */
.stButton button {
background: #e50914;
color: white;
padding: .6rem 1.2rem;
border-radius: 8px;
border: none;
transition: .25s;
}
.stButton button:hover {
transform: translateY(-2px);
background: #ff1b2d;
box-shadow: 0 3px 10px rgba(255,0,0,0.4);
}
</style>
""", unsafe_allow_html=True)
2️⃣ Application Logic
import boto3
import json
import streamlit as st
from langchain_aws import ChatBedrockConverse
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_classic.memory import ConversationBufferMemory
from app_feature import typing_css, typing_indicator, autoscroll
def bedrock_model_logic(model_id: str, region: str, user_input: str, max_tokens: float, temperature: float):
# Apply typing CSS
typing_css()
## Define bedrock client
bedrock_client = boto3.client(
"bedrock-runtime",
region_name="us-east-1"
)
## Configuration Memory
chat_history = InMemoryChatMessageHistory()
memory = ConversationBufferMemory(
memory_key="chat_history",
chat_memory=chat_history,
return_messages=True,
ai_prefix="\n\nAssistant",
human_prefix="\n\nHuman"
)
## Define the prompt template
messages = ChatPromptTemplate.from_messages(
[
("system", "Hey Human!! I am Alps. Welcome to my place 😊"),
("human", "{user_input}"),
]
)
## Connect to Bedrock Model
llm = ChatBedrockConverse(
client=bedrock_client,
model_id=model_id,
max_tokens=max_tokens,
temperature=temperature
)
if "messages" not in st.session_state:
st.session_state.messages = []
# Display existing messages with regenerate option for assistant messages
for i, message in enumerate(st.session_state.messages):
with st.chat_message(message["role"]):
if message["role"] == "user":
col1, col2 = st.columns([9, 1])
with col1:
st.markdown(message["content"])
with col2:
if st.button("⧉", key=f"copy_user_{i}", help="Copy message"):
st.write(f'<script>navigator.clipboard.writeText(`{message["content"]}`);</script>', unsafe_allow_html=True)
else:
st.markdown(message["content"])
if message["role"] == "assistant":
col1, col2, col3, col4, col5, col6 = st.columns([1, 1, 1, 1, 1, 5])
# Get current feedback state
current_feedback = st.session_state.get(f"feedback_{i}", None)
with col1:
like_style = "✅👍" if current_feedback == "liked" else "👍"
if st.button(like_style, key=f"like_{i}", help="Good"):
st.session_state[f"feedback_{i}"] = "liked"
st.rerun()
with col2:
dislike_style = "✅👎" if current_feedback == "disliked" else "👎"
if st.button(dislike_style, key=f"dislike_{i}", help="Poor"):
st.session_state[f"feedback_{i}"] = "disliked"
st.rerun()
with col3:
love_style = "✅❤️" if current_feedback == "loved" else "❤️"
if st.button(love_style, key=f"love_{i}", help="Love"):
st.session_state[f"feedback_{i}"] = "loved"
st.rerun()
with col4:
smile_style = "✅😊" if current_feedback == "smiled" else "😊"
if st.button(smile_style, key=f"smile_{i}", help="Nice"):
st.session_state[f"feedback_{i}"] = "smiled"
st.rerun()
with col5:
if st.button("🔄", key=f"regenerate_{i}", help="Regenerate"):
# Find the corresponding user message
if i > 0 and st.session_state.messages[i-1]["role"] == "user":
user_prompt = st.session_state.messages[i-1]["content"]
# Show typing indicator while regenerating
typing_placeholder = st.empty()
with typing_placeholder:
typing_indicator()
# Generate new response
output_parser = StrOutputParser()
chain = messages|llm|output_parser
new_response = chain.invoke({"user_input": user_prompt})
# Clear typing indicator
typing_placeholder.empty()
# Update the message
st.session_state.messages[i]["content"] = new_response
autoscroll() # Auto-scroll after regeneration
st.rerun()
if user_input:
with st.chat_message("user"):
col1, col2 = st.columns([9, 1])
with col1:
st.markdown(user_input)
with col2:
if st.button("⧉", key="copy_user_new", help="Copy"):
st.write(f'<script>navigator.clipboard.writeText(`{user_input}`);</script>', unsafe_allow_html=True)
st.session_state.messages.append({"role": "user", "content": user_input})
# Show typing indicator while generating response
typing_placeholder = st.empty()
with typing_placeholder:
typing_indicator()
output_parser = StrOutputParser()
chain = messages|llm|output_parser
response = chain.invoke({"user_input": user_input})
# Clear typing indicator
typing_placeholder.empty()
with st.chat_message("assistant"):
st.markdown(response)
autoscroll() # Auto-scroll after new message
# Add feedback emojis for new response
col1, col2, col3, col4, col5, col6 = st.columns([1, 1, 1, 1, 1, 5])
# Get current feedback state for new message
new_msg_index = len(st.session_state.messages)
current_feedback = st.session_state.get(f"feedback_{new_msg_index}", None)
with col1:
like_style = "✅👍" if current_feedback == "liked" else "👍"
if st.button(like_style, key="like_new", help="Good response"):
st.session_state[f"feedback_{new_msg_index}"] = "liked"
st.rerun()
with col2:
dislike_style = "✅👎" if current_feedback == "disliked" else "👎"
if st.button(dislike_style, key="dislike_new", help="Poor response"):
st.session_state[f"feedback_{new_msg_index}"] = "disliked"
st.rerun()
with col3:
love_style = "✅❤️" if current_feedback == "loved" else "❤️"
if st.button(love_style, key="love_new", help="Love this response"):
st.session_state[f"feedback_{new_msg_index}"] = "loved"
st.rerun()
with col4:
smile_style = "✅😊" if current_feedback == "smiled" else "😊"
if st.button(smile_style, key="smile_new", help="Nice response"):
st.session_state[f"feedback_{new_msg_index}"] = "smiled"
st.rerun()
with col5:
if st.button("🔄", key="regenerate_new", help="Regenerate response"):
# Show typing indicator while regenerating
typing_placeholder = st.empty()
with typing_placeholder:
typing_indicator()
new_response = chain.invoke({"user_input": user_input})
# Clear typing indicator
typing_placeholder.empty()
st.session_state.messages.append({"role": "assistant", "content": new_response})
autoscroll() # Auto-scroll after regeneration
st.rerun()
st.session_state.messages.append({"role": "assistant", "content": response})
3️⃣ Streamlit UI Configuration
import boto3
import streamlit as st
from bedrock_model import bedrock_model_logic
from app_feature import apply_sidebar
## Set page configuration
st.set_page_config(page_title="Chatbot", page_icon="img.png", layout="wide")
def app():
## Sidebar Settings:
apply_sidebar()
## Title
st.title(":rainbow[🦜Langchain ChatBot🦜]")
## List of models
model_list = [
"anthropic.claude-3-sonnet-20240229-v1:0",
"anthropic.claude-3-haiku-20240307-v1:0",
"cohere.command-r-plus-v1:0",
"cohere.command-r-v1:0"
]
## Type User Prompt
user_input = st.chat_input("Ask something")
## Define Streamlit Properties
with st.sidebar:
st.title('Settings')
model_id = st.selectbox("### 📈 Select Model", model_list)
temperature = st.slider("### 🔥 Temperature", min_value=0.0, max_value=1.0, value=0.7, step=0.1, help="Higher = more creative output | Lower = more factual")
max_tokens = st.slider("### 🧩 Max Tokens", min_value=100, max_value=2048, value=1024, step=100)
if st.button("New Message", type="primary"):
st.session_state.messages = []
st.rerun()
st.divider()
# Display user prompts
st.title("Chat History")
if "messages" in st.session_state:
user_prompts = [msg["content"] for msg in st.session_state.messages if msg["role"] == "user"]
if user_prompts:
for i, prompt in enumerate(user_prompts, 1):
# with st.expander(f"Prompt {i}"):
st.write(prompt)
else:
st.write("No prompts yet")
else:
st.write("No prompts yet")
region = "us-east-1"
bedrock_model_logic(model_id, region, user_input, max_tokens, temperature)
app()
🚀Deployment Configuration
In this project, we have containerized the application using Docker and deployed in Amazon ECS service with FARGATE launch type. There are two ECS containers configured behind the application load balancer where traffic will come at 8501 port from the load balancer with proper listener configuration at 80 port.
Below are the resources created as part of the deployment -
- Elastic Container Repository
- Elastic Container Service
- Application Load Balancer
Dockerfile
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY Chatbot/ ./Chatbot/
EXPOSE 8501
CMD ["streamlit", "run", "Chatbot/chatbot.py", "--server.port=8501", "--server.address=0.0.0.0"]
var.tf
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Variables of ALB~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
variable "TG_conf" {
type = object({
name = string
port = string
protocol = string
target_type = string
enabled = bool
healthy_threshold = string
interval = string
path = string
})
}
variable "ALB_conf" {
type = object({
name = string
internal = bool
load_balancer_type = string
ip_address_type = string
})
}
variable "Listener_conf" {
type = map(object({
port = string
protocol = string
type = string
priority = number
}))
}
variable "alb_tags" {
description = "provides the tags for ALB"
type = object({
Environment = string
Email = string
Type = string
Owner = string
})
default = {
Email = "dasanirban9019@gmail.com"
Environment = "Dev"
Owner = "Anirban Das"
Type = "External"
}
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Variables of ECR~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
variable "ecr_repo" {
description = "Name of repository"
default = "streamlit-chatbot"
}
variable "ecr_tags" {
type = map(any)
default = {
"AppName" = "StreamlitApp"
"Env" = "Dev"
}
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Variables of ECS~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
variable "region" {
type = string
default = "us-east-1"
}
variable "ecs_role" {
description = "ecs roles"
default = "ecsTaskExecutionRole"
}
variable "ecs_details" {
description = "details of ECS cluster"
type = object({
Name = string
logging = string
cloud_watch_encryption_enabled = bool
})
}
variable "ecs_task_def" {
description = "defines the configurations of task definition"
type = object({
family = string
cont_name = string
cpu = number
cpu_allocations = number
mem_allocations = number
memory = number
essential = bool
logdriver = string
containerport = number
networkmode = string
requires_compatibilities = list(string)
})
}
variable "cw_log_grp" {
description = "defines the log group in cloudwatch"
type = string
default = ""
}
variable "kms_key" {
description = "defines the kms key"
type = object({
description = string
deletion_window_in_days = number
})
}
variable "custom_tags" {
description = "defines common tags"
type = object({})
default = {
AppName = "StreamlitApp"
Env = "Dev"
}
}
variable "ecs_task_count" {
description = "ecs task count"
type = number
default = 2
}
terraform.tfvars
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Terraform/terraform.tfvars of ALB~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
TG_conf = {
enabled = true
healthy_threshold = "2"
interval = "30"
name = "ChatbotTG"
port = "8501"
protocol = "HTTP"
target_type = "ip"
path = "/"
}
ALB_conf = {
internal = false
ip_address_type = "ipv4"
load_balancer_type = "application"
name = "ALB-Chatbot"
}
Listener_conf = {
"1" = {
port = "80"
priority = 100
protocol = "HTTP"
type = "forward"
}
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Terraform/terraform.tfvars of ECS~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
ecs_details = {
Name = "Chatbot"
logging = "OVERRIDE"
cloud_watch_encryption_enabled = true
}
ecs_task_def = {
family = "custom-task-definition-chatbot"
cont_name = "streamlit-chatbot"
cpu = 1024
cpu_allocations = 800
memory = 3072
mem_allocations = 2000
essential = true
logdriver = "awslogs"
containerport = 8501
networkmode = "awsvpc"
requires_compatibilities = ["FARGATE", ]
}
cw_log_grp = "cloudwatch-log-group-ecs-cluster-chatbot"
kms_key = {
description = "log group encryption"
deletion_window_in_days = 7
}
data.tf
# vpc details :
data "aws_vpc" "this_vpc" {
state = "available"
filter {
name = "tag:Name"
values = ["custom-vpc"]
}
}
# subnets details :
data "aws_subnet" "web_subnet_1a" {
vpc_id = data.aws_vpc.this_vpc.id
filter {
name = "tag:Name"
values = ["weblayer-pub1-1a"]
}
}
data "aws_subnet" "web_subnet_1b" {
vpc_id = data.aws_vpc.this_vpc.id
filter {
name = "tag:Name"
values = ["weblayer-pub2-1b"]
}
}
# ALB security group details :
data "aws_security_group" "ext_alb" {
filter {
name = "tag:Name"
values = ["ALBSG"]
}
}
data "aws_security_group" "streamlit_app" {
filter {
name = "tag:Name"
values = ["StreamlitAppSG"]
}
}
iam.tf
resource "aws_iam_role" "ecsTaskExecutionRole" {
name = var.ecs_role
assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
}
data "aws_iam_policy_document" "assume_role_policy" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
}
}
locals {
policy_arn = [
"arn:aws:iam::aws:policy/AdministratorAccess",
"arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role",
"arn:aws:iam::669122243705:policy/CustomPolicyECS"
]
}
resource "aws_iam_role_policy_attachment" "ecsTaskExecutionRole_policy" {
count = length(local.policy_arn)
role = aws_iam_role.ecsTaskExecutionRole.name
policy_arn = element(local.policy_arn, count.index)
}
ecr.tf
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~AWS ECR Repository~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
resource "aws_ecr_repository" "aws-ecr" {
name = var.ecr_repo
tags = var.ecr_tags
}
ecs.tf
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~AWS ECS Cluster~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
resource "aws_ecs_cluster" "aws-ecs-cluster" {
name = var.ecs_details["Name"]
configuration {
execute_command_configuration {
kms_key_id = aws_kms_key.kms.arn
logging = var.ecs_details["logging"]
log_configuration {
cloud_watch_encryption_enabled = true
cloud_watch_log_group_name = aws_cloudwatch_log_group.log-group.name
}
}
}
tags = var.custom_tags
}
resource "aws_ecs_task_definition" "taskdef" {
family = var.ecs_task_def["family"]
container_definitions = jsonencode([
{
"name" : "${var.ecs_task_def["cont_name"]}",
"image" : "${aws_ecr_repository.aws-ecr.repository_url}:v1",
"entrypoint" : [],
"essential" : "${var.ecs_task_def["essential"]}",
"logConfiguration" : {
"logDriver" : "${var.ecs_task_def["logdriver"]}",
"options" : {
"awslogs-group" : "${aws_cloudwatch_log_group.log-group.id}",
"awslogs-region" : "${var.region}",
"awslogs-stream-prefix" : "app-dev"
}
},
"portMappings" : [
{
"containerPort" : "${var.ecs_task_def["containerport"]}",
}
],
"cpu" : "${var.ecs_task_def["cpu_allocations"]}",
"memory" : "${var.ecs_task_def["mem_allocations"]}",
"networkMode" : "${var.ecs_task_def["networkmode"]}"
}
])
requires_compatibilities = var.ecs_task_def["requires_compatibilities"]
network_mode = var.ecs_task_def["networkmode"]
memory = var.ecs_task_def["memory"]
cpu = var.ecs_task_def["cpu"]
execution_role_arn = aws_iam_role.ecsTaskExecutionRole.arn
task_role_arn = aws_iam_role.ecsTaskExecutionRole.arn
tags = var.custom_tags
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~AWS CloudWatch Log Group~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
resource "aws_cloudwatch_log_group" "log-group" {
name = var.cw_log_grp
tags = var.custom_tags
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~AWS KMS Key~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
resource "aws_kms_key" "kms" {
description = var.kms_key["description"]
deletion_window_in_days = var.kms_key["deletion_window_in_days"]
tags = var.custom_tags
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ECS Service~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
resource "aws_ecs_service" "streamlit" {
name = "service-chatbot"
cluster = aws_ecs_cluster.aws-ecs-cluster.id
task_definition = aws_ecs_task_definition.taskdef.arn
desired_count = var.ecs_task_count
launch_type = "FARGATE"
load_balancer {
target_group_arn = aws_lb_target_group.this_tg.arn
container_name = "${var.ecs_task_def["cont_name"]}"
container_port = "${var.ecs_task_def["containerport"]}"
}
network_configuration {
assign_public_ip = true
subnets = [data.aws_subnet.web_subnet_1a.id, data.aws_subnet.web_subnet_1b.id]
security_groups = [data.aws_security_group.streamlit_app.id]
}
}
alb.tf
resource "aws_lb_target_group" "this_tg" {
name = var.TG_conf["name"]
port = var.TG_conf["port"]
protocol = var.TG_conf["protocol"]
vpc_id = data.aws_vpc.this_vpc.id
health_check {
enabled = var.TG_conf["enabled"]
healthy_threshold = var.TG_conf["healthy_threshold"]
interval = var.TG_conf["interval"]
path = var.TG_conf["path"]
}
target_type = var.TG_conf["target_type"]
tags = {
Attached_ALB_dns = aws_lb.this_alb.dns_name
}
}
resource "aws_lb" "this_alb" {
name = var.ALB_conf["name"]
load_balancer_type = var.ALB_conf["load_balancer_type"]
ip_address_type = var.ALB_conf["ip_address_type"]
internal = var.ALB_conf["internal"]
security_groups = [data.aws_security_group.ext_alb.id]
subnets = [data.aws_subnet.web_subnet_1a.id, data.aws_subnet.web_subnet_1b.id]
tags = merge(var.alb_tags)
}
resource "aws_lb_listener" "this_alb_lis" {
for_each = var.Listener_conf
load_balancer_arn = aws_lb.this_alb.arn
port = each.value["port"]
protocol = each.value["protocol"]
default_action {
type = each.value["type"]
target_group_arn = aws_lb_target_group.this_tg.arn
}
}
.gitlab-ci.yml
default:
tags:
- anirban
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
URL: <account-number>.dkr.ecr.us-east-1.amazonaws.com/
REPO: streamlit-chatbot
TAG: v1
stages:
- Image_Build
- Resources_Build
Image Build:
stage: Image_Build
image: docker:latest
services:
- docker:dind
script:
- echo "~~~~~~~~~~~~~~~~~~~~~~~~Build ECR Repo and Push the Docker Image ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
- terraform -chdir=Terraform init
- terraform -chdir=Terraform plan -target=aws_ecr_repository.aws-ecr
- terraform -chdir=Terraform apply -target=aws_ecr_repository.aws-ecr -auto-approve
- echo '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Validate if the docker image exists ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'
- |
if ! sudo docker images | awk '{print $1}' | grep $URL$REPO; then
echo "Docker image not found."
echo "~~~~~~~~~~~~~~~~~~~~~~~~Building Docker Image~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
sudo docker build -t streamlit-chatbot-app:latest .
sleep 60
echo "~~~~~~~~~~~~~~~~~~~~~~~~Logging in to AWS ECR~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
sudo aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $URL
echo "~~~~~~~~~~~~~~~~~~~~~~~~Pushing image to AWS ECR~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
sudo docker tag streamlit-chatbot-app:latest $URL$REPO:$TAG
sudo docker push $URL$REPO:$TAG
else
echo "~~~~~~~~~~~~~~~~~~~~~~~~Docker image already exists~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
fi
artifacts:
paths:
- Terraform/.terraform/
- Terraform/terraform.tfstate*
expire_in: 1 hour
except:
changes:
- README.md
Resource Build:
stage: Resources_Build
script:
- terraform -chdir=Terraform init
- terraform -chdir=Terraform plan
- terraform -chdir=Terraform apply -auto-approve
dependencies:
- Image Build
except:
changes:
- README.md
Application URL: http://alb-chatbot-733072597.us-east-1.elb.amazonaws.com/
Repository Link: https://github.com/dasanirban834/build-llm-chatbot-using-langchain.git
🏁Conclusion:
Building an LLM-powered chatbot no longer requires complex infrastructure or deep ML expertise. By combining Streamlit for a clean and interactive UI, Amazon Bedrock for secure and scalable access to foundation models, and LangChain for prompt orchestration and memory management, you can rapidly develop a powerful, enterprise-ready conversational AI application.
This architecture strikes a perfect balance between simplicity and extensibility. You can start with a basic chatbot in minutes and gradually evolve it into a sophisticated assistant by adding features like persistent memory, RAG with enterprise documents, authentication, analytics, and multi-model support—all while staying within the AWS ecosystem.
Whether you are building an internal productivity tool, a customer-facing assistant, or experimenting with GenAI use cases, this approach provides a strong foundation that is both future-proof and production-friendly.
With the right prompts, thoughtful UX, and responsible model usage, your chatbot can become more than just a demo—it can be a real business enabler.
Happy building and exploring the power of Generative AI! 🚀




Top comments (0)