CoolFace
Apppublic

dw112/PocketLLM

sourceHugging Faceapache-2.0updated 4mo agoView on Hugging Face
0likes
streamlit_app.py483 linesDownload Raw Back to src
1import random2import re3import json4import os5import base646from threading import Thread7from datetime import datetime8 9import torch10import numpy as np11import streamlit as st12from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer13 14st.set_page_config(page_title="PocketLLM", initial_sidebar_state="expanded", layout="wide")15 16# ================= 本地图片转 Base64 =================17def get_image_base64(filename):18    img_dir = "/app/images"19    path = os.path.join(img_dir, filename)20    if os.path.exists(path):21        with open(path, "rb") as f:22            return f"data:image/png;base64,{base64.b64encode(f.read()).decode()}"23    # 占位透明图24    return "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"25 26logo_b64 = get_image_base64("logo.png")27banner_b64 = get_image_base64("顶部横幅.png")28avatar_b64 = get_image_base64("助手头像.png")29# =================================================30 31st.markdown("""32    <style>33        /* 全局字体 (Gemini 风格) */34        @import url('https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap');35        html, body, [class*="css"] {36            font-family: 'Google Sans', 'Noto Sans SC', sans-serif !important;37            color: #1f1f1f;38        }39        .stMainBlockContainer {40            padding-top: 1rem !important;41            padding-bottom: 5rem !important;42        }43        /* 历史对话按钮模拟 */44        .history-btn {45            background: transparent;46            border: none;47            color: #444;48            padding: 10px 15px;49            width: 100%;50            text-align: left;51            border-radius: 8px;52            cursor: pointer;53            font-size: 14px;54            margin-bottom: 5px;55            transition: background 0.2s;56        }57        .history-btn:hover {58            background: #f0f4f9;59        }60        /* 滚动条 */61        ::-webkit-scrollbar { width: 6px; height: 6px; }62        ::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; }63        ::-webkit-scrollbar-track { background: transparent; }64    </style>65""", unsafe_allow_html=True)66 67device = "cuda" if torch.cuda.is_available() else "cpu"68 69# ================= 多语言文本 =================70LANG_TEXTS = {71    'zh': {72        'settings': '模型设定调整',73        'history_rounds': '历史对话轮次',74        'max_length': '最大生成长度',75        'temperature': '温度',76        'thinking': '✨ 开启深度思考',77        'tools': '工具选择 (最多4个)',78        'language': '语言',79        'send': '给 PocketLLM 发送消息...',80        'disclaimer': 'AI 生成内容可能存在错误,请仔细核实',81        'think_tip': '自适应思考,多轮对话或Tool Call时可能不稳定',82    },83    'en': {84        'settings': 'Model Settings',85        'history_rounds': 'History Rounds',86        'max_length': 'Max Length',87        'temperature': 'Temperature',88        'thinking': '✨ Enable Deep Thinking',89        'tools': 'Tool Selection (max 4)',90        'language': 'Language',91        'send': 'Message PocketLLM...',92        'disclaimer': 'AI-generated content may be inaccurate, please verify',93        'think_tip': 'Adaptive thinking; may be unstable with multi-turn or Tool Call',94    }95}96 97def get_text(key):98    lang = st.session_state.get('lang', 'zh')99    return LANG_TEXTS.get(lang, {}).get(key, LANG_TEXTS['zh'].get(key, key))100 101# ================= 工具定义 & 执行 (完全保持原版) =================102TOOLS = [103    {"type": "function", "function": {"name": "calculate_math", "description": "计算数学表达式", "parameters": {"type": "object", "properties": {"expression": {"type": "string", "description": "数学表达式"}}, "required": ["expression"]}}},104    {"type": "function", "function": {"name": "get_current_time", "description": "获取当前时间", "parameters": {"type": "object", "properties": {"timezone": {"type": "string", "default": "Asia/Shanghai"}}, "required": []}}},105    {"type": "function", "function": {"name": "random_number", "description": "生成随机数", "parameters": {"type": "object", "properties": {"min": {"type": "integer"}, "max": {"type": "integer"}}, "required": ["min", "max"]}}},106    {"type": "function", "function": {"name": "text_length", "description": "计算文本长度", "parameters": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}},107    {"type": "function", "function": {"name": "unit_converter", "description": "单位转换", "parameters": {"type": "object", "properties": {"value": {"type": "number"}, "from_unit": {"type": "string"}, "to_unit": {"type": "string"}}, "required": ["value", "from_unit", "to_unit"]}}},108    {"type": "function", "function": {"name": "get_current_weather", "description": "获取天气", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}},109    {"type": "function", "function": {"name": "get_exchange_rate", "description": "获取汇率", "parameters": {"type": "object", "properties": {"from_currency": {"type": "string"}, "to_currency": {"type": "string"}}, "required": ["from_currency", "to_currency"]}}},110    {"type": "function", "function": {"name": "translate_text", "description": "翻译文本", "parameters": {"type": "object", "properties": {"text": {"type": "string"}, "target_lang": {"type": "string"}}, "required": ["text", "target_lang"]}}},111]112 113TOOL_SHORT_NAMES = {114    'calculate_math': '数学', 'get_current_time': '时间', 'random_number': '随机',115    'text_length': '字数', 'unit_converter': '单位', 'get_current_weather': '天气',116    'get_exchange_rate': '汇率', 'translate_text': '翻译'117}118 119def execute_tool(tool_name, args):120    import datetime121    try:122        if tool_name == 'calculate_math':123            return {"result": eval(args.get('expression', '0'))}124        elif tool_name == 'get_current_time':125            return {"result": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}126        elif tool_name == 'random_number':127            return {"result": random.randint(args.get('min', 0), args.get('max', 100))}128        elif tool_name == 'text_length':129            return {"result": len(args.get('text', ''))}130        elif tool_name == 'unit_converter':131            return {"result": f"{args.get('value', 0)} {args.get('from_unit', '')} = ? {args.get('to_unit', '')}"}132        elif tool_name == 'get_current_weather':133            return {"result": f"{args.get('city', 'Unknown')}: 晴, 7~10°C"}134        elif tool_name == 'get_exchange_rate':135            return {"result": f"1 {args.get('from_currency', 'USD')} = 7.2 {args.get('to_currency', 'CNY')}"}136        elif tool_name == 'translate_text':137            return {"result": f"[翻译结果]: hello world"}138        return {"result": "Unknown tool"}139    except Exception as e:140        return {"error": str(e)}141 142# ================= 原版 process_assistant_content (完全保持副本逻辑) =================143def process_assistant_content(content, is_streaming=False):144    # 处理tool_call标签,格式化显示145    if '<tool_call>' in content:146        def format_tool_call(match):147            try:148                tc = json.loads(match.group(1))149                name = tc.get('name', 'unknown')150                args = tc.get('arguments', {})151                return f'<div style="background: rgba(80, 110, 150, 0.20); border: 1px solid rgba(140, 170, 210, 0.30); padding: 10px 12px; border-radius: 12px; margin: 6px 0;"><div style="font-size:12px;opacity:.75;display:block;margin:0 0 6px 0;line-height:1;">ToolCalling</div><div><b>{name}</b>: {json.dumps(args, ensure_ascii=False)}</div></div>'152            except:153                return match.group(0)154        content = re.sub(r'<tool_call>(.*?)</tool_call>', format_tool_call, content, flags=re.DOTALL)155    156    # 流式生成且开启思考时,一开始就放到折叠里157    if is_streaming and st.session_state.get('enable_thinking', False) and '</think>' not in content and '<think>' not in content:158        m = re.search(r'(\n\n(?:我是|您好|你好)[^\n]*)', content)159        if m and m.start(1) > 5:160            i = m.start(1)161            think_part = content[:i]162            answer_part = content[i:]163            return f'<details open style="border-left: 2px solid #666; padding-left: 12px; margin: 8px 0;"><summary style="cursor: pointer; color: #888;">已思考</summary><div style="color: #aaa; font-size: 0.95em; margin-top: 8px; max-height: 100px; overflow-y: auto;">{think_part.strip()}</div></details>{answer_part}'164        elif len(content) > 5:165            return f'<details open style="border-left: 2px solid #666; padding-left: 12px; margin: 8px 0;"><summary style="cursor: pointer; color: #888;">思考中...</summary><div style="color: #aaa; font-size: 0.95em; margin-top: 8px; max-height: 100px; overflow-y: auto; display: flex; flex-direction: column-reverse;"><div style="margin-bottom: auto;">{content.strip().replace(chr(10), "<br>")}</div></div></details>'166 167    if '<think>' in content and '</think>' in content:168        def format_think(match):169            think_content = match.group(2)170            if think_content.replace('\n', '').strip():171                return f'<details open style="border-left: 2px solid #666; padding-left: 12px; margin: 8px 0;"><summary style="cursor: pointer; color: #888;">已思考</summary><div style="color: #aaa; font-size: 0.95em; margin-top: 8px; max-height: 100px; overflow-y: auto;">{think_content.strip()}</div></details>'172            return ''173        content = re.sub(r'(<think>)(.*?)(</think>)', format_think, content, flags=re.DOTALL)174 175    if '<think>' in content and '</think>' not in content:176        def format_think_in_progress(match):177            tc = match.group(1)178            return f'<details open style="border-left: 2px solid #666; padding-left: 12px; margin: 8px 0;"><summary style="cursor: pointer; color: #888;">思考中...</summary><div style="color: #aaa; font-size: 0.95em; margin-top: 8px; max-height: 100px; overflow-y: auto; display: flex; flex-direction: column-reverse;"><div style="margin-bottom: auto;">{tc.strip().replace(chr(10), "<br>")}</div></div></details>'179        content = re.sub(r'<think>(.*?)$', format_think_in_progress, content, flags=re.DOTALL)180 181    if '<think>' not in content and '</think>' in content:182        def format_think_no_start(match):183            think_content = match.group(1)184            if think_content.replace('\n', '').strip():185                return f'<details open style="border-left: 2px solid #666; padding-left: 12px; margin: 8px 0;"><summary style="cursor: pointer; color: #888;">已思考</summary><div style="color: #aaa; font-size: 0.95em; margin-top: 8px; max-height: 100px; overflow-y: auto;">{think_content.strip()}</div></details>'186            return ''187        content = re.sub(r'(.*?)</think>', format_think_no_start, content, flags=re.DOTALL)188 189    return content190 191# ================= 模型加载 =================192@st.cache_resource193def load_model_tokenizer(model_path):194    model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True)195    tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)196    model = model.half().eval().to(device)197    return model, tokenizer198 199def clear_chat_messages():200    # 保存当前对话到历史(非空时)201    save_current_conversation()202    st.session_state.messages = []203    st.session_state.chat_messages = []204 205def save_current_conversation():206    """保存当前对话到历史记录列表中"""207    if "messages" not in st.session_state or not st.session_state.messages:208        return209    # 避免重复保存空对话210    if len(st.session_state.messages) == 0:211        return212    # 获取最后一条消息时间作为标题213    timestamp = datetime.now().strftime("%m-%d %H:%M")214    # 取用户的第一条消息作为预览215    preview = ""216    for msg in st.session_state.messages:217        if msg["role"] == "user":218            preview = msg["content"][:30]219            break220    title = f"{timestamp} - {preview}" if preview else timestamp221    222    # 保存到历史列表(最多20条)223    if "conversation_history" not in st.session_state:224        st.session_state.conversation_history = []225    # 避免与最后一条完全相同226    if st.session_state.conversation_history and st.session_state.conversation_history[-1]["messages"] == st.session_state.messages:227        return228    st.session_state.conversation_history.append({229        "title": title,230        "messages": st.session_state.messages.copy(),231        "chat_messages": st.session_state.chat_messages.copy()232    })233    # 保留最近20条234    if len(st.session_state.conversation_history) > 20:235        st.session_state.conversation_history = st.session_state.conversation_history[-20:]236 237def load_conversation(index):238    """加载指定索引的历史对话"""239    if 0 <= index < len(st.session_state.conversation_history):240        conv = st.session_state.conversation_history[index]241        st.session_state.messages = conv["messages"].copy()242        st.session_state.chat_messages = conv["chat_messages"].copy()243        st.rerun()244 245MODEL_PATHS = {246    "PocketLLM": ["/app/pout", "PocketLLM"]247}248 249# 动态扫描模型目录250# script_dir = os.path.dirname(os.path.abspath(__file__))251# MODEL_PATHS = {}252# for d in sorted(os.listdir(script_dir), reverse=True):253#     full_path = os.path.join(script_dir, d)254#     if os.path.isdir(full_path) and not d.startswith('.') and not d.startswith('_'):255#         if any(f.endswith(('.bin', '.safetensors', '.pt')) or os.path.exists(os.path.join(full_path, 'model.safetensors.index.json')) for f in os.listdir(full_path) if os.path.isfile(os.path.join(full_path, f))):256#             MODEL_PATHS[d] = [d, d]257# if not MODEL_PATHS:258#     MODEL_PATHS = {"No models found": ["", "No models"]}259 260# ================= 侧边栏 UI (Logo 放大 + 深度思考独立开关) =================261with st.sidebar:262    # 侧边栏顶部:大 Logo + 标题263    st.markdown(f'<div style="text-align: center; margin-bottom: 20px;"><img src="{logo_b64}" style="width: 100px; border-radius: 12px;"></div>', unsafe_allow_html=True)264    st.markdown("<h2 style='text-align: center; margin-top: -10px; margin-bottom: 20px;'>PocketLLM</h2>", unsafe_allow_html=True)265    266    # 新建对话按钮267    if st.button("➕ 新建对话", use_container_width=True, type="primary"):268        clear_chat_messages()269        st.rerun()270    271    st.markdown("<div style='margin-top: 24px; font-size: 13px; color: #5f6368; font-weight: 500; margin-bottom: 8px;'>历史对话</div>", unsafe_allow_html=True)272    273    # 动态显示所有已保存的历史对话列表(不再显示独立的“之前的讨论...”按钮)274    if "conversation_history" in st.session_state and st.session_state.conversation_history:275        for idx, conv in enumerate(reversed(st.session_state.conversation_history)):276            # 倒序显示,最新的在上277            if st.button(f"📝 {conv['title']}", key=f"hist_{idx}", use_container_width=True):278                load_conversation(len(st.session_state.conversation_history) - 1 - idx)279    else:280        st.caption("暂无历史对话,新建对话后会自动保存")281    282    st.markdown("<hr style='margin: 20px 0;'>", unsafe_allow_html=True)283    284    # 深度思考开关作为独立显眼组件285    st.session_state.enable_thinking = st.toggle(get_text('thinking'), value=st.session_state.get('enable_thinking', False), help=get_text('think_tip'))286    287    st.markdown("<hr style='margin: 20px 0 12px 0;'>", unsafe_allow_html=True)288    289    # 设置收纳在 Popover290    with st.popover("⚙️ 设置", use_container_width=True):291        selected_model = st.selectbox('模型 (Model)', list(MODEL_PATHS.keys()), index=0)292        lang_options = {'中文': 'zh', 'English': 'en'}293        current_lang = st.session_state.get('lang', 'zh')294        lang_index = 0 if current_lang == 'zh' else 1295        lang_label = st.radio('语言 (Language)', list(lang_options.keys()), index=lang_index, horizontal=True)296        if lang_options[lang_label] != current_lang:297            st.session_state.lang = lang_options[lang_label]298            st.rerun()299        300        st.divider()301        st.session_state.history_chat_num = st.slider(get_text('history_rounds'), 0, 8, 0, step=2)302        st.session_state.max_new_tokens = st.slider(get_text('max_length'), 256, 8192, 8192, step=1)303        st.session_state.temperature = st.slider(get_text('temperature'), 0.6, 1.2, 0.90, step=0.01)304        305        st.divider()306        st.caption(get_text('tools'))307        st.session_state.selected_tools = []308        selected_count = sum(1 for tool in TOOLS if st.session_state.get(f"tool_{tool['function']['name']}", False))309        t_col1, t_col2 = st.columns(2)310        for i, tool in enumerate(TOOLS):311            name = tool['function']['name']312            short_name = TOOL_SHORT_NAMES.get(name, name)313            col = t_col1 if i % 2 == 0 else t_col2314            with col:315                checked = st.checkbox(short_name, key=f"tool_{name}", disabled=(selected_count >= 4 and not st.session_state.get(f"tool_{name}", False)))316                if checked and len(st.session_state.selected_tools) < 4:317                    st.session_state.selected_tools.append(name)318 319model_path = MODEL_PATHS[selected_model][0]320slogan = f"我是 {MODEL_PATHS[selected_model][1]},有什么可以帮你的?" if st.session_state.get('lang', 'zh') == 'zh' else f"I am {MODEL_PATHS[selected_model][1]}, how can I help you?"321 322 323def render_user_msg(text):324    return f'<div style="display: flex; justify-content: flex-end; margin: 16px 0;"><div style="background-color: #f0f4f9; color: #1f1f1f; padding: 12px 20px; border-radius: 24px; font-size: 15px; max-width: 80%; line-height: 1.6;">{text}</div></div>'325 326def render_bot_msg(html_content):327    return f'<div style="display: flex; gap: 16px; margin: 20px 0;"><img src="{avatar_b64}" style="width: 34px; height: 34px; border-radius: 50%; object-fit: cover; border: 1px solid #eee;"><div style="color: #1f1f1f; font-size: 15px; padding-top: 4px; line-height: 1.7; flex: 1; overflow-x: auto;">{html_content}</div></div>'328 329def setup_seed(seed):330    random.seed(seed)331    np.random.seed(seed)332    torch.manual_seed(seed)333    torch.cuda.manual_seed(seed)334    torch.cuda.manual_seed_all(seed)335    torch.backends.cudnn.deterministic = True336    torch.backends.cudnn.benchmark = False337 338# ================= Main =================339# def main():340 341 342#     st.write("当前工作目录:", os.getcwd())343 344#     st.write("/app 内容:", os.listdir("/app"))345 346#     if os.path.exists("/app/src"):347#         st.write("/app/src 内容:", os.listdir("/app/src"))348 349#     if os.path.exists("/app/pout"):350#         st.write("/app/pout 内容:", os.listdir("/app/pout"))351 352#     model, tokenizer = load_model_tokenizer("../pout")353def main():354    # model, tokenizer = load_model_tokenizer("/app/pout")355    model, tokenizer = load_model_tokenizer(model_path)356 357    if "messages" not in st.session_state:358        st.session_state.messages = []359        st.session_state.chat_messages = []360    if "conversation_history" not in st.session_state:361        st.session_state.conversation_history = []362 363    messages = st.session_state.messages364 365    # 主界面顶部:横幅图片(修复截断问题:使用 contain 确保完整显示)366    if banner_b64 and not banner_b64.endswith("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"):367        st.markdown(368            f'<div style="display: flex; justify-content: center; overflow: visible; padding-top: 18px;">'369            f'<img src="{banner_b64}" style="width: auto; max-width: 500px; max-height: 150px; height: auto; object-fit: contain; border-radius: 12px; margin-bottom: 20px;">'370            f'</div>',371            unsafe_allow_html=True372        )373 374    # 欢迎界面 (无历史消息时)375    if not messages:376        st.markdown(377            f'<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; margin-top: 2rem;">'378            f'<h1 style="font-size: 28px; color: #202124; margin-bottom: 8px;">{slogan}</h1>'379            f'<p style="color: #5f6368; font-size: 14px;">{get_text("disclaimer")}</p>'380            '</div>',381            unsafe_allow_html=True382        )383 384    # 渲染历史消息 (带左侧头像)385    for message in messages:386        if message["role"] == "assistant":387            st.markdown(render_bot_msg(process_assistant_content(message["content"])), unsafe_allow_html=True)388        else:389            st.markdown(render_user_msg(message["content"]), unsafe_allow_html=True)390 391    392    prompt = st.chat_input(key="input", placeholder=get_text('send'))393 394    if prompt:395        396        st.markdown(render_user_msg(prompt), unsafe_allow_html=True)397        messages.append({"role": "user", "content": prompt[-st.session_state.max_new_tokens:]})398        st.session_state.chat_messages.append({"role": "user", "content": prompt[-st.session_state.max_new_tokens:]})399 400        placeholder = st.empty()  # 用于流式输出401 402        random_seed = random.randint(0, 2 ** 32 - 1)403        setup_seed(random_seed)404 405 406        tools = [t for t in TOOLS if t['function']['name'] in st.session_state.get('selected_tools', [])] or None407        sys_prompt = [] if tools else [{"role": "system", "content": "你是PocketLLM,一个乐于助人、知识渊博的AI助手。请用完整且友好的方式回答用户问题。"}]408        st.session_state.chat_messages = sys_prompt + st.session_state.chat_messages[-(st.session_state.history_chat_num + 1):]409 410        template_kwargs = {"tokenize": False, "add_generation_prompt": True}411        if st.session_state.get('enable_thinking', False):412            template_kwargs["open_thinking"] = True413        if tools:414            template_kwargs["tools"] = tools415 416        new_prompt = tokenizer.apply_chat_template(st.session_state.chat_messages, **template_kwargs)417        inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)418 419        # 流式生成420        streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)421        generation_kwargs = {422            "input_ids": inputs.input_ids,423            "max_length": inputs.input_ids.shape[1] + st.session_state.max_new_tokens,424            "num_return_sequences": 1,425            "do_sample": True,426            "attention_mask": inputs.attention_mask,427            "pad_token_id": tokenizer.pad_token_id,428            "eos_token_id": tokenizer.eos_token_id,429            "temperature": st.session_state.temperature,430            "top_p": 0.85,431            "streamer": streamer,432        }433 434        Thread(target=model.generate, kwargs=generation_kwargs).start()435 436        answer = ""437        for new_text in streamer:438            answer += new_text439            placeholder.markdown(process_assistant_content(answer, is_streaming=True), unsafe_allow_html=True)440 441        full_answer = answer442 443       444        for _ in range(16):445            tool_calls = re.findall(r'<tool_call>(.*?)</tool_call>', answer, re.DOTALL)446            if not tool_calls:447                break448            st.session_state.chat_messages.append({"role": "assistant", "content": answer})449            tool_results = []450            for tc_str in tool_calls:451                try:452                    tc = json.loads(tc_str.strip())453                    result = execute_tool(tc.get('name', ''), tc.get('arguments', {}))454                    st.session_state.chat_messages.append({"role": "tool", "content": json.dumps(result, ensure_ascii=False)})455                    tool_results.append(f'<div style="background: rgba(90, 130, 110, 0.20); border: 1px solid rgba(150, 200, 170, 0.30); padding: 10px 12px; border-radius: 12px; margin: 6px 0;"><div style="font-size:12px;opacity:.75;display:block;margin:0 0 6px 0;line-height:1;">ToolCalled</div><div><b>{tc.get("name", "")}</b>: {json.dumps(result, ensure_ascii=False)}</div></div>')456                except:457                    pass458            full_answer += "\n" + "\n".join(tool_results) + "\n"459            placeholder.markdown(process_assistant_content(full_answer, is_streaming=True), unsafe_allow_html=True)460 461            new_prompt = tokenizer.apply_chat_template(st.session_state.chat_messages, **template_kwargs)462            inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)463            streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)464            generation_kwargs["input_ids"] = inputs.input_ids465            generation_kwargs["attention_mask"] = inputs.attention_mask466            generation_kwargs["max_length"] = inputs.input_ids.shape[1] + st.session_state.max_new_tokens467            generation_kwargs["streamer"] = streamer468 469            Thread(target=model.generate, kwargs=generation_kwargs).start()470            answer = ""471            for new_text in streamer:472                answer += new_text473                placeholder.markdown(process_assistant_content(full_answer + answer, is_streaming=True), unsafe_allow_html=True)474            full_answer += answer475 476        answer = full_answer477        messages.append({"role": "assistant", "content": answer})478        st.session_state.chat_messages.append({"role": "assistant", "content": answer})479        480      481 482if __name__ == "__main__":483    main()