Chainlit: Membangun Chat UI Production-Ready untuk Aplikasi LLM
Membangun aplikasi LLM yang powerful saja tidak cukup. Pengguna membutuhkan antarmuka chat yang intuitif dan profesional untuk berinteraksi dengan model AI. Namun, membangun UI chat dari nol membutuhkan waktu dan effort yang besar, mulai dari WebSocket, state management, hingga file handling.
Chainlit hadir sebagai framework Python yang memungkinkan Anda membangun chat UI production-ready dalam hitungan menit. Dengan Chainlit, Anda bisa fokus pada logika AI sambil mendapatkan antarmuka yang cantik, streaming response, file upload, dan banyak fitur lainnya secara otomatis.Apa Itu Chainlit?
Chainlit adalah framework open-source Python untuk membangun antarmuka chat conversational AI. Fitur utamanya meliputi:
- Chat UI Siap Produksi: Antarmuka modern dengan dukungan markdown, code highlighting, dan file preview
- Streaming Response: Menampilkan respons LLM secara real-time token per token
- Multi-Step Reasoning: Visualisasi proses berpikir AI dengan step-by-step display
- File Upload & Download: Handling file bawaan untuk dokumen, gambar, dan lainnya
- User Authentication: Sistem login terintegrasi dengan berbagai provider
- Chat History: Penyimpanan dan recall percakapan sebelumnya
- Integrasi LangChain/LlamaIndex: Dukungan native untuk framework AI populer
- Custom Actions: Tombol interaktif dan aksi kustom dalam chat
- Theming: Kustomisasi tampilan sesuai brand Anda
Instalasi
Instalasi Dasar
pip install chainlit
Instalasi dengan Dependensi Tambahan
# Dengan dukungan LangChain
pip install chainlit langchain langchain-openai
Dengan dukungan LlamaIndex
pip install chainlit llama-index
Verifikasi Instalasi
chainlit --version
Menjalankan Aplikasi Pertama
Buat file app.py:
import chainlit as cl
@cl.onmessage
async def main(message: cl.Message):
# Kirim respons sederhana
await cl.Message(
content=f"Anda mengirim: {message.content}"
).send()
Jalankan dengan:
chainlit run app.py
Buka browser di http://localhost:8000 dan Anda akan melihat antarmuka chat yang profesional.
Konsep Dasar: Decorator dan Message
@cl.onmessage - Handler Pesan Utama
Decorator @cl.onmessage adalah entry point utama. Fungsi ini dipanggil setiap kali pengguna mengirim pesan.
import chainlit as cl
@cl.onmessage
async def handlemessage(message: cl.Message):
# Akses konten pesan
userinput = message.content
# Akses file yang diunggah (jika ada)
files = message.elements
# Proses dan kirim respons
response = f"Pesan diterima: {userinput}"
await cl.Message(content=response).send()
@cl.onchatstart - Inisialisasi Chat
Decorator ini dipanggil saat sesi chat baru dimulai. Gunakan untuk menyiapkan konteks, model, atau menyapa pengguna.
import chainlit as cl
@cl.onchatstart
async def start():
# Simpan state di session
cl.usersession.set("history", [])
# Kirim pesan sambutan
await cl.Message(
content="Halo! Saya adalah asisten AI. Ada yang bisa saya bantu?"
).send()
@cl.onmessage
async def main(message: cl.Message):
# Ambil history dari session
history = cl.usersession.get("history")
history.append({"role": "user", "content": message.content})
# Proses pesan...
response = "Respons dari AI"
history.append({"role": "assistant", "content": response})
cl.usersession.set("history", history)
await cl.Message(content=response).send()
Tipe Pesan: Teks, Gambar, dan File
Pesan Teks dengan Markdown
@cl.onmessage
async def main(message: cl.Message):
# Chainlit mendukung markdown penuh
response = """
Hasil Analisis
Berikut adalah hasil analisis data Anda:
| Metrik | Nilai |
|--------|-------|
| Akurasi | 95.2% |
| Presisi | 93.8% |
| Recall | 96.1% |
Kesimpulan: Model menunjukkan performa yang sangat baik.
"""
await cl.Message(content=response).send()
Mengirim Gambar
@cl.onmessage
async def main(message: cl.Message):
# Kirim gambar sebagai element
image = cl.Image(
path="./chart.png",
name="analysis
chart",
display="inline"
)
await cl.Message(
content="Berikut visualisasi hasil analisis:",
elements=[image]
).send()
Mengirim File untuk Diunduh
@cl.onmessage
async def main(message: cl.Message):
# Kirim file sebagai attachment
file = cl.File(
path="./report.pdf",
name="laporan
analisis.pdf",
display="inline"
)
await cl.Message(
content="Laporan telah selesai dibuat. Silakan unduh:",
elements=[file]
).send()
Streaming Response
Streaming memungkinkan respons ditampilkan secara real-time, memberikan pengalaman pengguna yang lebih baik.
Streaming Dasar
import chainlit as cl
from openai import AsyncOpenAI
client = AsyncOpenAI()
@cl.onmessage
async def main(message: cl.Message):
msg = cl.Message(content="")
# Streaming dari OpenAI
stream = await client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "user", "content": message.content}
],
stream=True
)
async for chunk in stream:
if chunk.choices[0].delta.content:
await msg.streamtoken(chunk.choices[0].delta.content)
# Finalisasi pesan
await msg.send()
Streaming dengan Token Tracking
@cl.onmessage
async def main(message: cl.Message):
msg = cl.Message(content="")
total
tokens = 0
stream = await client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": message.content}],
stream=True,
streamoptions={"includeusage": True}
)
async for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
await msg.streamtoken(chunk.choices[0].delta.content)
if chunk.usage:
totaltokens = chunk.usage.totaltokens
await msg.send()
# Tampilkan info token
await cl.Message(
content=f"Total token yang digunakan: {totaltokens}"
).send()
Multi-Step Reasoning Display
Chainlit dapat menampilkan proses berpikir AI secara bertahap menggunakan Steps.
import chainlit as cl
@cl.onmessage
async def main(message: cl.Message):
# Step 1: Analisis pertanyaan
async with cl.Step(name="Menganalisis Pertanyaan") as step:
step.input = message.content
# Simulasi analisis
analysis = "Pertanyaan tentang machine learning classification"
step.output = analysis
# Step 2: Pencarian dokumen
async with cl.Step(name="Mencari Dokumen Relevan") as step:
step.input = analysis
# Simulasi pencarian
docs = "Ditemukan 5 dokumen relevan"
step.output = docs
# Step 3: Generasi jawaban
async with cl.Step(name="Menghasilkan Jawaban") as step:
step.input = f"Query: {analysis}\nDokumen: {docs}"
answer = "Berdasarkan dokumen yang ditemukan..."
step.output = answer
# Kirim jawaban final
await cl.Message(content=answer).send()
File Upload Handling
Meminta File dari Pengguna
import chainlit as cl
@cl.onchatstart
async def start():
files = await cl.AskFileMessage(
content="Silakan unggah file CSV untuk dianalisis:",
accept=["text/csv"],
maxsizemb=10,
maxfiles=1
).send()
if files:
file = files[0]
# Baca konten file
import pandas as pd
df = pd.readcsv(file.path)
await cl.Message(
content=f"File {file.name} berhasil diunggah!\n\n"
f"- Jumlah baris: {len(df)}\n"
f"- Jumlah kolom: {len(df.columns)}\n"
f"- Kolom: {', '.join(df.columns.tolist())}"
).send()
cl.usersession.set("dataframe", df)
Menangani Multiple File Types
@cl.onchatstart
async def start():
files = await cl.AskFileMessage(
content="Unggah dokumen Anda (PDF, TXT, atau DOCX):",
accept={
"application/pdf": [".pdf"],
"text/plain": [".txt"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"]
},
maxsizemb=20,
maxfiles=5
).send()
if files:
fileinfo = []
for f in files:
fileinfo.append(f"- {f.name} ({f.type}, {f.size} bytes)")
await cl.Message(
content=f"Berhasil mengunggah {len(files)} file:\n" +
"\n".join(fileinfo)
).send()
User Authentication
Password Authentication
Buat file .chainlit/config.toml:
[project]
name = "My AI Assistant"
enabletelemetry = false
[UI]
name = "AI Assistant"
Implementasi autentikasi:
import chainlit as cl
@cl.passwordauthcallback
def authcallback(username: str, password: str):
# Validasi kredensial (gunakan database di produksi)
if username == "admin" and password == "secret123":
return cl.User(
identifier="admin",
metadata={"role": "admin", "name": "Administrator"}
)
return None
@cl.onchatstart
async def start():
user = cl.usersession.get("user")
await cl.Message(
content=f"Selamat datang, {user.metadata['name']}!"
).send()
Header Authentication (untuk Reverse Proxy)
@cl.headerauthcallback
def header
auth(headers: dict):
# Autentikasi via header (misalnya dari reverse proxy)
if headers.get("X-User-Email"):
return cl.User(
identifier=headers["X-User-Email"],
metadata={"provider": "header"}
)
return None
Chat History dan Persistence
Menyimpan Chat History
import chainlit as cl
import json
from pathlib import Path
HISTORYDIR = Path("chathistory")
HISTORYDIR.mkdir(existok=True)
@cl.onchatstart
async def start():
cl.usersession.set("history", [])
@cl.onmessage
async def main(message: cl.Message):
history = cl.usersession.get("history")
history.append({
"role": "user",
"content": message.content
})
# Proses dengan LLM
response = await generateresponse(history)
history.append({
"role": "assistant",
"content": response
})
cl.usersession.set("history", history)
await cl.Message(content=response).send()
@cl.onchatend
async def end():
history = cl.usersession.get("history")
if history:
# Simpan ke file
sessionid = cl.usersession.get("id")
filepath = HISTORYDIR / f"{sessionid}.json"
with open(filepath, "w") as f:
json.dump(history, f, indent=2)
Menggunakan Chainlit Data Layer
Chainlit menyediakan data layer abstraction untuk menyimpan percakapan secara persisten.
import chainlit.data as cldata
from chainlit.data.sql import SQLAlchemyDataLayer
Konfigurasi data layer dengan PostgreSQL
cl
data.init(
SQLAlchemyDataLayer(
conninfo="postgresql://user:pass@localhost/chainlitdb"
)
)
Integrasi dengan LangChain
import chainlit as cl
from langchainopenai import ChatOpenAI
from langchain.schema import HumanMessage, AIMessage, SystemMessage
from langchain.schema.runnable.config import RunnableConfig
@cl.onchatstart
async def start():
model = ChatOpenAI(
model="gpt-4",
temperature=0.7,
streaming=True
)
cl.usersession.set("model", model)
cl.usersession.set("history", [
SystemMessage(content="Kamu adalah asisten AI yang membantu.")
])
@cl.onmessage
async def main(message: cl.Message):
model = cl.usersession.get("model")
history = cl.usersession.get("history")
history.append(HumanMessage(content=message.content))
msg = cl.Message(content="")
async for chunk in model.astream(
history,
config=RunnableConfig(callbacks=[cl.LangchainCallbackHandler()])
):
await msg.streamtoken(chunk.content)
history.append(AIMessage(content=msg.content))
cl.usersession.set("history", history)
await msg.send()
Integrasi dengan LlamaIndex
import chainlit as cl
from llamaindex.core import (
VectorStoreIndex,
SimpleDirectoryReader,
Settings
)
from llamaindex.llms.openai import OpenAI
@cl.onchatstart
async def start():
Settings.llm = OpenAI(model="gpt-4", temperature=0.1)
# Muat dan indeks dokumen
await cl.Message(content="Memuat dokumen...").send()
documents = SimpleDirectoryReader("./data").loaddata()
index = VectorStoreIndex.fromdocuments(documents)
queryengine = index.asqueryengine(streaming=True)
cl.usersession.set("queryengine", queryengine)
await cl.Message(
content="Dokumen berhasil dimuat! Silakan ajukan pertanyaan."
).send()
@cl.onmessage
async def main(message: cl.Message):
queryengine = cl.usersession.get("queryengine")
msg = cl.Message(content="")
response = await cl.makeasync(queryengine.query)(message.content)
# Tampilkan sumber
sources = []
for node in response.sourcenodes:
sources.append(f"- {node.metadata.get('filename', 'Unknown')} "
f"(score: {node.score:.3f})")
sourcetext = "\n\nSumber:\n" + "\n".join(sources)
await cl.Message(
content=str(response) + sourcetext
).send()
Custom Actions dan Buttons
import chainlit as cl
@cl.onchatstart
async def start():
# Kirim pesan dengan tombol aksi
actions = [
cl.Action(
name="summarize",
payload={"action": "summarize"},
label="Ringkas Teks",
description="Meringkas teks yang diberikan"
),
cl.Action(
name="translate",
payload={"action": "translate"},
label="Terjemahkan",
description="Terjemahkan ke bahasa lain"
),
cl.Action(
name="analyze",
payload={"action": "analyze"},
label="Analisis Sentimen",
description="Analisis sentimen teks"
)
]
await cl.Message(
content="Pilih aksi yang ingin dilakukan:",
actions=actions
).send()
@cl.actioncallback("summarize")
async def onsummarize(action: cl.Action):
await cl.Message(content="Silakan kirim teks yang ingin diringkas.").send()
cl.usersession.set("mode", "summarize")
@cl.actioncallback("translate")
async def ontranslate(action: cl.Action):
await cl.Message(content="Silakan kirim teks yang ingin diterjemahkan.").send()
cl.usersession.set("mode", "translate")
@cl.actioncallback("analyze")
async def onanalyze(action: cl.Action):
await cl.Message(content="Silakan kirim teks untuk analisis sentimen.").send()
cl.usersession.set("mode", "analyze")
Theming dan Kustomisasi
Konfigurasi Theme
Buat file .chainlit/config.toml:
[project]
name = "AI Assistant Pro"
enabletelemetry = false
[UI]
name = "AI Assistant Pro"
description = "Asisten AI cerdas untuk kebutuhan Anda"
defaulttheme = "dark"
[UI.theme]
fontfamily = "Inter, sans-serif"
[UI.theme.light]
background = "#FFFFFF"
paper = "#F5F5F5"
[UI.theme.dark]
background = "#1A1A2E"
paper = "#16213E"
Custom Logo dan Favicon
Letakkan file di folder public/:
public/
logodark.png
logolight.png
favicon.png
Custom CSS
Buat file public/stylesheet.css:
/ Kustomisasi tampilan chat /
.message-content {
font-size: 15px;
line-height: 1.6;
}
/ Kustomisasi code block /
pre code {
font-size: 13px;
border-radius: 8px;
}
Deployment
Menggunakan Docker
Buat Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["chainlit", "run", "app.py", "--host", "0.0.0.0", "--port", "8000"]
Buat docker-compose.yml:
version: '3.8'
services:
chainlit-app:
build: .
ports:
- "8000:8000"
environment:
- OPENAIAPIKEY=${OPENAIAPIKEY}
volumes:
- ./data:/app/data
- ./chathistory:/app/chathistory
restart: unless-stopped
Deploy ke Cloud
# Build dan jalankan
docker build -t chainlit-app .
docker run -d -p 8000:8000 \
-e OPENAIAPIKEY=$OPENAIAPIKEY \
chainlit-app
Contoh Praktis: RAG Chatbot dengan File Upload dan Source Citations
Berikut adalah contoh lengkap membangun RAG chatbot yang mendukung file upload dan menampilkan sumber kutipan.
import chainlit as cl
from langchainopenai import ChatOpenAI, OpenAIEmbeddings
from langchain.textsplitter import RecursiveCharacterTextSplitter
from langchaincommunity.vectorstores import FAISS
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferWindowMemory
from langchaincommunity.documentloaders import (
PyPDFLoader,
TextLoader,
CSVLoader
)
import tempfile
import os
Konfigurasi
CHUNKSIZE = 1000
CHUNKOVERLAP = 200
def loaddocument(filepath: str, filetype: str):
"""Muat dokumen berdasarkan tipe file."""
if filetype == "application/pdf":
loader = PyPDFLoader(filepath)
elif filetype == "text/csv":
loader = CSVLoader(filepath)
else:
loader = TextLoader(filepath)
return loader.load()
@cl.onchatstart
async def start():
# Minta pengguna mengunggah dokumen
files = await cl.AskFileMessage(
content="Selamat datang di RAG Chatbot!\n\n"
"Silakan unggah dokumen yang ingin Anda tanyakan "
"(PDF, TXT, atau CSV):",
accept={
"application/pdf": [".pdf"],
"text/plain": [".txt"],
"text/csv": [".csv"]
},
maxsizemb=20,
maxfiles=5
).send()
if not files:
return
# Proses dokumen
processingmsg = cl.Message(content="Memproses dokumen Anda...")
await processingmsg.send()
alldocs = []
for file in files:
docs = loaddocument(file.path, file.type)
for doc in docs:
doc.metadata["source"] = file.name
alldocs.extend(docs)
# Split dokumen
async with cl.Step(name="Memecah Dokumen") as step:
textsplitter = RecursiveCharacterTextSplitter(
chunksize=CHUNKSIZE,
chunkoverlap=CHUNKOVERLAP
)
splits = textsplitter.splitdocuments(alldocs)
step.output = f"Dokumen dipecah menjadi {len(splits)} bagian"
# Buat vector store
async with cl.Step(name="Membuat Vector Index") as step:
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.fromdocuments(splits, embeddings)
step.output = "Vector index berhasil dibuat"
# Setup retrieval chain
memory = ConversationBufferWindowMemory(
memorykey="chathistory",
returnmessages=True,
outputkey="answer",
k=5
)
llm = ChatOpenAI(
model="gpt-4",
temperature=0.3,
streaming=True
)
chain = ConversationalRetrievalChain.fromllm(
llm=llm,
retriever=vectorstore.asretriever(searchkwargs={"k": 4}),
memory=memory,
returnsourcedocuments=True,
verbose=False
)
# Simpan di session
cl.usersession.set("chain", chain)
cl.usersession.set("vectorstore", vectorstore)
# Notifikasi sukses
filenames = ", ".join([f.name for f in files])
await cl.Message(
content=f"Dokumen berhasil diproses: {filenames}\n\n"
f"Total {len(splits)} bagian teks telah diindeks. "
f"Silakan ajukan pertanyaan tentang dokumen Anda!"
).send()
@cl.onmessage
async def main(message: cl.Message):
chain = cl.usersession.get("chain")
if not chain:
await cl.Message(
content="Silakan unggah dokumen terlebih dahulu."
).send()
return
# Jalankan query dengan steps
async with cl.Step(name="Mencari Konteks Relevan") as step:
step.input = message.content
result = await cl.makeasync(chain.invoke)(
{"question": message.content}
)
step.output = f"Ditemukan {len(result['sourcedocuments'])} sumber"
# Format sumber kutipan
sources = []
seen = set()
for doc in result["sourcedocuments"]:
sourcename = doc.metadata.get("source", "Unknown")
page = doc.metadata.get("page", "N/A")
key = f"{sourcename}-{page}"
if key not in seen:
seen.add(key)
snippet = doc.pagecontent[:100].replace("\n", " ")
sources.append(
f"- {sourcename} (hal. {page}): \"{snippet}...\""
)
# Kirim jawaban dengan sumber
answer = result["answer"]
if sources:
answer += "\n\nSumber Kutipan:\n" + "\n".join(sources)
await cl.Message(content=answer).send()
Menjalankan RAG Chatbot
# Install dependensi
pip install chainlit langchain langchain-openai langchain-community \
faiss-cpu pypdf
Set API key
export OPENAIAPIKEY="sk-your-api-key"
Jalankan
chainlit run app.py
Tips dan Best Practices
cl.usersession karena setiap pengguna memiliki session terpisahcl.Step untuk menampilkan proses di balik layarKesimpulan
Chainlit adalah pilihan yang sangat baik untuk membangun antarmuka chat AI production-ready. Dengan API yang sederhana namun powerful, Anda bisa fokus pada logika AI tanpa harus menghabiskan waktu membangun UI dari nol. Integrasi native dengan LangChain dan LlamaIndex membuatnya semakin mudah untuk membangun aplikasi RAG yang lengkap dengan fitur enterprise seperti autentikasi, chat history, dan deployment yang mudah.