Skip to content

Commit c2181c5

Browse files
authored
extended thinking in the ui - cookbook example (#183)
* added the extended thinking in the ui cookbook example * updated spacing in the main app
1 parent 077b61c commit c2181c5

File tree

8 files changed

+329
-0
lines changed

8 files changed

+329
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ANTHROPIC_API_KEY=

extended-thinking-in-the-ui/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
## Extended Thinking Cookbook
2+
3+
Using Chainlit’s native `@step` decorator and a bit of custom JavaScript, it's easy to recreate the "extended thinking" effect we see in many popular LLM applications such as those by Anthropic, OpenAI, Meta, or DeepSeek.
4+
5+
For this cookbook example, I wanted the application to leverage native thinking tokens exposed at the API level to developers. Unfortunately, OpenAI and Meta do not provide access to raw thinking tokens. As a result, when using models from those companies, Chain-of-Thought reasoning is typically achieved by prompting external models (like GPT-4o) to break problems down and then delegating sub-questions to other models.
6+
7+
In contrast, this application uses Anthropic’s Claude 3.7 Sonnet. Anthropic is one of the few companies that expose a model's internal thinking via their API. This makes Claude an excellent choice for showcasing the difference between “Extended Thinking” (i.e., thinking tokens) and simply streaming a final response to the screen.
8+
9+
Additionally, I modified the logic for the thinking step dropdown so that it automatically opens for each message. You can still close it manually if you'd like to keep the chat history cleaner.
10+
11+
---
12+
13+
### How to Use
14+
15+
1. Clone this cookbook example.
16+
17+
2. Navigate into the cloned directory:
18+
19+
```bash
20+
cd <cloned-directory>
21+
```
22+
23+
3. Install the required dependencies:
24+
25+
```bash
26+
pip install -r requirements.txt
27+
```
28+
29+
4. Rename the `.env.example` file to `.env`, and add your `ANTHROPIC_API_KEY`.
30+
31+
5. Run the app:
32+
33+
```bash
34+
chainlit run app.py
35+
```
36+
37+
6. After running the app for the first time, a `.chainlit` folder will be automatically created with a `config.toml` file inside it.
38+
Delete that auto-generated file and replace it by copying the `config.toml` file from the main directory of this example application into the `.chainlit` folder.
39+
This will apply the custom JavaScript and CSS settings I’ve included.
40+
41+
7. Run the app again:
42+
43+
```bash
44+
chainlit run app.py
45+
```
46+
47+
8. Once the application opens, type any question into the chat input box.
48+
The model will decide—based on the complexity of your question—whether to go through a short or extended thinking step. After that, it will stream the final response separately to the screen.
49+
50+
The model supports message history, so feel free to engage in a natural back-and-forth and use it just like your own personal LLM-powered application.

extended-thinking-in-the-ui/app.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import chainlit as cl
2+
from anthropic import Anthropic
3+
from dotenv import load_dotenv
4+
5+
load_dotenv(override=True)
6+
client = Anthropic()
7+
8+
@cl.on_chat_start
9+
async def start():
10+
cl.user_session.set("message_history", [])
11+
12+
@cl.step(name="Extended Thinking", show_input=False)
13+
async def thinking_step(user_message: str):
14+
current_step = cl.context.current_step
15+
current_step.output = ""
16+
has_thinking = False
17+
message_history = cl.user_session.get("message_history")
18+
message_history.append({"role": "user", "content": user_message})
19+
response = client.messages.create(
20+
model="claude-3-7-sonnet-latest",
21+
system="You are a helpful assistant! Your goal is to provide the most accurate and truthful responses possible.",
22+
max_tokens=64000,
23+
thinking={"type": "enabled", "budget_tokens": 20000},
24+
messages=message_history,
25+
stream=True
26+
)
27+
for chunk in response:
28+
if chunk.type == "content_block_delta" and chunk.index == 0:
29+
delta = chunk.delta
30+
if getattr(delta, "type", None) == "thinking_delta" and hasattr(delta, "thinking"):
31+
await current_step.stream_token(delta.thinking)
32+
has_thinking = True
33+
elif chunk.type == "content_block_stop" and chunk.index == 0:
34+
break
35+
return has_thinking, response
36+
37+
@cl.on_message
38+
async def main(msg: cl.Message):
39+
message_history = cl.user_session.get("message_history")
40+
has_thinking, response = await thinking_step(msg.content)
41+
final_message = cl.Message(content="")
42+
await final_message.send()
43+
ai_response = ""
44+
for chunk in response:
45+
if has_thinking and chunk.type == "content_block_delta" and chunk.index == 0:
46+
continue
47+
elif chunk.type == "content_block_delta" and chunk.index == 1:
48+
delta = chunk.delta
49+
if getattr(delta, "type", None) == "text_delta" and hasattr(delta, "text"):
50+
await final_message.stream_token(delta.text)
51+
ai_response += delta.text
52+
elif chunk.type == "content_block_stop" and chunk.index == 1:
53+
await final_message.update()
54+
if ai_response:
55+
message_history.append({"role": "assistant", "content": ai_response})
56+
cl.user_session.set("message_history", message_history)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
## Extended Thinking Cookbook
2+
3+
Using Chainlit’s native `@step` decorator and a bit of custom JavaScript, it's easy to recreate the "extended thinking" effect we see in many popular LLM applications such as those by Anthropic, OpenAI, Meta, or DeepSeek.
4+
5+
For this cookbook example, I wanted the application to leverage native thinking tokens exposed at the API level to developers. Unfortunately, OpenAI and Meta do not provide access to raw thinking tokens. As a result, when using models from those companies, Chain-of-Thought reasoning is typically achieved by prompting external models (like GPT-4o) to break problems down and then delegating sub-questions to other models.
6+
7+
In contrast, this application uses Anthropic’s Claude 3.7 Sonnet. Anthropic is one of the few companies that expose a model's internal thinking via their API. This makes Claude an excellent choice for showcasing the difference between “Extended Thinking” (i.e., thinking tokens) and simply streaming a final response to the screen.
8+
9+
Additionally, I modified the logic for the thinking step dropdown so that it automatically opens for each message. You can still close it manually if you'd like to keep the chat history cleaner.
10+
11+
---
12+
13+
### How to Use
14+
15+
1. Clone this cookbook example.
16+
17+
2. Navigate into the cloned directory:
18+
19+
```bash
20+
cd <cloned-directory>
21+
```
22+
23+
3. Install the required dependencies:
24+
25+
```bash
26+
pip install -r requirements.txt
27+
```
28+
29+
4. Rename the `.env.example` file to `.env`, and add your `ANTHROPIC_API_KEY`.
30+
31+
5. Run the app:
32+
33+
```bash
34+
chainlit run app.py
35+
```
36+
37+
6. After running the app for the first time, a `.chainlit` folder will be automatically created with a `config.toml` file inside it.
38+
Delete that auto-generated file and replace it by copying the `config.toml` file from the main directory of this example application into the `.chainlit` folder.
39+
This will apply the custom JavaScript and CSS settings I’ve included.
40+
41+
7. Run the app again:
42+
43+
```bash
44+
chainlit run app.py
45+
```
46+
47+
8. Once the application opens, type any question into the chat input box.
48+
The model will decide—based on the complexity of your question—whether to go through a short or extended thinking step. After that, it will stream the final response separately to the screen.
49+
50+
The model supports message history, so feel free to engage in a natural back-and-forth and use it just like your own personal LLM-powered application.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
[project]
2+
# Whether to enable telemetry (default: true). No personal data is collected.
3+
enable_telemetry = true
4+
5+
6+
# List of environment variables to be provided by each user to use the app.
7+
user_env = []
8+
9+
# Duration (in seconds) during which the session is saved when the connection is lost
10+
session_timeout = 3600
11+
12+
# Duration (in seconds) of the user session expiry
13+
user_session_timeout = 1296000 # 15 days
14+
15+
# Enable third parties caching (e.g LangChain cache)
16+
cache = false
17+
18+
# Authorized origins
19+
allow_origins = ["*"]
20+
21+
[features]
22+
# Process and display HTML in messages. This can be a security risk (see https://stackoverflow.com/questions/19603097/why-is-it-dangerous-to-render-user-generated-html-or-javascript)
23+
unsafe_allow_html = false
24+
25+
# Process and display mathematical expressions. This can clash with "$" characters in messages.
26+
latex = false
27+
28+
# Autoscroll new user messages at the top of the window
29+
user_message_autoscroll = true
30+
31+
# Automatically tag threads with the current chat profile (if a chat profile is used)
32+
auto_tag_thread = true
33+
34+
# Allow users to edit their own messages
35+
edit_message = true
36+
37+
# Authorize users to spontaneously upload files with messages
38+
[features.spontaneous_file_upload]
39+
enabled = false
40+
# Define accepted file types using MIME types
41+
# Examples:
42+
# 1. For specific file types:
43+
# accept = ["image/jpeg", "image/png", "application/pdf"]
44+
# 2. For all files of certain type:
45+
# accept = ["image/*", "audio/*", "video/*"]
46+
# 3. For specific file extensions:
47+
# accept = { "application/octet-stream" = [".xyz", ".pdb"] }
48+
# Note: Using "*/*" is not recommended as it may cause browser warnings
49+
accept = ["*/*"]
50+
max_files = 20
51+
max_size_mb = 500
52+
53+
[features.audio]
54+
# Sample rate of the audio
55+
sample_rate = 24000
56+
57+
[UI]
58+
# Name of the assistant.
59+
name = "Extended Thinking Example"
60+
61+
# default_theme = "dark"
62+
63+
layout = "wide"
64+
65+
# Description of the assistant. This is used for HTML tags.
66+
# description = ""
67+
68+
# Chain of Thought (CoT) display mode. Can be "hidden", "tool_call" or "full".
69+
cot = "full"
70+
71+
# Specify a CSS file that can be used to customize the user interface.
72+
# The CSS file can be served from the public directory or via an external link.
73+
custom_css = "/public/styles.css"
74+
75+
# Specify a Javascript file that can be used to customize the user interface.
76+
# The Javascript file can be served from the public directory.
77+
custom_js = "/public/script.js"
78+
79+
# Specify a custom meta image url.
80+
# custom_meta_image_url = "https://chainlit-cloud.s3.eu-west-3.amazonaws.com/logo/chainlit_banner.png"
81+
82+
# Specify a custom build directory for the frontend.
83+
# This can be used to customize the frontend code.
84+
# Be careful: If this is a relative path, it should not start with a slash.
85+
# custom_build = "./public/build"
86+
87+
# Specify optional one or more custom links in the header.
88+
# [[UI.header_links]]
89+
# name = "Issues"
90+
# icon_url = "https://avatars.githubusercontent.com/u/128686189?s=200&v=4"
91+
# url = "https://github.com/Chainlit/chainlit/issues"
92+
93+
[meta]
94+
generated_by = "2.4.1"
Binary file not shown.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const alreadyExpanded = new WeakSet();
2+
3+
function autoOpenSteps(element) {
4+
if (element.matches?.('button[id^="step-"]')) {
5+
tryExpand(element);
6+
}
7+
element.querySelectorAll?.('button[id^="step-"]').forEach((btn) => {
8+
tryExpand(btn);
9+
});
10+
}
11+
12+
function tryExpand(btn) {
13+
const isClosed = btn.getAttribute('data-state') === 'closed';
14+
if (
15+
isClosed &&
16+
!alreadyExpanded.has(btn) &&
17+
btn.querySelector('svg.lucide-chevron-down')
18+
) {
19+
btn.click();
20+
alreadyExpanded.add(btn);
21+
}
22+
}
23+
24+
function removeCopyButtons() {
25+
document.querySelectorAll('button[data-state="closed"]').forEach((button) => {
26+
if (button.querySelector('.lucide-copy')) {
27+
button.remove();
28+
}
29+
});
30+
}
31+
32+
removeCopyButtons();
33+
34+
const mutationObserver = new MutationObserver((mutationList) => {
35+
for (const mutation of mutationList) {
36+
if (mutation.type === 'childList') {
37+
for (const node of mutation.addedNodes) {
38+
if (node.nodeType === Node.ELEMENT_NODE) {
39+
autoOpenSteps(node);
40+
}
41+
}
42+
}
43+
}
44+
});
45+
46+
mutationObserver.observe(document.body, {
47+
childList: true,
48+
subtree: true,
49+
});
50+
51+
const copyButtonObserver = new MutationObserver(() => {
52+
removeCopyButtons();
53+
});
54+
55+
copyButtonObserver.observe(document.body, {
56+
childList: true,
57+
subtree: true,
58+
});
59+
60+
document.querySelectorAll('button[id^="step-"]').forEach(autoOpenSteps);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#theme-toggle,
2+
#new-chat-button {
3+
display: none !important;
4+
}
5+
6+
.watermark {
7+
display: none !important;
8+
visibility: hidden !important;
9+
opacity: 0 !important;
10+
pointer-events: none !important;
11+
height: 0px !important;
12+
width: 0px !important;
13+
overflow: hidden !important;
14+
}
15+
16+
#chat-input:empty::before {
17+
content: 'Ask anything';
18+
}

0 commit comments

Comments
 (0)