chatAIO / app.py
vinhAIO's picture
update 1
c22187d verified
Raw
History Blame Contribute Delete
6.51 kB
import gradio as gr
from huggingface_hub import InferenceClient
# ── Cơ chế token theo IOAI ────────────────────────────────────────────────
# Theo Technical Appendix của IOAI 2026 (mục "Language Models and AI Assistance"):
# - Individual Contest: Gemma 3, tối đa 1000 token ĐẦU RA mỗi câu hỏi.
# - GAITE Contest: Gemma 4, tối đa 2000 token ĐẦU RA mỗi câu hỏi.
# => Đây là giới hạn token đầu ra cho TỪNG câu hỏi (per query), không cộng dồn.
# Về kỹ thuật chính là tham số max_tokens: câu trả lời dài hơn sẽ bị cắt.
# --------------------------------------------------------------------------
# Danh sách model (mặc định Gemma 3 để giống Individual Contest của IOAI).
# Dropdown bật allow_custom_value=True nên bạn cũng có thể gõ model ID bất kỳ.
MODELS = [
"google/gemma-3-27b-it", # Gemma 3 (IOAI Individual)
"google/gemma-3-12b-it",
"google/gemma-2-9b-it",
"meta-llama/Llama-3.3-70B-Instruct",
"meta-llama/Llama-3.1-8B-Instruct",
"Qwen/Qwen2.5-72B-Instruct",
"Qwen/Qwen2.5-7B-Instruct",
"mistralai/Mistral-7B-Instruct-v0.3",
"deepseek-ai/DeepSeek-V3",
"HuggingFaceH4/zephyr-7b-beta",
]
# Giới hạn token đầu ra mỗi câu hỏi (IOAI Individual = 1000, GAITE = 2000)
DEFAULT_OUTPUT_LIMIT = 1000
# Dấu nhận biết dòng thống kê token (để cắt khỏi history trước khi gửi lại model)
TOKEN_FOOTER_MARKER = "📊 Token đầu ra:"
def extract_text(content):
"""Lấy phần text từ 'content' của một message.
Gradio 6 trả content dạng list block, ví dụ [{"type": "text", "text": "..."}],
còn các bản cũ trả thẳng một chuỗi. Hàm này xử lý được cả hai trường hợp.
"""
if isinstance(content, str):
return content
if isinstance(content, list):
parts = []
for block in content:
if isinstance(block, dict):
parts.append(block.get("text", ""))
elif isinstance(block, str):
parts.append(block)
return "".join(parts)
return str(content)
def strip_token_footer(text):
"""Bỏ dòng thống kê token ở cuối câu trả lời (nếu có)."""
idx = text.rfind(TOKEN_FOOTER_MARKER)
if idx == -1:
return text
sep = text.rfind("\n\n", 0, idx)
return text[:sep].rstrip() if sep != -1 else text[:idx].rstrip()
def estimate_tokens(text):
"""Ước lượng số token khi provider không trả về 'usage' (xấp xỉ 4 ký tự/token)."""
if not isinstance(text, str):
text = str(text)
return len(text) // 4
def respond(
message,
history: list[dict],
model_name,
output_limit,
system_message,
temperature,
top_p,
hf_token: gr.OAuthToken,
):
"""
For more information on `huggingface_hub` Inference API support, please check the docs:
https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
"""
# Giới hạn token đầu ra cho CHÍNH câu hỏi này (giống IOAI: per query)
max_output = int(output_limit) if output_limit else DEFAULT_OUTPUT_LIMIT
client = InferenceClient(token=hf_token.token, model=model_name)
# Dựng messages, cắt bỏ dòng thống kê token khỏi history
messages = [{"role": "system", "content": system_message}]
for turn in history:
content = extract_text(turn["content"])
if turn["role"] == "assistant":
content = strip_token_footer(content)
messages.append({"role": turn["role"], "content": content})
messages.append({"role": "user", "content": extract_text(message)})
response = ""
usage = None
finish_reason = None
# max_tokens = giới hạn token đầu ra cho câu hỏi này -> model tự cắt khi đạt
for chunk in client.chat_completion(
messages,
max_tokens=max_output,
stream=True,
temperature=temperature,
top_p=top_p,
):
# Chunk cuối cùng mang 'usage' (token thật), choices rỗng
if getattr(chunk, "usage", None) is not None:
usage = chunk.usage
choices = chunk.choices
if len(choices):
if choices[0].finish_reason:
finish_reason = choices[0].finish_reason
if choices[0].delta.content:
response += choices[0].delta.content
yield response
# Số token ĐẦU RA đã dùng cho câu hỏi này
if usage is not None and getattr(usage, "completion_tokens", None) is not None:
out_tokens = usage.completion_tokens
prefix = ""
else:
out_tokens = max(1, estimate_tokens(response))
prefix = "~"
footer = f"\n\n`{TOKEN_FOOTER_MARKER} {prefix}{out_tokens}/{max_output} (mỗi câu hỏi)`"
# finish_reason == "length" nghĩa là câu trả lời bị cắt vì chạm giới hạn token
if finish_reason == "length":
footer += "\n`✂️ Câu trả lời đã chạm giới hạn token và bị cắt (giống IOAI).`"
yield response + footer
"""
For information on how to customize the ChatInterface, peruse the gradio docs:
https://www.gradio.app/docs/chatinterface
"""
chatbot = gr.ChatInterface(
respond,
additional_inputs=[
gr.Dropdown(
choices=MODELS,
value=MODELS[0],
label="Model",
allow_custom_value=True,
info="Mặc định Gemma 3 (như IOAI Individual). Có thể gõ model ID bất kỳ.",
),
gr.Slider(
minimum=1,
maximum=4096,
value=DEFAULT_OUTPUT_LIMIT,
step=1,
label="Giới hạn token đầu ra / câu hỏi",
info="Như IOAI: Individual = 1000, GAITE = 2000. Câu trả lời dài hơn sẽ bị cắt.",
),
gr.Textbox(value="You are a friendly Chatbot.", label="System message"),
gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
gr.Slider(
minimum=0.1,
maximum=1.0,
value=0.95,
step=0.05,
label="Top-p (nucleus sampling)",
),
],
)
with gr.Blocks() as demo:
with gr.Sidebar():
gr.LoginButton()
chatbot.render()
if __name__ == "__main__":
demo.launch()