Skip to content

Commit 8d6b589

Browse files
Add tutorial for chatbot application with LangChain and Streamlit (#979)
* Add Langchain chatbot example with Docker and Kubernetes configuration * Refactor Langchain chatbot to use PostgreSQL instead of Redis * Enhance Langchain chatbot with user-specific chat IDs and reset functionality; update Docker and Terraform configurations * Remove InferenceService configuration * Correct typos * Rewrite manual to include manual deployment steps * Update README and Terraform configurations to clarify GKE cluster usage * Remove Helm provider * Update README to clarify IAP setup and OAuth client creation steps * Specify language model requirements * Add optional section for provisioning GKE cluster * Include instructions for port-forwarding KServe model service * Refine instructions for creating Cloud SQL instance and VPC Peering * Enhance instructions for deploying the application to GKE * Update README to recommend using tuned Gemma2 model * Clarify Ingress and A record setup * Clarify the importance of configuring the redirect URI for OAuth 2.0 client ID * Enhance IAP configuration instructions for better clarity on access control * Add troubleshooting and common errors sections * Refine README for LangChain chatbot tutorial: improve clarity, update links, and enhance GKE provisioning instructions * Emphasize the importance of accessibility, reliability, and consistency in AI applications * Add architecture overview and renumber deployment steps for clarity * Enhance clarity of the intro and domain handling section * Update README title * Clarify domain variable usage * Apply suggestions from code review Co-authored-by: igooch <[email protected]> * Fix variable substitution in Cloud Build configuration * Update Docker run command for proper host resolution * Clarify Docker image naming in configuration * Add note about using Cloud SQL proxy as an alternative for VPC Peering * Add a note about network name * Add a note that CloudSQL instance creation takes some time * Add details about Cloud SQL instance connection string * Fix typo * Add sample output for gcloud compute addresses describe * Clarify DNS propagation time for A record setup * Fix typos * Add examples for model base URL * Clarify instructions for configuring permissions for IAP * Clarify model base URL and name * Clarify expected provisioning time for Managed Certificate * Clarify instructions for granting principal access in IAP configuration * Format code * Disable special characters in database password generation * Clarify deployment instructions for the instruction-tuned Gemma2 model --------- Co-authored-by: igooch <[email protected]>
1 parent 6ce872b commit 8d6b589

18 files changed

+1106
-0
lines changed

tutorials-and-examples/langchain-chatbot/README.md

+420
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
FROM python:3.9-slim
2+
3+
WORKDIR /app
4+
5+
RUN apt-get update && \
6+
apt-get install -y --no-install-recommends \
7+
build-essential \
8+
software-properties-common \
9+
libpq-dev && \
10+
apt-get clean && \
11+
rm -rf /var/lib/apt/lists/*
12+
13+
COPY requirements.txt ./
14+
RUN pip3 install --no-cache-dir -r requirements.txt
15+
16+
COPY chat.py ./
17+
18+
EXPOSE 8501
19+
USER 1000
20+
21+
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
22+
23+
ENTRYPOINT ["streamlit", "run", "chat.py", "--server.port=8501", "--server.address=0.0.0.0"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import streamlit as st
2+
import os
3+
import uuid
4+
5+
from langchain_core.messages import HumanMessage
6+
from langchain_openai import ChatOpenAI
7+
from langgraph.checkpoint.postgres import PostgresSaver
8+
from langgraph.graph import START, MessagesState, StateGraph
9+
from psycopg import Connection
10+
11+
AI_PREFIX = "Assistant"
12+
HUMAN_PREFIX = "User"
13+
14+
@st.cache_resource
15+
def get_checkpointer():
16+
db_uri = os.environ.get("DB_URI")
17+
connection_kwargs = { "autocommit": True, "prepare_threshold": 0 }
18+
conn = Connection.connect(conninfo=db_uri, **connection_kwargs)
19+
checkpointer = PostgresSaver(conn)
20+
checkpointer.setup()
21+
return checkpointer
22+
23+
@st.cache_resource
24+
def get_model():
25+
model_base_url = os.environ.get("MODEL_BASE_URL")
26+
model_name = os.environ.get("MODEL_NAME")
27+
return ChatOpenAI(base_url=model_base_url, openai_api_key="-", model=model_name)
28+
29+
# Initialize Streamlit
30+
st.set_page_config(page_title="Streamlit chatbot", page_icon="🤖")
31+
st.title("Streamlit chatbot")
32+
st.caption("Powered by Google Cloud, Langchain and PostgreSQL")
33+
34+
# Initialize the chat_id and messages
35+
headers = st.context.headers
36+
user_id = headers.get("X-Goog-Authenticated-User-Id")
37+
38+
if "chat_id" not in st.session_state:
39+
st.session_state.chat_id = user_id or str(uuid.uuid4())
40+
if "messages" not in st.session_state:
41+
st.session_state.messages = []
42+
43+
# Initialize the model
44+
model = get_model()
45+
def call_model(state: MessagesState):
46+
response = model.invoke(state["messages"])
47+
return {"messages": response}
48+
49+
# Initialize the workflow and LangChain Graph state
50+
workflow = StateGraph(state_schema=MessagesState)
51+
workflow.add_edge(START, "model")
52+
workflow.add_node("model", call_model)
53+
app = workflow.compile(checkpointer=get_checkpointer())
54+
config = {"configurable": {"thread_id": st.session_state.chat_id}}
55+
56+
# Load messages from LangChain Graph state and display them
57+
app_state = app.get_state(config)
58+
if "messages" in app_state.values:
59+
for message in app_state.values["messages"]:
60+
st.chat_message(message.type).markdown(message.content)
61+
62+
# Get the user input and generate a response
63+
if prompt := st.chat_input("Enter your message"):
64+
prompt = prompt.strip()
65+
st.chat_message("human").markdown(prompt)
66+
st.session_state.messages.append({"role": "human", "content": prompt})
67+
68+
with st.spinner(text="Processing..."):
69+
output = app.invoke({"messages": [HumanMessage(prompt)]}, config)
70+
71+
response_content = output["messages"][-1].content.strip()
72+
st.chat_message("ai").markdown(response_content)
73+
st.session_state.messages.append({"role": "ai", "content": response_content})
74+
75+
# Add button to reset chat history
76+
if len(st.session_state.messages) and st.button("Restart chat"):
77+
st.session_state.messages = []
78+
cursor = get_checkpointer().conn.cursor()
79+
cursor.execute("DELETE FROM checkpoints WHERE thread_id = %s", (st.session_state.chat_id,))
80+
cursor.execute("DELETE FROM checkpoint_writes WHERE thread_id = %s", (st.session_state.chat_id,))
81+
cursor.execute("DELETE FROM checkpoint_blobs WHERE thread_id = %s", (st.session_state.chat_id,))
82+
st.rerun()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
streamlit
2+
langchain
3+
langchain-community
4+
langchain-openai
5+
langgraph
6+
langgraph-checkpoint-postgres
7+
psycopg
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: cloud.google.com/v1
2+
kind: BackendConfig
3+
metadata:
4+
name: chat-ui
5+
spec:
6+
iap:
7+
enabled: true
8+
oauthclientCredentials:
9+
secretName: chat-ui-oauth
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# to build, run `gcloud builds submit --config cloudbuild.yaml .` in directory
16+
steps:
17+
- name: 'gcr.io/cloud-builders/docker'
18+
args: [ 'pull', 'python:3.9-slim' ]
19+
- name: 'gcr.io/cloud-builders/docker'
20+
args: [ 'build', '-t', 'us-central1-docker.pkg.dev/$PROJECT_ID/${_REPO_NAME}/app:latest', '.' ]
21+
images:
22+
- 'us-central1-docker.pkg.dev/$PROJECT_ID/${_REPO_NAME}/app:latest'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: chat
5+
spec:
6+
replicas: 1
7+
selector:
8+
matchLabels:
9+
app: chat
10+
template:
11+
metadata:
12+
labels:
13+
app: chat
14+
spec:
15+
containers:
16+
- name: app
17+
image: <your-docker-image>
18+
imagePullPolicy: Always
19+
env:
20+
- name: MODEL_BASE_URL
21+
value: "<your-model-base-url>"
22+
- name: MODEL_NAME
23+
value: "<your-model-name>"
24+
- name: DB_URI
25+
value: "postgresql://postgres:<your-db-password>@<your-db-private-address>:5432/<your-db-name>"
26+
ports:
27+
- containerPort: 8501
28+
livenessProbe:
29+
httpGet:
30+
path: /_stcore/health
31+
port: 8501
32+
scheme: HTTP
33+
timeoutSeconds: 1
34+
periodSeconds: 10
35+
readinessProbe:
36+
httpGet:
37+
path: /_stcore/health
38+
port: 8501
39+
scheme: HTTP
40+
timeoutSeconds: 1
41+
periodSeconds: 10
42+
resources:
43+
limits:
44+
cpu: 1
45+
memory: 2Gi
46+
requests:
47+
cpu: 200m
48+
memory: 1Gi
49+
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: networking.k8s.io/v1
2+
kind: Ingress
3+
metadata:
4+
name: chat-ui
5+
annotations:
6+
kubernetes.io/ingress.global-static-ip-name: langchain-chatbot
7+
networking.gke.io/managed-certificates: chat-ui
8+
kubernetes.io/ingress.class: "gce"
9+
spec:
10+
defaultBackend:
11+
service:
12+
name: chat
13+
port:
14+
number: 80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: networking.gke.io/v1
2+
kind: ManagedCertificate
3+
metadata:
4+
name: chat-ui
5+
spec:
6+
domains:
7+
- <your-domain>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
name: chat
5+
spec:
6+
selector:
7+
app: chat
8+
ports:
9+
- protocol: TCP
10+
port: 80
11+
targetPort: 8501
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
resource "kubernetes_deployment" "app" {
16+
metadata {
17+
name = var.k8s_app_deployment_name
18+
namespace = var.k8s_namespace
19+
}
20+
spec {
21+
replicas = 1
22+
selector {
23+
match_labels = {
24+
app = "chat"
25+
}
26+
}
27+
template {
28+
metadata {
29+
labels = {
30+
app = "chat"
31+
}
32+
}
33+
spec {
34+
container {
35+
name = "app"
36+
image = var.k8s_app_image
37+
image_pull_policy = "Always"
38+
env {
39+
name = "MODEL_BASE_URL"
40+
value = var.model_base_url
41+
}
42+
env {
43+
name = "MODEL_NAME"
44+
value = var.model_name
45+
}
46+
env {
47+
name = "DB_URI"
48+
value = "postgresql://postgres:${random_password.db_password.result}@${google_sql_database_instance.langchain_storage.private_ip_address}/${var.db_name}"
49+
}
50+
port {
51+
container_port = 8501
52+
}
53+
liveness_probe {
54+
http_get {
55+
path = "/_stcore/health"
56+
port = 8501
57+
scheme = "HTTP"
58+
}
59+
timeout_seconds = 1
60+
period_seconds = 10
61+
}
62+
readiness_probe {
63+
http_get {
64+
path = "/_stcore/health"
65+
port = 8501
66+
scheme = "HTTP"
67+
}
68+
timeout_seconds = 1
69+
period_seconds = 10
70+
}
71+
resources {
72+
limits = {
73+
cpu = "1"
74+
ephemeral-storage = "1Gi"
75+
memory = "2Gi"
76+
}
77+
requests = {
78+
cpu = "200m"
79+
ephemeral-storage = "1Gi"
80+
memory = "1Gi"
81+
}
82+
}
83+
security_context {
84+
allow_privilege_escalation = false
85+
privileged = false
86+
read_only_root_filesystem = true
87+
run_as_non_root = true
88+
run_as_user = 1000
89+
90+
capabilities {
91+
drop = ["ALL"]
92+
}
93+
}
94+
}
95+
96+
security_context {
97+
run_as_non_root = true
98+
supplemental_groups = []
99+
}
100+
}
101+
}
102+
}
103+
104+
lifecycle {
105+
ignore_changes = [
106+
metadata[0].annotations,
107+
spec[0].template[0].spec[0].toleration,
108+
spec[0].template[0].spec[0].security_context[0].seccomp_profile,
109+
]
110+
}
111+
}
112+
113+
resource "kubernetes_service" "app" {
114+
metadata {
115+
name = "chat"
116+
namespace = var.k8s_namespace
117+
}
118+
spec {
119+
selector = {
120+
app = "chat"
121+
}
122+
port {
123+
protocol = "TCP"
124+
port = 80
125+
target_port = 8501
126+
}
127+
}
128+
129+
lifecycle {
130+
ignore_changes = [
131+
metadata[0].annotations
132+
]
133+
}
134+
}

0 commit comments

Comments
 (0)