Files
wehub-resource-sync 2aaeece67c
Codestyle Check / Lint (push) Has been cancelled
Codestyle Check / Check bypass (push) Has been cancelled
Pipelines-Test / Pipelines-Test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:37:14 +08:00

355 lines
14 KiB
Python

#!/usr/bin/env python
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import argparse
import json
import logging
import re
import gradio as gr
import requests
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
def setup_args():
"""Setup arguments."""
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=8073)
parser.add_argument("--api_key", type=str, default=None, help="Your API key")
parser.add_argument("--model", type=str, default="", help="Model name")
parser.add_argument("--title", type=str, default="PaddleNLP Chat", help="UI Title")
parser.add_argument("--sub_title", type=str, default="powered by paddlenlp team.", help="UI Sub Title")
parser.add_argument("--flask_port", type=int, default=None, help="The port of flask service")
args = parser.parse_args()
return args
def create_src_slider(value, maximum):
return gr.Slider(
minimum=1,
maximum=maximum,
value=value,
step=1,
label="Max Src Length",
info="最大输入长度。",
)
def create_max_slider(value, maximum):
return gr.Slider(
minimum=1,
maximum=maximum,
value=value,
step=1,
label="Max Decoding Length",
info="生成结果的最大长度。",
)
def remove_think_tags(text):
"""
清除文本中 <think> 和 </think> 标签之间的所有字符。
Args:
text: 要处理的文本字符串。
Returns:
清除 <think> 和 </think> 标签之间内容的文本字符串。
"""
pattern = re.compile(r"\\<think\\>.*?\\<\\\/think\\>", re.DOTALL)
# 将匹配到的部分替换为空字符串
cleaned_text = pattern.sub("", text).strip()
return cleaned_text
def launch(args, default_params: dict = {}):
"""Launch chat UI with OpenAI API."""
def rollback(state):
"""Rollback context."""
context = state.setdefault("context", [])
# 回退时移除最后一次对话
if len(context) >= 2:
content = context[-2]["content"]
context = context[:-2]
state["context"] = context
shown_context = get_shown_context(context)
return content, shown_context, context, state
else:
gr.Warning("没有可撤回的对话历史")
return None, get_shown_context(context), context, state
def regen(state, top_k, top_p, temperature, repetition_penalty, max_tokens, src_length):
"""Regenerate response."""
context = state.setdefault("context", [])
if len(context) < 2:
gr.Warning("No chat history!")
shown_context = get_shown_context(context)
return None, shown_context, context, state
# 删除上一次回复,重新生成
context.pop()
user_turn = context.pop()
context.append({"role": "user", "content": user_turn["content"]})
context.append({"role": "assistant", "content": ""})
shown_context = get_shown_context(context)
return user_turn["content"], shown_context, context, state
def begin(content, state):
"""记录用户输入,并初始化 bot 回复为空。"""
context = state.setdefault("context", [])
if not content:
gr.Warning("Invalid inputs")
shown_context = get_shown_context(context)
return None, shown_context, context, state
context.append({"role": "user", "content": content})
context.append({"role": "assistant", "content": ""})
shown_context = get_shown_context(context)
return content, shown_context, context, state
def infer(content, state, top_k, top_p, temperature, repetition_penalty, max_tokens, src_length):
"""调用 OpenAI 接口生成回答,并以流式返回部分结果。"""
context = state.setdefault("context", [])
if not content:
gr.Warning("Invalid inputs")
shown_context = get_shown_context(context)
return None, shown_context, context, state
# 构造 OpenAI API 要求的 messages 格式
messages = []
for turn in context[:-1]:
messages.append({"role": turn["role"], "content": remove_think_tags(turn["content"])})
# 默认模型名称从参数中获取
model = getattr(args, "model", default_params.get("model", ""))
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"repetition_penalty": repetition_penalty,
"max_tokens": max_tokens,
"src_length": src_length,
"top_p": top_p,
"top_k": top_k,
"stream": True,
}
headers = {
# "Authorization": "Bearer " + args.api_key,
"Content-Type": "application/json"
}
url = f"http://0.0.0.0:{args.flask_port}/v1/chat/completions"
try:
res = requests.post(url, json=payload, headers=headers, stream=True)
except Exception as e:
gr.Warning(f"请求异常: {e}")
shown_context = get_shown_context(context)
yield None, shown_context, context, state
return
# 流式处理返回结果,实时更新最后一个对话记录(即 bot 回复)
for line in res.iter_lines():
if line:
try:
decoded_line = line.decode("utf-8").strip()
# OpenAI 流返回每行以 "data:" 开头
if decoded_line.startswith("data:"):
data_str = decoded_line[len("data:") :].strip()
if data_str == "[DONE]":
logger.info("Conversation round over.")
break
data_json = json.loads(data_str)
# delta 中可能包含部分回复内容
delta = data_json["choices"][0]["delta"].get("content", "")
if delta:
# Reformat <think> tags to show in chatbot
delta = delta.replace("<think>", r"\<think\>")
delta = delta.replace("</think>", r"\<\/think\>")
context[-1]["content"] += delta
shown_context = get_shown_context(context)
yield None, shown_context, context, state
else:
logger.error(f"{decoded_line}")
gr.Warning(f"{decoded_line}")
except Exception as e:
logger.error(f"解析返回结果异常: {e}")
gr.Warning(f"解析返回结果异常: {e}")
continue
def get_shown_context(context):
"""将对话上下文转换为 gr.Chatbot 显示格式,每一对 [用户, 助手]"""
shown_context = []
# 每两项组成一对
for turn_idx in range(0, len(context), 2):
user_text = context[turn_idx]["content"]
bot_text = context[turn_idx + 1]["content"] if turn_idx + 1 < len(context) else ""
shown_context.append([user_text, bot_text])
return shown_context
with gr.Blocks(title="LLM", theme=gr.themes.Soft()) as block:
gr.Markdown(f"# {args.title} <font style='color: red !important' size=2>{args.sub_title}</font>")
with gr.Row():
with gr.Column(scale=1):
top_k = gr.Slider(
minimum=0,
maximum=100,
value=0,
step=1,
label="Top-k",
info="控制采样token个数。(不建议设置)",
)
top_p = gr.Slider(
minimum=0,
maximum=1,
value=default_params.get("top_p", 0.7),
step=0.05,
label="Top-p",
info="控制采样范围。",
)
temperature = gr.Slider(
minimum=0.05,
maximum=1.5,
value=default_params.get("temperature", 0.95),
step=0.05,
label="Temperature",
info="温度,控制生成随机性。",
)
repetition_penalty = gr.Slider(
minimum=0.1,
maximum=10,
value=default_params.get("repetition_penalty", 1.2),
step=0.05,
label="Repetition Penalty",
info="生成结果重复惩罚。(不建议设置)",
)
default_src_length = default_params.get("src_length", 128)
total_length = default_src_length + default_params.get("max_tokens", 50)
src_length = create_src_slider(default_src_length, total_length)
max_tokens = create_max_slider(max(total_length - default_src_length, 50), total_length)
def src_length_change_event(src_length_value, max_tokens_value):
return create_max_slider(
min(total_length - src_length_value, max_tokens_value),
total_length - src_length_value,
)
def max_tokens_change_event(src_length_value, max_tokens_value):
return create_src_slider(
min(total_length - max_tokens_value, src_length_value),
total_length - max_tokens_value,
)
src_length.change(src_length_change_event, inputs=[src_length, max_tokens], outputs=max_tokens)
max_tokens.change(max_tokens_change_event, inputs=[src_length, max_tokens], outputs=src_length)
with gr.Column(scale=4):
state = gr.State({})
# 这里修改 gr.Chatbot 组件,启用 Markdown 渲染并支持 LaTeX 展示
context_chatbot = gr.Chatbot(
label="Context",
render_markdown=True,
latex_delimiters=[
{"left": "$$", "right": "$$", "display": True},
{"left": "\\[", "right": "\\]", "display": True},
{"left": "$", "right": "$", "display": True},
],
)
utt_text = gr.Textbox(placeholder="请输入...", label="Content")
with gr.Row():
clear_btn = gr.Button("清空")
rollback_btn = gr.Button("撤回")
regen_btn = gr.Button("重新生成")
send_btn = gr.Button("发送")
with gr.Row():
raw_context_json = gr.JSON(label="Raw Context")
utt_text.submit(
begin,
inputs=[utt_text, state],
outputs=[utt_text, context_chatbot, raw_context_json, state],
queue=False,
api_name="chat",
).then(
infer,
inputs=[utt_text, state, top_k, top_p, temperature, repetition_penalty, max_tokens, src_length],
outputs=[utt_text, context_chatbot, raw_context_json, state],
)
clear_btn.click(
lambda _: (None, None, None, {}),
inputs=clear_btn,
outputs=[utt_text, context_chatbot, raw_context_json, state],
api_name="clear",
show_progress=False,
)
rollback_btn.click(
rollback,
inputs=[state],
outputs=[utt_text, context_chatbot, raw_context_json, state],
show_progress=False,
)
regen_btn.click(
regen,
inputs=[state, top_k, top_p, temperature, repetition_penalty, max_tokens, src_length],
outputs=[utt_text, context_chatbot, raw_context_json, state],
queue=False,
api_name="chat",
).then(
infer,
inputs=[utt_text, state, top_k, top_p, temperature, repetition_penalty, max_tokens, src_length],
outputs=[utt_text, context_chatbot, raw_context_json, state],
)
send_btn.click(
begin,
inputs=[utt_text, state],
outputs=[utt_text, context_chatbot, raw_context_json, state],
queue=False,
api_name="chat",
).then(
infer,
inputs=[utt_text, state, top_k, top_p, temperature, repetition_penalty, max_tokens, src_length],
outputs=[utt_text, context_chatbot, raw_context_json, state],
)
block.queue().launch(server_name="0.0.0.0", server_port=args.port, debug=True)
def main(args, default_params: dict = {}):
launch(args, default_params)
if __name__ == "__main__":
# 可以在 default_params 中设置默认参数,如 src_length, max_tokens, temperature, top_p 等
default_params = {
"src_length": 1024,
"max_tokens": 1024,
"temperature": 0.95,
"top_p": 0.7,
}
args = setup_args()
main(args, default_params)