DEV Community

Cover image for Develop a LLM Chatbot Using Streamlit+Bedrock+Langchain

Develop a LLM Chatbot Using Streamlit+Bedrock+Langchain

✨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
Enter fullscreen mode Exit fullscreen mode

🧠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
Enter fullscreen mode Exit fullscreen mode

🧩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)
Enter fullscreen mode Exit fullscreen mode

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})
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

🚀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"]
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"]
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

ecr.tf

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~AWS ECR Repository~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

resource "aws_ecr_repository" "aws-ecr" {
  name = var.ecr_repo
  tags = var.ecr_tags
}

Enter fullscreen mode Exit fullscreen mode

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]
  }

}


Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

.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
Enter fullscreen mode Exit fullscreen mode

ECS Service:

Application Load Balancer

Streamlit Application UI

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)