Skip to content

Commit 8c68700

Browse files
committed
Merge branch 'main' of github.com:lm-sys/FastChat
2 parents 20055a0 + 1cd4b74 commit 8c68700

13 files changed

+280
-160
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,33 @@ This is the user interface that users will interact with.
237237
By following these steps, you will be able to serve your models using the web UI. You can open your browser and chat with a model now.
238238
If the models do not show up, try to reboot the gradio web server.
239239

240+
## Launch Chatbot Arena (side-by-side battle UI)
241+
242+
Currently, Chatbot Arena is powered by FastChat. Here is how you can launch an instance of Chatbot Arena locally.
243+
244+
FastChat supports popular API-based models such as OpenAI, Anthropic, Gemini, Mistral and more. To add a custom API, please refer to the model support [doc](./docs/model_support.md). Below we take OpenAI models as an example.
245+
246+
Create a JSON configuration file `api_endpoint.json` with the api endpoints of the models you want to serve, for example:
247+
```
248+
{
249+
"gpt-4o-2024-05-13": {
250+
"model_name": "gpt-4o-2024-05-13",
251+
"api_base": "https://api.openai.com/v1",
252+
"api_type": "openai",
253+
"api_key": [Insert API Key],
254+
"anony_only": false
255+
}
256+
}
257+
```
258+
For Anthropic models, specify `"api_type": "anthropic_message"` with your Anthropic key. Similarly, for gemini model, specify `"api_type": "gemini"`. More details can be found in [api_provider.py](https://github.com/lm-sys/FastChat/blob/main/fastchat/serve/api_provider.py).
259+
260+
To serve your own model using local gpus, follow the instructions in [Serving with Web GUI](#serving-with-web-gui).
261+
262+
Now you're ready to launch the server:
263+
```
264+
python3 -m fastchat.serve.gradio_web_server_multi --register-api-endpoint-file api_endpoint.json
265+
```
266+
240267
#### (Optional): Advanced Features, Scalability, Third Party UI
241268
- You can register multiple model workers to a single controller, which can be used for serving a single model with higher throughput or serving multiple models at the same time. When doing so, please allocate different GPUs and ports for different model workers.
242269
```

fastchat/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99

1010
# Survey Link URL (to be removed) #00729c
1111
SURVEY_LINK = """<div style='text-align: left; margin: 20px 0;'>
12-
<div style='display: inline-block; border: 2px solid #C41E3A; padding: 20px; padding-bottom: 10px; padding-top: 10px; border-radius: 5px;'>
13-
<span style='color: #C41E3A; font-weight: bold;'>New Launch! Jailbreak models at <a href='https://redarena.ai' style='color: #C41E3A; text-decoration: underline;'>RedTeam Arena</a>. </span>
12+
<div style='display: inline-block; border: 2px solid #00729c; padding: 20px; padding-bottom: 10px; padding-top: 10px; border-radius: 5px;'>
13+
<span style='color: #00729c; font-weight: bold;'>New Launch! Copilot Arena: <a href='https://marketplace.visualstudio.com/items?itemName=copilot-arena.copilot-arena' style='color: #00729c; text-decoration: underline;'>VS Code Extension</a> to compare Top LLMs</span>
1414
</div>
1515
</div>"""
1616
# SURVEY_LINK = ""

fastchat/serve/gradio_block_arena_anony.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,12 @@ def build_side_by_side_ui_anony(models):
480480
elem_id="chatbot",
481481
height=650,
482482
show_copy_button=True,
483+
latex_delimiters=[
484+
{"left": "$", "right": "$", "display": False},
485+
{"left": "$$", "right": "$$", "display": True},
486+
{"left": r"\(", "right": r"\)", "display": False},
487+
{"left": r"\[", "right": r"\]", "display": True},
488+
],
483489
)
484490

485491
with gr.Row():

fastchat/serve/gradio_block_arena_named.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,12 @@ def build_side_by_side_ui_named(models):
358358
elem_id=f"chatbot",
359359
height=650,
360360
show_copy_button=True,
361+
latex_delimiters=[
362+
{"left": "$", "right": "$", "display": False},
363+
{"left": "$$", "right": "$$", "display": True},
364+
{"left": r"\(", "right": r"\)", "display": False},
365+
{"left": r"\[", "right": r"\]", "display": True},
366+
],
361367
)
362368

363369
with gr.Row():

fastchat/serve/gradio_block_arena_vision.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,12 @@ def build_single_vision_language_model_ui(
356356
label="Scroll down and start chatting",
357357
height=650,
358358
show_copy_button=True,
359+
latex_delimiters=[
360+
{"left": "$", "right": "$", "display": False},
361+
{"left": "$$", "right": "$$", "display": True},
362+
{"left": r"\(", "right": r"\)", "display": False},
363+
{"left": r"\[", "right": r"\]", "display": True},
364+
],
359365
)
360366

361367
with gr.Row():

fastchat/serve/gradio_block_arena_vision_anony.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,12 @@ def build_side_by_side_vision_ui_anony(context: Context, random_questions=None):
432432
elem_id="chatbot",
433433
height=650,
434434
show_copy_button=True,
435+
latex_delimiters=[
436+
{"left": "$", "right": "$", "display": False},
437+
{"left": "$$", "right": "$$", "display": True},
438+
{"left": r"\(", "right": r"\)", "display": False},
439+
{"left": r"\[", "right": r"\]", "display": True},
440+
],
435441
)
436442

437443
with gr.Row():

fastchat/serve/gradio_block_arena_vision_named.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,12 @@ def build_side_by_side_vision_ui_named(context: Context, random_questions=None):
372372
elem_id=f"chatbot",
373373
height=650,
374374
show_copy_button=True,
375+
latex_delimiters=[
376+
{"left": "$", "right": "$", "display": False},
377+
{"left": "$$", "right": "$$", "display": True},
378+
{"left": r"\(", "right": r"\)", "display": False},
379+
{"left": r"\[", "right": r"\]", "display": True},
380+
],
375381
)
376382

377383
with gr.Row():

fastchat/serve/gradio_web_server.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,12 @@ def build_single_model_ui(models, add_promotion_links=False):
873873
label="Scroll down and start chatting",
874874
height=650,
875875
show_copy_button=True,
876+
latex_delimiters=[
877+
{"left": "$", "right": "$", "display": False},
878+
{"left": "$$", "right": "$$", "display": True},
879+
{"left": r"\(", "right": r"\)", "display": False},
880+
{"left": r"\[", "right": r"\]", "display": True},
881+
],
876882
)
877883
with gr.Row():
878884
textbox = gr.Textbox(

fastchat/serve/monitor/clean_chat_data.py

Lines changed: 146 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
python3 clean_chat_data.py
66
"""
77
import argparse
8-
import datetime
98
import json
109
import os
10+
import hashlib
1111
from pytz import timezone
12-
import time
13-
12+
from functools import partial
13+
from math import ceil
14+
from datetime import datetime, timedelta
1415
from tqdm import tqdm
16+
import time
17+
import multiprocessing as mp
1518

1619
from fastchat.serve.monitor.basic_stats import NUM_SERVERS
1720
from fastchat.serve.monitor.clean_battle_data import (
@@ -26,12 +29,20 @@
2629
)
2730

2831

29-
def get_log_files(max_num_files=None):
30-
dates = []
31-
for month in range(4, 12):
32-
for day in range(1, 33):
33-
dates.append(f"2023-{month:02d}-{day:02d}")
32+
def date_range(start="2023-04-01"):
33+
start_date = datetime.strptime(start, "%Y-%m-%d").date()
34+
end_date = datetime.now().date()
35+
delta = end_date - start_date
36+
dates = [
37+
(start_date + timedelta(days=d)).strftime("%Y-%m-%d")
38+
for d in range(delta.days + 2)
39+
]
3440

41+
return dates
42+
43+
44+
def get_log_files(max_num_files=None):
45+
dates = date_range()
3546
filenames = []
3647
for d in dates:
3748
for i in range(NUM_SERVERS):
@@ -44,90 +55,141 @@ def get_log_files(max_num_files=None):
4455
return filenames
4556

4657

47-
def clean_chat_data(log_files, action_type):
58+
def get_action_type_data(filename, action_type):
59+
for _ in range(5):
60+
try:
61+
lines = open(filename).readlines()
62+
break
63+
except FileNotFoundError:
64+
time.sleep(2)
65+
66+
rows = []
67+
for l in lines:
68+
row = json.loads(l)
69+
if row["type"] == action_type:
70+
rows.append(row)
71+
return rows
72+
73+
74+
def process_data(row, action_type):
75+
try:
76+
if action_type in ["chat", "upvote", "downvote"]:
77+
state = row["state"]
78+
model = row["model"]
79+
elif action_type == "leftvote":
80+
state = row["states"][0]
81+
model = row["states"][0]["model_name"]
82+
elif action_type == "rightvote":
83+
state = row["states"][1]
84+
model = row["states"][1]["model_name"]
85+
conversation_id = state["conv_id"]
86+
except KeyError:
87+
return {
88+
"ct_invalid_conv_id": 1,
89+
}
90+
91+
if conversation_id is None:
92+
return {
93+
"ct_invalid_conv_id": 1,
94+
}
95+
96+
conversation = to_openai_format(state["messages"][state["offset"] :])
97+
if not isinstance(model, str):
98+
return {
99+
"ct_invalid": 1,
100+
}
101+
model = replace_model_name(model, row["tstamp"])
102+
103+
try:
104+
lang_code = detect_language(state["messages"][state["offset"]][1])
105+
except IndexError:
106+
return {
107+
"ct_invalid": 1,
108+
}
109+
110+
if not all(isinstance(x["content"], str) for x in conversation):
111+
return {
112+
"ct_invalid": 1,
113+
}
114+
115+
messages = "".join([x["content"] for x in conversation]).lower()
116+
if NETWORK_ERROR_MSG in messages:
117+
return {
118+
"ct_network_error": 1,
119+
}
120+
user_id = hashlib.md5(row["ip"].encode()).hexdigest()
121+
122+
# Prepare the result data
123+
result = dict(
124+
conversation_id=conversation_id,
125+
model=model,
126+
conversation=conversation,
127+
turn=len(conversation) // 2,
128+
language=lang_code,
129+
user_id=user_id,
130+
tstamp=row["tstamp"],
131+
)
132+
133+
return {
134+
"result": result,
135+
"model": model,
136+
}
137+
138+
139+
def clean_chat_data(log_files, action_type, num_parallel):
140+
with mp.Pool(num_parallel) as pool:
141+
# Use partial to pass action_type to get_action_type_data
142+
func = partial(get_action_type_data, action_type=action_type)
143+
file_data = list(
144+
tqdm(
145+
pool.imap(
146+
func, log_files, chunksize=ceil(len(log_files) / len(pool._pool))
147+
),
148+
total=len(log_files),
149+
desc="Processing Log Files",
150+
)
151+
)
152+
# filter out Nones as some files may not contain any data belong to action_type
48153
raw_data = []
49-
for filename in tqdm(log_files, desc="read files"):
50-
for retry in range(5):
51-
try:
52-
lines = open(filename).readlines()
53-
break
54-
except FileNotFoundError:
55-
time.sleep(2)
56-
57-
for l in lines:
58-
row = json.loads(l)
59-
if row["type"] == action_type:
60-
raw_data.append(row)
154+
for data in file_data:
155+
raw_data.extend(data)
156+
raw_data = [r for r in raw_data if not (r is None)]
157+
158+
# Use the multiprocessing Pool
159+
with mp.Pool(num_parallel) as pool:
160+
func = partial(process_data, action_type=action_type)
161+
results = list(
162+
tqdm(
163+
pool.imap(
164+
func, raw_data, chunksize=ceil(len(raw_data) / len(pool._pool))
165+
),
166+
total=len(raw_data),
167+
desc="Processing Raw Data",
168+
)
169+
)
61170

62-
all_models = set()
63-
all_ips = dict()
64-
chats = []
171+
# Aggregate results from child processes
65172
ct_invalid_conv_id = 0
66173
ct_invalid = 0
67174
ct_network_error = 0
68-
for row in raw_data:
69-
try:
70-
if action_type in ["chat", "upvote", "downvote"]:
71-
state = row["state"]
72-
model = row["model"]
73-
elif action_type == "leftvote":
74-
state = row["states"][0]
75-
model = row["states"][0]["model_name"]
76-
elif action_type == "rightvote":
77-
state = row["states"][1]
78-
model = row["states"][1]["model_name"]
79-
conversation_id = state["conv_id"]
80-
except KeyError:
81-
ct_invalid_conv_id += 1
82-
continue
83-
84-
if conversation_id is None:
85-
ct_invalid_conv_id += 1
86-
continue
87-
88-
conversation = to_openai_format(state["messages"][state["offset"] :])
89-
if not isinstance(model, str):
90-
ct_invalid += 1
91-
continue
92-
model = replace_model_name(model, row["tstamp"])
93-
94-
try:
95-
lang_code = detect_language(state["messages"][state["offset"]][1])
96-
except IndexError:
97-
ct_invalid += 1
175+
all_models = set()
176+
chats = []
177+
for data in tqdm(results):
178+
if "ct_invalid_conv_id" in data:
179+
ct_invalid_conv_id += data["ct_invalid_conv_id"]
98180
continue
99-
100-
if not all(isinstance(x["content"], str) for x in conversation):
101-
ct_invalid += 1
181+
if "ct_invalid" in data:
182+
ct_invalid += data["ct_invalid"]
102183
continue
103-
104-
messages = "".join([x["content"] for x in conversation]).lower()
105-
if NETWORK_ERROR_MSG in messages:
106-
ct_network_error += 1
184+
if "ct_network_error" in data:
185+
ct_network_error += data["ct_network_error"]
107186
continue
108-
109-
ip = row["ip"]
110-
if ip not in all_ips:
111-
all_ips[ip] = len(all_ips)
112-
user_id = all_ips[ip]
113-
114-
chats.append(
115-
dict(
116-
conversation_id=conversation_id,
117-
model=model,
118-
conversation=conversation,
119-
turn=len(conversation) // 2,
120-
language=lang_code,
121-
user_id=user_id,
122-
tstamp=row["tstamp"],
123-
)
124-
)
125-
126-
all_models.update([model])
187+
all_models.update([data["model"]])
188+
chats.append(data["result"])
127189

128190
chats.sort(key=lambda x: x["tstamp"])
129191
last_updated_tstamp = chats[-1]["tstamp"]
130-
last_updated_datetime = datetime.datetime.fromtimestamp(
192+
last_updated_datetime = datetime.fromtimestamp(
131193
last_updated_tstamp, tz=timezone("US/Pacific")
132194
).strftime("%Y-%m-%d %H:%M:%S %Z")
133195

@@ -156,12 +218,13 @@ def clean_chat_data(log_files, action_type):
156218
parser = argparse.ArgumentParser()
157219
parser.add_argument("--action-type", type=str, default="chat")
158220
parser.add_argument("--max-num-files", type=int)
221+
parser.add_argument("--num-parallel", type=int, default=16)
159222
args = parser.parse_args()
160223

161224
log_files = get_log_files(args.max_num_files)
162-
chats = clean_chat_data(log_files, args.action_type)
225+
chats = clean_chat_data(log_files, args.action_type, args.num_parallel)
163226
last_updated_tstamp = chats[-1]["tstamp"]
164-
cutoff_date = datetime.datetime.fromtimestamp(
227+
cutoff_date = datetime.fromtimestamp(
165228
last_updated_tstamp, tz=timezone("US/Pacific")
166229
).strftime("%Y%m%d")
167230

0 commit comments

Comments
 (0)