Chainlit Cheat Sheet
Overview
Chainlit is an open-source Python framework for building conversational AI applications. It provides a polished chat UI with streaming responses, multi-step reasoning visualization, file uploads, human-in-the-loop feedback, authentication, and data persistence. Chainlit integrates with LangChain, LlamaIndex, and any Python-based LLM framework to turn backend AI logic into a production-ready chat application in minutes.
The framework focuses on observability and user experience: it visualizes intermediate chain steps, supports multi-modal messages (text, images, files, audio), provides user authentication with OAuth, and includes a feedback system for collecting user ratings. Chainlit apps can be deployed as standalone web services or embedded into existing applications.
Installation
pip install chainlit
# Create starter app
chainlit init
# Run app
chainlit run app.py
# Opens at http://localhost:8000
Core Usage
Basic Chat App
# app.py
import chainlit as cl
@cl.on_message
async def main(message: cl.Message):
response = f"You said: {message.content}"
await cl.Message(content=response).send()
OpenAI Integration
import chainlit as cl
from openai import AsyncOpenAI
client = AsyncOpenAI()
@cl.on_chat_start
async def start():
cl.user_session.set("history", [
{"role": "system", "content": "You are a helpful assistant."}
])
@cl.on_message
async def main(message: cl.Message):
history = cl.user_session.get("history")
history.append({"role": "user", "content": message.content})
msg = cl.Message(content="")
await msg.send()
stream = await client.chat.completions.create(
model="gpt-4o",
messages=history,
stream=True
)
async for chunk in stream:
if chunk.choices[0].delta.content:
await msg.stream_token(chunk.choices[0].delta.content)
await msg.update()
history.append({"role": "assistant", "content": msg.content})
cl.user_session.set("history", history)
LangChain Integration
import chainlit as cl
from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage, SystemMessage
from langchain.schema.runnable.config import RunnableConfig
@cl.on_chat_start
async def start():
model = ChatOpenAI(model="gpt-4o", streaming=True)
cl.user_session.set("model", model)
@cl.on_message
async def main(message: cl.Message):
model = cl.user_session.get("model")
msg = cl.Message(content="")
async for chunk in model.astream(
[SystemMessage(content="You are helpful."), HumanMessage(content=message.content)],
config=RunnableConfig(callbacks=[cl.LangchainCallbackHandler()])
):
await msg.stream_token(chunk.content)
await msg.send()
Message Types
import chainlit as cl
@cl.on_message
async def main(message: cl.Message):
# Text message
await cl.Message(content="Hello!").send()
# Message with author
await cl.Message(content="Analyzing...", author="Agent").send()
# Message with elements (attachments)
elements = [
cl.Text(name="result", content="Detailed output here", display="inline"),
cl.Image(name="chart", path="chart.png", display="inline"),
cl.File(name="report", path="report.pdf", display="inline"),
cl.Pdf(name="document", path="doc.pdf", display="side"),
]
await cl.Message(content="Here are the results:", elements=elements).send()
# Code block
await cl.Message(content="```python\nprint('hello')\n```").send()
Steps and Chains
import chainlit as cl
@cl.on_message
async def main(message: cl.Message):
# Show intermediate steps
async with cl.Step(name="Retrieval") as step:
step.input = message.content
# Simulate retrieval
step.output = "Found 5 relevant documents"
async with cl.Step(name="Generation") as step:
step.input = "Context + Question"
msg = cl.Message(content="")
await msg.send()
# Stream tokens
for word in "The answer based on context is...".split():
await msg.stream_token(word + " ")
await msg.update()
step.output = msg.content
User Input and Actions
import chainlit as cl
@cl.on_chat_start
async def start():
# Ask user for input
res = await cl.AskUserMessage(
content="What topic would you like to explore?",
timeout=60
).send()
if res:
await cl.Message(content=f"Great! Let's discuss {res['output']}").send()
@cl.on_message
async def main(message: cl.Message):
# Action buttons
actions = [
cl.Action(name="summarize", value="summarize", label="Summarize"),
cl.Action(name="expand", value="expand", label="Expand"),
cl.Action(name="translate", value="translate", label="Translate"),
]
await cl.Message(content="What would you like to do?", actions=actions).send()
@cl.action_callback("summarize")
async def on_summarize(action: cl.Action):
await cl.Message(content="Summarizing...").send()
@cl.action_callback("expand")
async def on_expand(action: cl.Action):
await cl.Message(content="Expanding...").send()
File Handling
import chainlit as cl
@cl.on_message
async def main(message: cl.Message):
# Handle uploaded files
if message.elements:
for element in message.elements:
if isinstance(element, cl.File):
# Process file
content = element.path # Local path to uploaded file
await cl.Message(content=f"Received file: {element.name}").send()
# Ask for file upload
files = await cl.AskFileMessage(
content="Upload a document to analyze",
accept=["application/pdf", "text/plain"],
max_size_mb=20,
max_files=5
).send()
if files:
for f in files:
await cl.Message(content=f"Processing {f.name} ({f.size} bytes)").send()
Configuration
.chainlit/config.toml
[project]
name = "My AI Assistant"
enable_telemetry = false
[features]
prompt_playground = true
multi_modal = true
[features.spontaneous_file_upload]
enabled = true
accept = ["*/*"]
max_files = 10
max_size_mb = 500
[UI]
name = "AI Assistant"
description = "A helpful AI assistant"
default_collapse_content = true
default_expand_messages = false
hide_cot = false
[UI.theme]
layout = "wide"
[UI.theme.light]
background = "#FFFFFF"
paper = "#F5F5F5"
[UI.theme.dark]
background = "#1A1A2E"
paper = "#16213E"
Authentication
# OAuth (Google, GitHub, etc.)
# .chainlit/config.toml
# [project]
# enable_auth = true
import chainlit as cl
@cl.oauth_callback
def oauth_callback(provider_id, token, raw_user_data, default_user):
return default_user
@cl.on_chat_start
async def start():
user = cl.user_session.get("user")
await cl.Message(content=f"Welcome, {user.identifier}!").send()
Environment Variables
CHAINLIT_AUTH_SECRET=your-secret-key
OPENAI_API_KEY=sk-...
CHAINLIT_PORT=8000
CHAINLIT_HOST=0.0.0.0
Advanced Usage
Custom Data Layer (Persistence)
import chainlit as cl
from chainlit.data import BaseDataLayer
class CustomDataLayer(BaseDataLayer):
async def create_user(self, user):
# Store user in database
pass
async def get_thread(self, thread_id):
# Retrieve conversation
pass
async def create_step(self, step_dict):
# Store step
pass
# Set in app startup
cl.data_layer = CustomDataLayer()
Human Feedback
@cl.on_message
async def main(message: cl.Message):
msg = cl.Message(content="Here is the answer...")
await msg.send()
# Ask for feedback
res = await cl.AskActionMessage(
content="Was this helpful?",
actions=[
cl.Action(name="thumbs_up", value="positive", label="Yes"),
cl.Action(name="thumbs_down", value="negative", label="No"),
]
).send()
if res and res.get("value") == "negative":
await cl.Message(content="Sorry! Let me try again...").send()
Deployment
# Docker
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["chainlit", "run", "app.py", "--host", "0.0.0.0", "--port", "8000"]
Troubleshooting
| Issue | Solution |
|---|---|
| Port 8000 in use | Use chainlit run app.py --port 8001 |
| Streaming not working | Use await msg.stream_token() then await msg.update() |
| Auth not working | Set CHAINLIT_AUTH_SECRET env var |
| Files not uploading | Check max_size_mb in config, verify multi_modal=true |
| Steps not showing | Wrap in async with cl.Step() context manager |
| Session state lost | Use cl.user_session.set/get for persistence |
| Hot reload broken | Save file, ensure --watch flag (default) |
| CORS errors | Set allowed origins in config |
# Debug mode
chainlit run app.py --debug
# Watch mode (auto-reload)
chainlit run app.py --watch
# Custom host/port
chainlit run app.py --host 0.0.0.0 --port 8080