[update] minimind-3

This commit is contained in:
jingyaogong
2026-03-25 23:57:45 +08:00
parent 83e52f6a27
commit 101d7df2da
60 changed files with 34516 additions and 33285 deletions
@@ -1,8 +1,8 @@
from openai import OpenAI
client = OpenAI(
api_key="ollama",
base_url="http://127.0.0.1:8998/v1"
api_key="sk-123",
base_url="http://localhost:11434/v1"
)
stream = True
conversation_history_origin = []
@@ -12,22 +12,29 @@ while True:
query = input('[Q]: ')
conversation_history.append({"role": "user", "content": query})
response = client.chat.completions.create(
model="minimind",
model="minimind-local:latest",
messages=conversation_history[-(history_messages_num or 1):],
stream=stream,
temperature=0.7,
temperature=0.8,
max_tokens=2048,
top_p=0.9
top_p=0.8,
extra_body={"chat_template_kwargs": {"open_thinking": True}, "reasoning_effort": "medium"} # 思考开关
)
if not stream:
assistant_res = response.choices[0].message.content
print('[A]: ', assistant_res)
else:
print('[A]: ', end='')
print('[A]: ', end='', flush=True)
assistant_res = ''
for chunk in response:
print(chunk.choices[0].delta.content or "", end="")
assistant_res += chunk.choices[0].delta.content or ""
delta = chunk.choices[0].delta
r = getattr(delta, 'reasoning_content', None) or ""
c = delta.content or ""
if r:
print(f'\033[90m{r}\033[0m', end="", flush=True)
if c:
print(c, end="", flush=True)
assistant_res += c
conversation_history.append({"role": "assistant", "content": assistant_res})
print('\n\n')
print('\n\n')
+100 -34
View File
@@ -5,14 +5,14 @@ import json
__package__ = "scripts"
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import torch
import transformers
import warnings
from transformers import AutoTokenizer, AutoModelForCausalLM, LlamaConfig, LlamaForCausalLM
from transformers import AutoTokenizer, AutoModelForCausalLM, Qwen3Config, Qwen3ForCausalLM, Qwen3MoeConfig, Qwen3MoeForCausalLM
from model.model_minimind import MiniMindConfig, MiniMindForCausalLM
from model.model_lora import apply_lora, merge_lora
warnings.filterwarnings('ignore', category=UserWarning)
# MoE模型需使用此函数转换
def convert_torch2transformers_minimind(torch_path, transformers_path, dtype=torch.float16):
MiniMindConfig.register_for_auto_class()
MiniMindForCausalLM.register_for_auto_class("AutoModelForCausalLM")
@@ -26,52 +26,118 @@ def convert_torch2transformers_minimind(torch_path, transformers_path, dtype=tor
lm_model.save_pretrained(transformers_path, safe_serialization=False)
tokenizer = AutoTokenizer.from_pretrained('../model/')
tokenizer.save_pretrained(transformers_path)
# 兼容transformers-5.0的写法
config_path = os.path.join(transformers_path, "tokenizer_config.json")
json.dump({**json.load(open(config_path, 'r', encoding='utf-8')), "tokenizer_class": "PreTrainedTokenizerFast", "extra_special_tokens": {}}, open(config_path, 'w', encoding='utf-8'), indent=2, ensure_ascii=False)
# ======= transformers-5.0的兼容低版本写法 =======
if int(transformers.__version__.split('.')[0]) >= 5:
tokenizer_config_path, config_path = os.path.join(transformers_path, "tokenizer_config.json"), os.path.join(transformers_path, "config.json")
json.dump({**json.load(open(tokenizer_config_path, 'r', encoding='utf-8')), "tokenizer_class": "PreTrainedTokenizerFast", "extra_special_tokens": {}}, open(tokenizer_config_path, 'w', encoding='utf-8'), indent=2, ensure_ascii=False)
config = json.load(open(config_path, 'r', encoding='utf-8'))
config['rope_theta'] = lm_config.rope_theta; config['rope_scaling'] = None; del config['rope_parameters']
json.dump(config, open(config_path, 'w', encoding='utf-8'), indent=2, ensure_ascii=False)
print(f"模型已保存为 Transformers-MiniMind 格式: {transformers_path}")
# LlamaForCausalLM结构兼容第三方生态
def convert_torch2transformers_llama(torch_path, transformers_path, dtype=torch.float16):
# QwenForCausalLM/LlamaForCausalLM结构兼容生态
def convert_torch2transformers(torch_path, transformers_path, dtype=torch.float16):
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
state_dict = torch.load(torch_path, map_location=device)
llama_config = LlamaConfig(
vocab_size=lm_config.vocab_size,
hidden_size=lm_config.hidden_size,
intermediate_size=64 * ((int(lm_config.hidden_size * 8 / 3) + 64 - 1) // 64),
num_hidden_layers=lm_config.num_hidden_layers,
num_attention_heads=lm_config.num_attention_heads,
num_key_value_heads=lm_config.num_key_value_heads,
max_position_embeddings=lm_config.max_position_embeddings,
rms_norm_eps=lm_config.rms_norm_eps,
rope_theta=lm_config.rope_theta,
tie_word_embeddings=True
)
llama_model = LlamaForCausalLM(llama_config)
llama_model.load_state_dict(state_dict, strict=False)
llama_model = llama_model.to(dtype) # 转换模型权重精度
llama_model.save_pretrained(transformers_path)
model_params = sum(p.numel() for p in llama_model.parameters() if p.requires_grad)
common_config = {
"vocab_size": lm_config.vocab_size,
"hidden_size": lm_config.hidden_size,
"intermediate_size": lm_config.intermediate_size,
"num_hidden_layers": lm_config.num_hidden_layers,
"num_attention_heads": lm_config.num_attention_heads,
"num_key_value_heads": lm_config.num_key_value_heads,
"head_dim": lm_config.hidden_size // lm_config.num_attention_heads,
"max_position_embeddings": lm_config.max_position_embeddings,
"rms_norm_eps": lm_config.rms_norm_eps,
"rope_theta": lm_config.rope_theta,
"tie_word_embeddings": True
}
if not lm_config.use_moe:
qwen_config = Qwen3Config(
**common_config,
use_sliding_window=False,
sliding_window=None
)
qwen_model = Qwen3ForCausalLM(qwen_config)
else:
qwen_config = Qwen3MoeConfig(
**common_config,
num_experts=lm_config.num_experts,
num_experts_per_tok=lm_config.num_experts_per_tok,
moe_intermediate_size=lm_config.moe_intermediate_size,
norm_topk_prob=lm_config.norm_topk_prob
)
qwen_model = Qwen3MoeForCausalLM(qwen_config)
# ======= transformers-5.0的兼容低版本写法 =======
if int(transformers.__version__.split('.')[0]) >= 5:
new_sd = {k: v for k, v in state_dict.items() if 'experts.' not in k or 'gate.weight' in k}
for l in range(lm_config.num_hidden_layers):
p = f'model.layers.{l}.mlp.experts'
new_sd[f'{p}.gate_up_proj'] = torch.cat([torch.stack([state_dict[f'{p}.{e}.gate_proj.weight'] for e in range(lm_config.num_experts)]), torch.stack([state_dict[f'{p}.{e}.up_proj.weight'] for e in range(lm_config.num_experts)])], dim=1)
new_sd[f'{p}.down_proj'] = torch.stack([state_dict[f'{p}.{e}.down_proj.weight'] for e in range(lm_config.num_experts)])
state_dict = new_sd
qwen_model.load_state_dict(state_dict, strict=True)
qwen_model = qwen_model.to(dtype) # 转换模型权重精度
qwen_model.save_pretrained(transformers_path)
model_params = sum(p.numel() for p in qwen_model.parameters() if p.requires_grad)
print(f'模型参数: {model_params / 1e6} 百万 = {model_params / 1e9} B (Billion)')
tokenizer = AutoTokenizer.from_pretrained('../model/')
tokenizer.save_pretrained(transformers_path)
# 兼容transformers-5.0的写法
config_path = os.path.join(transformers_path, "tokenizer_config.json")
json.dump({**json.load(open(config_path, 'r', encoding='utf-8')), "tokenizer_class": "PreTrainedTokenizerFast", "extra_special_tokens": {}}, open(config_path, 'w', encoding='utf-8'), indent=2, ensure_ascii=False)
print(f"模型已保存为 Transformers-Llama 格式: {transformers_path}")
# ======= transformers-5.0的兼容低版本写法 =======
if int(transformers.__version__.split('.')[0]) >= 5:
tokenizer_config_path, config_path = os.path.join(transformers_path, "tokenizer_config.json"), os.path.join(transformers_path, "config.json")
json.dump({**json.load(open(tokenizer_config_path, 'r', encoding='utf-8')), "tokenizer_class": "PreTrainedTokenizerFast", "extra_special_tokens": {}}, open(tokenizer_config_path, 'w', encoding='utf-8'), indent=2, ensure_ascii=False)
config = json.load(open(config_path, 'r', encoding='utf-8'))
config['rope_theta'] = lm_config.rope_theta; config['rope_scaling'] = None; del config['rope_parameters']
json.dump(config, open(config_path, 'w', encoding='utf-8'), indent=2, ensure_ascii=False)
print(f"模型已保存为 Transformers 格式: {transformers_path}")
def convert_transformers2torch(transformers_path, torch_path):
model = AutoModelForCausalLM.from_pretrained(transformers_path, trust_remote_code=True)
torch.save({k: v.cpu().half() for k, v in model.state_dict().items()}, torch_path)
print(f"模型已保存为 PyTorch 格式 (half精度): {torch_path}")
print(f"模型已保存为 PyTorch 格式: {torch_path}")
def convert_merge_base_lora(base_torch_path, lora_path, merged_torch_path):
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
lm_model = MiniMindForCausalLM(lm_config).to(device)
state_dict = torch.load(base_torch_path, map_location=device)
lm_model.load_state_dict(state_dict, strict=False)
apply_lora(lm_model)
merge_lora(lm_model, lora_path, merged_torch_path)
print(f"LoRA 已合并并保存为基模结构 PyTorch 格式: {merged_torch_path}")
def convert_jinja_to_json(jinja_path):
with open(jinja_path, 'r') as f: template = f.read()
escaped = json.dumps(template)
print(f'"chat_template": {escaped}')
def convert_json_to_jinja(json_file_path, output_path):
with open(json_file_path, 'r') as f: config = json.load(f)
template = config['chat_template']
with open(output_path, 'w') as f: f.write(template)
print(f"模板已保存为 jinja 文件: {output_path}")
if __name__ == '__main__':
lm_config = MiniMindConfig(hidden_size=512, num_hidden_layers=8, max_seq_len=8192, use_moe=False)
lm_config = MiniMindConfig(hidden_size=768, num_hidden_layers=8, max_seq_len=8192, use_moe=True)
# convert torch to transformers
torch_path = f"../out/full_sft_{lm_config.hidden_size}{'_moe' if lm_config.use_moe else ''}.pth"
transformers_path = '../MiniMind2-Small'
convert_torch2transformers_llama(torch_path, transformers_path)
# # convert transformers to torch model
transformers_path = '../minimind-3-moe'
convert_torch2transformers(torch_path, transformers_path)
# # merge lora
# base_torch_path = f"../out/full_sft_{lm_config.hidden_size}{'_moe' if lm_config.use_moe else ''}.pth"
# lora_path = f"../out/lora_identity_{lm_config.hidden_size}.pth"
# merged_torch_path = f"../out/merge_identity_{lm_config.hidden_size}{'_moe' if lm_config.use_moe else ''}.pth"
# convert_merge_base_lora(base_torch_path, lora_path, merged_torch_path)
# convert_transformers2torch(transformers_path, torch_path)
# convert_json_to_jinja('../model/tokenizer_config.json', '../model/chat_template.jinja')
# convert_jinja_to_json('../model/chat_template.jinja')
+240
View File
@@ -0,0 +1,240 @@
import os
import sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import re
import json
import time
import random
import argparse
import warnings
import torch
from datetime import datetime
from transformers import AutoTokenizer, AutoModelForCausalLM, TextStreamer
from openai import OpenAI
from model.model_minimind import MiniMindConfig, MiniMindForCausalLM
from trainer.trainer_utils import setup_seed, get_model_params
warnings.filterwarnings('ignore')
TOOLS = [
{"type": "function", "function": {"name": "calculate_math", "description": "计算数学表达式的结果,支持加减乘除、幂运算、开方等", "parameters": {"type": "object", "properties": {"expression": {"type": "string", "description": "数学表达式,如123+456、2**10、sqrt(144)"}}, "required": ["expression"]}}},
{"type": "function", "function": {"name": "get_current_time", "description": "获取当前日期和时间,支持指定时区", "parameters": {"type": "object", "properties": {"timezone": {"type": "string", "description": "时区名称,如Asia/Shanghai、America/New_York", "default": "Asia/Shanghai"}}, "required": []}}},
{"type": "function", "function": {"name": "random_number", "description": "生成指定范围内的随机数", "parameters": {"type": "object", "properties": {"min": {"type": "integer", "description": "最小值", "default": 0}, "max": {"type": "integer", "description": "最大值", "default": 100}}, "required": []}}},
{"type": "function", "function": {"name": "text_length", "description": "计算文本的字符数和单词数", "parameters": {"type": "object", "properties": {"text": {"type": "string", "description": "要统计的文本"}}, "required": ["text"]}}},
{"type": "function", "function": {"name": "unit_converter", "description": "进行单位换算,支持长度、重量、温度等", "parameters": {"type": "object", "properties": {"value": {"type": "number", "description": "要转换的数值"}, "from_unit": {"type": "string", "description": "源单位,如km、miles、kg、pounds、celsius、fahrenheit"}, "to_unit": {"type": "string", "description": "目标单位"}}, "required": ["value", "from_unit", "to_unit"]}}},
{"type": "function", "function": {"name": "get_current_weather", "description": "获取指定城市的当前天气信息,包括温度、湿度和天气状况", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "城市名称,如北京、上海、New York"}, "unit": {"type": "string", "description": "温度单位,celsius或fahrenheit", "enum": ["celsius", "fahrenheit"], "default": "celsius"}}, "required": ["location"]}}},
{"type": "function", "function": {"name": "get_exchange_rate", "description": "查询两种货币之间的实时汇率", "parameters": {"type": "object", "properties": {"from_currency": {"type": "string", "description": "源货币代码,如USD、CNY、EUR"}, "to_currency": {"type": "string", "description": "目标货币代码,如USD、CNY、EUR"}}, "required": ["from_currency", "to_currency"]}}},
{"type": "function", "function": {"name": "translate_text", "description": "将文本翻译成目标语言", "parameters": {"type": "object", "properties": {"text": {"type": "string", "description": "要翻译的文本"}, "target_language": {"type": "string", "description": "目标语言,如english、chinese、japanese、french"}}, "required": ["text", "target_language"]}}},
]
MOCK_RESULTS = {
"calculate_math": lambda args: {"result": str(eval(str(args.get("expression", "0")).replace("^", "**").replace("×", "*").replace("÷", "/").replace("", "-").replace("²", "**2").replace("³", "**3").replace("", "(").replace("", ")")))},
"get_current_time": lambda args: {"datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "timezone": args.get("timezone", "Asia/Shanghai")},
"random_number": lambda args: {"result": random.randint(int(args.get("min", 0)), int(args.get("max", 100)))},
"text_length": lambda args: {"characters": len(args.get("text", "")), "words": len(args.get("text", "").split())},
"unit_converter": lambda args: {"result": round(float(args.get("value", 0)) * 0.621371, 2), "from": f"{args.get('value', 0)} {args.get('from_unit', '')}", "to": args.get("to_unit", "")},
"get_current_weather": lambda args: {"city": args.get("location"), "temperature": "22°C", "humidity": "65%", "condition": ""},
"get_exchange_rate": lambda args: {"from": args.get("from_currency", ""), "to": args.get("to_currency", ""), "rate": 7.15},
"translate_text": lambda args: {"translated": "hello world"},
}
TOOL_MAP = {t["function"]["name"]: t for t in TOOLS}
def get_tools(names):
return [TOOL_MAP[n] for n in names]
TEST_CASES = [
{"prompt": "帮我算一下 256 乘以 37 等于多少", "tools": ["calculate_math", "get_current_time"]},
{"prompt": "现在几点了?", "tools": ["get_current_time", "random_number"]},
{"prompt": "帮我把100公里换算成英里", "tools": ["unit_converter", "calculate_math"]},
{"prompt": "帮我生成一个1到1000的随机数,然后计算它的平方", "tools": ["random_number", "calculate_math", "text_length"]},
{"prompt": "北京今天天气怎么样?", "tools": ["get_current_weather", "get_current_time"]},
{"prompt": "查一下美元兑人民币汇率", "tools": ["get_exchange_rate", "get_current_time"]},
{"prompt": "'你好世界'翻译成英文", "tools": ["translate_text", "text_length"]},
{"prompt": "What is the weather in Tokyo? Also convert 30 celsius to fahrenheit.", "tools": ["get_current_weather", "unit_converter", "get_current_time"]},
]
def init_model(args):
tokenizer = AutoTokenizer.from_pretrained(args.load_from)
if 'model' in args.load_from:
model = MiniMindForCausalLM(MiniMindConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers, use_moe=bool(args.use_moe)))
moe_suffix = '_moe' if args.use_moe else ''
ckp = f'./{args.save_dir}/{args.weight}_{args.hidden_size}{moe_suffix}.pth'
model.load_state_dict(torch.load(ckp, map_location=args.device), strict=True)
else:
model = AutoModelForCausalLM.from_pretrained(args.load_from, trust_remote_code=True)
get_model_params(model, model.config)
return model.eval().to(args.device), tokenizer
def parse_tool_calls(text):
matches = re.findall(r'<tool_call>(.*?)</tool_call>', text, re.DOTALL)
calls = []
for m in matches:
try:
calls.append(json.loads(m.strip()))
except Exception:
pass
return calls
def parse_tool_call_from_text(content):
pattern = r'<tool_call>\s*(\{.*?\})\s*</tool_call>'
matches = re.findall(pattern, content, re.DOTALL)
if not matches:
return None
tool_calls = []
for i, match in enumerate(matches):
try:
data = json.loads(match)
tool_calls.append({
"id": f"call_{i}",
"function": {"name": data.get("name", ""), "arguments": json.dumps(data.get("arguments", {}), ensure_ascii=False)}
})
except Exception:
pass
return tool_calls if tool_calls else None
def execute_tool(call, arguments=None):
name = call.get("name", "") if isinstance(call, dict) else call
try:
raw_args = call.get("arguments", {}) if isinstance(call, dict) else arguments
args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
except Exception:
args = {}
fn = MOCK_RESULTS.get(name)
if not fn:
return {"error": f"未知工具: {name}"}
try:
return fn(args)
except Exception as e:
return {"error": f"工具执行失败: {str(e)[:80]}"}
def generate(model, tokenizer, messages, tools, args):
streamer = TextStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
input_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=tools, open_thinking=False)
inputs = tokenizer(input_text, return_tensors="pt", truncation=True).to(args.device)
st = time.time()
print('🧠: ', end='')
generated_ids = model.generate(
inputs["input_ids"], attention_mask=inputs["attention_mask"],
max_new_tokens=args.max_new_tokens, do_sample=True, streamer=streamer,
pad_token_id=tokenizer.pad_token_id, eos_token_id=tokenizer.eos_token_id,
top_p=args.top_p, temperature=args.temperature
)
response = tokenizer.decode(generated_ids[0][len(inputs["input_ids"][0]):], skip_special_tokens=True)
gen_tokens = len(generated_ids[0]) - len(inputs["input_ids"][0])
print(f'\n[Speed]: {gen_tokens / (time.time() - st):.2f} tokens/s') if args.show_speed else print()
return response
def chat_api(client, messages, tools, args, stream=True):
response = client.chat.completions.create(
model=args.api_model, messages=messages, tools=tools,
stream=stream, temperature=args.temperature,
max_tokens=8192, top_p=args.top_p
)
if not stream:
choice = response.choices[0]
content = choice.message.content or ""
tool_calls = choice.message.tool_calls
if not tool_calls:
tool_calls = parse_tool_call_from_text(content)
print(f'🧠: {content}')
return content, tool_calls
print('🧠: ', end='', flush=True)
content, tool_calls = "", None
for chunk in response:
delta = chunk.choices[0].delta
if delta.content:
print(delta.content, end="", flush=True)
content += delta.content
if delta.tool_calls:
if tool_calls is None:
tool_calls = []
for tc_chunk in delta.tool_calls:
idx = tc_chunk.index if tc_chunk.index is not None else len(tool_calls)
while len(tool_calls) <= idx:
tool_calls.append({
"id": "",
"function": {"name": "", "arguments": ""}
})
if tc_chunk.id:
tool_calls[idx]["id"] += tc_chunk.id
if tc_chunk.function:
if tc_chunk.function.name:
tool_calls[idx]["function"]["name"] += tc_chunk.function.name
if tc_chunk.function.arguments:
tool_calls[idx]["function"]["arguments"] += tc_chunk.function.arguments
print()
if not tool_calls:
tool_calls = parse_tool_call_from_text(content)
return content, tool_calls
def run_case(prompt, tools, args, model=None, tokenizer=None, client=None):
messages = [{"role": "user", "content": prompt}]
while True:
if args.backend == 'local':
content = generate(model, tokenizer, messages, tools, args)
tool_calls = parse_tool_calls(content)
else:
content, tool_calls = chat_api(client, messages, tools, args, stream=bool(args.stream))
if not tool_calls:
break
tool_calls = [{
"id": tc.id if hasattr(tc, 'id') else tc.get("id", ""),
"name": tc.function.name if hasattr(tc, 'function') else tc["function"]["name"],
"arguments": tc.function.arguments if hasattr(tc, 'function') else tc["function"]["arguments"]
} for tc in tool_calls] if args.backend == 'api' else tool_calls
messages.append({"role": "assistant", "content": content} if args.backend == 'local' else {"role": "assistant", "content": content, "tool_calls": [{"id": tc["id"], "type": "function", "function": {"name": tc["name"], "arguments": tc["arguments"]}} for tc in tool_calls]})
for tc in tool_calls:
name = tc["name"]
arguments = tc["arguments"]
print(f'📞 [Tool Calling]: {name} | args={arguments}')
result = execute_tool(tc if args.backend == 'local' else name, arguments)
print(f'✅ [Tool Called]: {json.dumps(result, ensure_ascii=False)}')
messages.append({"role": "tool", "content": json.dumps(result, ensure_ascii=False)} if args.backend == 'local' else {"role": "tool", "content": json.dumps(result, ensure_ascii=False), "tool_call_id": tc["id"]})
def main():
parser = argparse.ArgumentParser(description="MiniMind ToolCall评测")
parser.add_argument('--backend', default='local', choices=['local', 'api'], type=str, help="推理后端(local=本地模型,api=OpenAI兼容接口)")
parser.add_argument('--load_from', default='../model', type=str, help="模型加载路径(model=原生torch权重,其他路径=transformers格式)")
parser.add_argument('--save_dir', default='../out', type=str, help="模型权重目录")
parser.add_argument('--weight', default='full_sft', type=str, help="权重名称前缀(pretrain, full_sft, rlhf, reason, ppo_actor, grpo, spo")
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
parser.add_argument('--max_new_tokens', default=512, type=int, help="最大生成长度")
parser.add_argument('--temperature', default=0.9, type=float, help="生成温度,控制随机性(0-1,越大越随机)")
parser.add_argument('--top_p', default=0.9, type=float, help="nucleus采样阈值(0-1")
parser.add_argument('--show_speed', default=0, type=int, help="显示decode速度(tokens/s")
parser.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu', type=str, help="运行设备")
parser.add_argument('--api_base_url', default="http://localhost:11434/v1", type=str, help="OpenAI兼容接口的base_url")
parser.add_argument('--api_key', default='sk-123', type=str, help="OpenAI兼容接口的api_key")
parser.add_argument('--api_model', default='jingyaogong/minimind-3:latest', type=str, help="API请求时使用的模型名称")
parser.add_argument('--stream', default=1, type=int, help="API模式下是否流式输出(0=否,1=是)")
args = parser.parse_args()
model = tokenizer = client = None
if args.backend == 'local': model, tokenizer = init_model(args)
else: client = OpenAI(api_key=args.api_key, base_url=args.api_base_url)
input_mode = int(input('[0] 自动测试\n[1] 手动输入\n'))
cases = [{"prompt": case["prompt"], "tools": get_tools(case["tools"]), "tool_names": case["tools"]} for case in TEST_CASES] if input_mode == 0 else iter(lambda: {"prompt": input('💬: '), "tools": TOOLS, "tool_names": [t["function"]["name"] for t in TOOLS]}, {"prompt": "", "tools": TOOLS, "tool_names": []})
for case in cases:
if not case["prompt"]: break
setup_seed(random.randint(0, 31415926))
if input_mode == 0:
print(f'📦 可用工具: {case["tool_names"]}\n')
print(f'💬: {case["prompt"]}')
run_case(case["prompt"], case["tools"], args, model=model, tokenizer=tokenizer, client=client)
print('\n' + '-' * 50 + '\n')
if __name__ == "__main__":
main()
+86 -18
View File
@@ -1,5 +1,6 @@
import argparse
import json
import re
import os
import sys
@@ -52,8 +53,19 @@ class ChatRequest(BaseModel):
temperature: float = 0.7
top_p: float = 0.92
max_tokens: int = 8192
stream: bool = False
stream: bool = True
tools: list = []
open_thinking: bool = False
chat_template_kwargs: dict = None
def get_open_thinking(self) -> bool:
"""兼容多种方式开启 thinking"""
if self.open_thinking:
return True
if self.chat_template_kwargs:
return self.chat_template_kwargs.get('open_thinking', False) or \
self.chat_template_kwargs.get('enable_thinking', False)
return False
class CustomStreamer(TextStreamer):
@@ -68,9 +80,31 @@ class CustomStreamer(TextStreamer):
self.queue.put(None)
def generate_stream_response(messages, temperature, top_p, max_tokens):
def parse_response(text):
reasoning_content = None
think_match = re.search(r'<think>(.*?)</think>', text, re.DOTALL)
if think_match:
reasoning_content = think_match.group(1).strip()
text = re.sub(r'<think>.*?</think>\s*', '', text, flags=re.DOTALL)
elif '</think>' in text:
parts = text.split('</think>', 1)
reasoning_content = parts[0].strip()
text = parts[1].strip() if len(parts) > 1 else ''
tool_calls = []
for i, m in enumerate(re.findall(r'<tool_call>(.*?)</tool_call>', text, re.DOTALL)):
try:
call = json.loads(m.strip())
tool_calls.append({"id": f"call_{int(time.time())}_{i}", "type": "function", "function": {"name": call.get("name", ""), "arguments": json.dumps(call.get("arguments", {}), ensure_ascii=False)}})
except Exception:
pass
if tool_calls:
text = re.sub(r'<tool_call>.*?</tool_call>', '', text, flags=re.DOTALL)
return text.strip(), reasoning_content, tool_calls or None
def generate_stream_response(messages, temperature, top_p, max_tokens, tools=None, open_thinking=False):
try:
new_prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)[-max_tokens:]
new_prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=tools or None, open_thinking=open_thinking)[-max_tokens:]
inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)
queue = Queue()
@@ -91,20 +125,44 @@ def generate_stream_response(messages, temperature, top_p, max_tokens):
Thread(target=_generate).start()
full_text = ""
emitted = 0
thinking_ended = not bool(open_thinking)
while True:
text = queue.get()
if text is None:
yield json.dumps({
"choices": [{
"delta": {},
"finish_reason": "stop"
}]
}, ensure_ascii=False)
break
full_text += text
yield json.dumps({
"choices": [{"delta": {"content": text}}]
}, ensure_ascii=False)
if not thinking_ended:
pos = full_text.find('</think>')
if pos >= 0:
thinking_ended = True
new_r = full_text[emitted:pos]
if new_r:
yield json.dumps({"choices": [{"delta": {"reasoning_content": new_r}}]}, ensure_ascii=False)
emitted = pos + len('</think>')
after = full_text[emitted:].lstrip('\n')
emitted = len(full_text) - len(after)
if after:
yield json.dumps({"choices": [{"delta": {"content": after}}]}, ensure_ascii=False)
emitted = len(full_text)
else:
new_r = full_text[emitted:]
if new_r:
yield json.dumps({"choices": [{"delta": {"reasoning_content": new_r}}]}, ensure_ascii=False)
emitted = len(full_text)
else:
new_c = full_text[emitted:]
if new_c:
yield json.dumps({"choices": [{"delta": {"content": new_c}}]}, ensure_ascii=False)
emitted = len(full_text)
_, _, tool_calls = parse_response(full_text)
if tool_calls:
yield json.dumps({"choices": [{"delta": {"tool_calls": tool_calls}}]}, ensure_ascii=False)
yield json.dumps({"choices": [{"delta": {}, "finish_reason": "tool_calls" if tool_calls else "stop"}]}, ensure_ascii=False)
except Exception as e:
yield json.dumps({"error": str(e)})
@@ -119,7 +177,9 @@ async def chat_completions(request: ChatRequest):
messages=request.messages,
temperature=request.temperature,
top_p=request.top_p,
max_tokens=request.max_tokens
max_tokens=request.max_tokens,
tools=request.tools,
open_thinking=request.get_open_thinking()
)),
media_type="text/event-stream"
)
@@ -127,7 +187,9 @@ async def chat_completions(request: ChatRequest):
new_prompt = tokenizer.apply_chat_template(
request.messages,
tokenize=False,
add_generation_prompt=True
add_generation_prompt=True,
tools=request.tools or None,
open_thinking=request.get_open_thinking()
)[-request.max_tokens:]
inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)
with torch.no_grad():
@@ -142,6 +204,12 @@ async def chat_completions(request: ChatRequest):
temperature=request.temperature
)
answer = tokenizer.decode(generated_ids[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
content, reasoning_content, tool_calls = parse_response(answer)
message = {"role": "assistant", "content": content}
if reasoning_content:
message["reasoning_content"] = reasoning_content
if tool_calls:
message["tool_calls"] = tool_calls
return {
"id": f"chatcmpl-{int(time.time())}",
"object": "chat.completion",
@@ -150,8 +218,8 @@ async def chat_completions(request: ChatRequest):
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": answer},
"finish_reason": "stop"
"message": message,
"finish_reason": "tool_calls" if tool_calls else "stop"
}
]
}
@@ -165,8 +233,8 @@ if __name__ == "__main__":
parser.add_argument('--save_dir', default='out', type=str, help="模型权重目录")
parser.add_argument('--weight', default='full_sft', type=str, help="权重名称前缀(pretrain, full_sft, dpo, reason, ppo_actor, grpo, spo")
parser.add_argument('--lora_weight', default='None', type=str, help="LoRA权重名称(None表示不使用,可选:lora_identity, lora_medical")
parser.add_argument('--hidden_size', default=512, type=int, help="隐藏层维度512=Small-26M, 640=MoE-145M, 768=Base-104M")
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量Small/MoE=8, Base=16")
parser.add_argument('--hidden_size', default=768, type=int, help="隐藏层维度")
parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")
parser.add_argument('--max_seq_len', default=8192, type=int, help="最大序列长度")
parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")
parser.add_argument('--inference_rope_scaling', default=False, action='store_true', help="启用RoPE位置编码外推(4倍,仅解决位置编码问题)")
+234 -142
View File
@@ -1,10 +1,13 @@
import random
import re
import json
import os
from threading import Thread
import torch
import numpy as np
import streamlit as st
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
st.set_page_config(page_title="MiniMind", initial_sidebar_state="collapsed")
@@ -64,33 +67,130 @@ st.markdown("""
</style>
""", unsafe_allow_html=True)
system_prompt = []
device = "cuda" if torch.cuda.is_available() else "cpu"
# 多语言文本
LANG_TEXTS = {
'zh': {
'settings': '模型设定调整',
'history_rounds': '历史对话轮次',
'max_length': '最大生成长度',
'temperature': '温度',
'thinking': '思考',
'tools': '工具',
'language': '语言',
'send': '给 MiniMind 发送消息',
'disclaimer': 'AI 生成内容可能存在错误,请仔细核实',
'think_tip': '自适应思考,目前多轮对话或Tool Call共存时思考不稳定',
'tool_select': '工具选择(最多4个)',
},
'en': {
'settings': 'Model Settings',
'history_rounds': 'History Rounds',
'max_length': 'Max Length',
'temperature': 'Temperature',
'thinking': 'Thinking',
'tools': 'Tools',
'language': 'Language',
'send': 'Send a message to MiniMind',
'disclaimer': 'AI-generated content may be inaccurate, please verify',
'think_tip': 'Adaptive thinking; may be unstable with multi-turn or Tool Call',
'tool_select': 'Tool Selection (max 4)',
}
}
def process_assistant_content(content):
if model_source == "API" and 'R1' not in api_model_name:
return content
if model_source != "API" and 'R1' not in MODEL_PATHS[selected_model][1]:
return content
def get_text(key):
lang = st.session_state.get('lang', 'en')
return LANG_TEXTS.get(lang, {}).get(key, LANG_TEXTS['zh'].get(key, key))
# 工具定义
TOOLS = [
{"type": "function", "function": {"name": "calculate_math", "description": "计算数学表达式", "parameters": {"type": "object", "properties": {"expression": {"type": "string", "description": "数学表达式"}}, "required": ["expression"]}}},
{"type": "function", "function": {"name": "get_current_time", "description": "获取当前时间", "parameters": {"type": "object", "properties": {"timezone": {"type": "string", "default": "Asia/Shanghai"}}, "required": []}}},
{"type": "function", "function": {"name": "random_number", "description": "生成随机数", "parameters": {"type": "object", "properties": {"min": {"type": "integer"}, "max": {"type": "integer"}}, "required": ["min", "max"]}}},
{"type": "function", "function": {"name": "text_length", "description": "计算文本长度", "parameters": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}},
{"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"]}}},
{"type": "function", "function": {"name": "get_current_weather", "description": "获取天气", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}},
{"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"]}}},
{"type": "function", "function": {"name": "translate_text", "description": "翻译文本", "parameters": {"type": "object", "properties": {"text": {"type": "string"}, "target_lang": {"type": "string"}}, "required": ["text", "target_lang"]}}},
]
TOOL_SHORT_NAMES = {
'calculate_math': '数学', 'get_current_time': '时间', 'random_number': '随机',
'text_length': '字数', 'unit_converter': '单位', 'get_current_weather': '天气',
'get_exchange_rate': '汇率', 'translate_text': '翻译'
}
def execute_tool(tool_name, args):
import datetime
try:
if tool_name == 'calculate_math':
return {"result": eval(args.get('expression', '0'))}
elif tool_name == 'get_current_time':
tz = args.get('timezone', 'Asia/Shanghai')
return {"result": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
elif tool_name == 'random_number':
return {"result": random.randint(args.get('min', 0), args.get('max', 100))}
elif tool_name == 'text_length':
return {"result": len(args.get('text', ''))}
elif tool_name == 'unit_converter':
return {"result": f"{args.get('value', 0)} {args.get('from_unit', '')} = ? {args.get('to_unit', '')}"}
elif tool_name == 'get_current_weather':
return {"result": f"{args.get('city', 'Unknown')}: 晴, 7~10°C"}
elif tool_name == 'get_exchange_rate':
return {"result": f"1 {args.get('from_currency', 'USD')} = 7.2 {args.get('to_currency', 'CNY')}"}
elif tool_name == 'translate_text':
return {"result": f"[翻译结果]: hello world"}
return {"result": "Unknown tool"}
except Exception as e:
return {"error": str(e)}
def process_assistant_content(content, is_streaming=False):
# 处理tool_call标签,格式化显示
if '<tool_call>' in content:
def format_tool_call(match):
try:
tc = json.loads(match.group(1))
name = tc.get('name', 'unknown')
args = tc.get('arguments', {})
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>'
except:
return match.group(0)
content = re.sub(r'<tool_call>(.*?)</tool_call>', format_tool_call, content, flags=re.DOTALL)
# 流式生成且开启思考时,一开始就放到折叠里
if is_streaming and st.session_state.get('enable_thinking', False) and '</think>' not in content and '<think>' not in content:
m = re.search(r'(\n\n(?:我是|您好|你好)[^\n]*)', content)
if m and m.start(1) > 5:
i = m.start(1)
think_part = content[:i]
answer_part = content[i:]
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}'
elif len(content) > 5:
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>'
if '<think>' in content and '</think>' in content:
content = re.sub(r'(<think>)(.*?)(</think>)',
r'<details style="font-style: italic; background: rgba(222, 222, 222, 0.5); padding: 10px; border-radius: 10px;"><summary style="font-weight:bold;">推理内容(展开)</summary>\2</details>',
content,
flags=re.DOTALL)
def format_think(match):
think_content = match.group(2)
if think_content.replace('\n', '').strip(): # 不是全换行
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>'
return ''
content = re.sub(r'(<think>)(.*?)(</think>)', format_think, content, flags=re.DOTALL)
if '<think>' in content and '</think>' not in content:
content = re.sub(r'<think>(.*?)$',
r'<details open style="font-style: italic; background: rgba(222, 222, 222, 0.5); padding: 10px; border-radius: 10px;"><summary style="font-weight:bold;">推理中...</summary>\1</details>',
content,
flags=re.DOTALL)
def format_think_in_progress(match):
tc = match.group(1)
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>'
content = re.sub(r'<think>(.*?)$', format_think_in_progress, content, flags=re.DOTALL)
if '<think>' not in content and '</think>' in content:
content = re.sub(r'(.*?)</think>',
r'<details style="font-style: italic; background: rgba(222, 222, 222, 0.5); padding: 10px; border-radius: 10px;"><summary style="font-weight:bold;">推理内容(展开)</summary>\1</details>',
content,
flags=re.DOTALL)
def format_think_no_start(match):
think_content = match.group(1)
if think_content.replace('\n', '').strip():
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>'
return ''
content = re.sub(r'(.*?)</think>', format_think_no_start, content, flags=re.DOTALL)
return content
@@ -118,17 +218,10 @@ def init_chat_messages():
if "messages" in st.session_state:
for i, message in enumerate(st.session_state.messages):
if message["role"] == "assistant":
with st.chat_message("assistant", avatar=image_url):
st.markdown(process_assistant_content(message["content"]), unsafe_allow_html=True)
if st.button("🗑", key=f"delete_{i}"):
st.session_state.messages.pop(i)
st.session_state.messages.pop(i - 1)
st.session_state.chat_messages.pop(i)
st.session_state.chat_messages.pop(i - 1)
st.rerun()
st.markdown(process_assistant_content(message["content"]), unsafe_allow_html=True)
else:
st.markdown(
f'<div style="display: flex; justify-content: flex-end;"><div style="display: inline-block; margin: 10px 0; padding: 8px 12px 8px 12px; background-color: #ddd; border-radius: 10px; color: black;">{message["content"]}</div></div>',
f'<div style="display: flex; justify-content: flex-end;"><div style="display: inline-block; margin: 10px 0; padding: 8px 12px 8px 12px; background-color: #3d4450; border-radius: 22px; color: white;">{message["content"]}</div></div>',
unsafe_allow_html=True)
else:
@@ -143,52 +236,64 @@ def regenerate_answer(index):
st.rerun()
def delete_conversation(index):
st.session_state.messages.pop(index)
st.session_state.messages.pop(index - 1)
st.session_state.chat_messages.pop(index)
st.session_state.chat_messages.pop(index - 1)
# 动态扫描模型目录
script_dir = os.path.dirname(os.path.abspath(__file__))
MODEL_PATHS = {}
for d in sorted(os.listdir(script_dir), reverse=True):
full_path = os.path.join(script_dir, d)
if os.path.isdir(full_path) and not d.startswith('.') and not d.startswith('_'):
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))):
MODEL_PATHS[d] = [d, d]
if not MODEL_PATHS:
MODEL_PATHS = {"No models found": ["", "No models"]}
# 模型选择
selected_model = st.sidebar.selectbox('Model', list(MODEL_PATHS.keys()), index=0)
model_path = MODEL_PATHS[selected_model][0]
slogan = f"我是 {MODEL_PATHS[selected_model][1]},有什么可以帮你的?" if st.session_state.get('lang', 'en') == 'zh' else f"I am {MODEL_PATHS[selected_model][1]}, how can I help you?"
st.sidebar.markdown('<hr style="margin: 12px 0 16px 0;">', unsafe_allow_html=True)
# 语言选择
lang_options = {'中文': 'zh', 'English': 'en'}
current_lang = st.session_state.get('lang', 'en')
lang_index = 0 if current_lang == 'zh' else 1
lang_label = st.sidebar.radio('Language / 语言', list(lang_options.keys()), index=lang_index, horizontal=True)
if lang_options[lang_label] != current_lang:
st.session_state.lang = lang_options[lang_label]
st.rerun()
st.sidebar.markdown('<hr style="margin: 12px 0 16px 0;">', unsafe_allow_html=True)
st.sidebar.title("模型设定调整")
# 参数设置
st.session_state.history_chat_num = st.sidebar.slider(get_text('history_rounds'), 0, 8, 0, step=2)
st.session_state.max_new_tokens = st.sidebar.slider(get_text('max_length'), 256, 8192, 8192, step=1)
st.session_state.temperature = st.sidebar.slider(get_text('temperature'), 0.6, 1.2, 0.90, step=0.01)
# st.sidebar.text("训练数据偏差,增加上下文记忆时\n多轮对话(较单轮)容易出现能力衰减")
st.session_state.history_chat_num = st.sidebar.slider("Number of Historical Dialogues", 0, 6, 0, step=2)
# st.session_state.history_chat_num = 0
st.session_state.max_new_tokens = st.sidebar.slider("Max Sequence Length", 256, 8192, 8192, step=1)
st.session_state.temperature = st.sidebar.slider("Temperature", 0.6, 1.2, 0.85, step=0.01)
st.sidebar.markdown('<hr style="margin: 12px 0 16px 0;">', unsafe_allow_html=True)
model_source = st.sidebar.radio("选择模型来源", ["本地模型", "API"], index=0)
if model_source == "API":
api_url = st.sidebar.text_input("API URL", value="http://127.0.0.1:8000/v1")
api_model_id = st.sidebar.text_input("Model ID", value="minimind")
api_model_name = st.sidebar.text_input("Model Name", value="MiniMind2")
api_key = st.sidebar.text_input("API Key", value="none", type="password")
slogan = f"Hi, I'm {api_model_name}"
else:
MODEL_PATHS = {
"MiniMind2-R1 (0.1B)": ["../MiniMind2-R1", "MiniMind2-R1"],
"MiniMind2-Small-R1 (0.02B)": ["../MiniMind2-Small-R1", "MiniMind2-Small-R1"],
"MiniMind2 (0.1B)": ["../MiniMind2", "MiniMind2"],
"MiniMind2-MoE (0.15B)": ["../MiniMind2-MoE", "MiniMind2-MoE"],
"MiniMind2-Small (0.02B)": ["../MiniMind2-Small", "MiniMind2-Small"]
}
selected_model = st.sidebar.selectbox('Models', list(MODEL_PATHS.keys()), index=2) # 默认选择 MiniMind2
model_path = MODEL_PATHS[selected_model][0]
slogan = f"Hi, I'm {MODEL_PATHS[selected_model][1]}"
# 功能开关
st.session_state.enable_thinking = st.sidebar.checkbox(get_text('thinking'), value=False, help=get_text('think_tip'))
st.session_state.selected_tools = []
with st.sidebar.expander(get_text('tools')):
st.caption(get_text('tool_select'))
selected_count = sum(1 for tool in TOOLS if st.session_state.get(f"tool_{tool['function']['name']}", False))
for tool in TOOLS:
name = tool['function']['name']
short_name = TOOL_SHORT_NAMES.get(name, name)
checked = st.checkbox(short_name, key=f"tool_{name}", disabled=(selected_count >= 4 and not st.session_state.get(f"tool_{name}", False)))
if checked and len(st.session_state.selected_tools) < 4:
st.session_state.selected_tools.append(name)
image_url = "https://www.modelscope.cn/api/v1/studio/gongjy/MiniMind/repo?Revision=master&FilePath=images%2Flogo2.png&View=true"
st.markdown(
f'<div style="display: flex; flex-direction: column; align-items: center; text-align: center; margin: 0; padding: 0;">'
'<div style="font-style: italic; font-weight: 900; margin: 0; padding-top: 4px; display: flex; align-items: center; justify-content: center; flex-wrap: wrap; width: 100%;">'
f'<img src="{image_url}" style="width: 45px; height: 45px; "> '
f'<img src="{image_url}" style="width: 40px; height: 40px; "> '
f'<span style="font-size: 26px; margin-left: 10px;">{slogan}</span>'
'</div>'
'<span style="color: #bbb; font-style: italic; margin-top: 6px; margin-bottom: 10px;">内容完全由AI生成,请务必仔细甄别<br>Content AI-generated, please discern with care</span>'
f'<span style="color: #bbb; font-style: italic; margin-top: 6px; margin-bottom: 10px;">{get_text("disclaimer")}</span>'
'</div>',
unsafe_allow_html=True
)
@@ -205,10 +310,7 @@ def setup_seed(seed):
def main():
if model_source == "本地模型":
model, tokenizer = load_model_tokenizer(model_path)
else:
model, tokenizer = None, None
model, tokenizer = load_model_tokenizer(model_path)
if "messages" not in st.session_state:
st.session_state.messages = []
@@ -218,18 +320,13 @@ def main():
for i, message in enumerate(messages):
if message["role"] == "assistant":
with st.chat_message("assistant", avatar=image_url):
st.markdown(process_assistant_content(message["content"]), unsafe_allow_html=True)
if st.button("×", key=f"delete_{i}"):
st.session_state.messages = st.session_state.messages[:i - 1]
st.session_state.chat_messages = st.session_state.chat_messages[:i - 1]
st.rerun()
st.markdown(process_assistant_content(message["content"]), unsafe_allow_html=True)
else:
st.markdown(
f'<div style="display: flex; justify-content: flex-end;"><div style="display: inline-block; margin: 10px 0; padding: 8px 12px 8px 12px; background-color: gray; border-radius: 10px; color:white; ">{message["content"]}</div></div>',
f'<div style="display: flex; justify-content: flex-end;"><div style="display: inline-block; margin: 10px 0; padding: 8px 12px 8px 12px; background-color: #3d4450; border-radius: 22px; color: white;">{message["content"]}</div></div>',
unsafe_allow_html=True)
prompt = st.chat_input(key="input", placeholder="给 MiniMind 发送消息")
prompt = st.chat_input(key="input", placeholder=get_text('send'))
if hasattr(st.session_state, 'regenerate') and st.session_state.regenerate:
prompt = st.session_state.last_user_message
@@ -240,89 +337,84 @@ def main():
if prompt:
st.markdown(
f'<div style="display: flex; justify-content: flex-end;"><div style="display: inline-block; margin: 10px 0; padding: 8px 12px 8px 12px; background-color: gray; border-radius: 10px; color:white; ">{prompt}</div></div>',
f'<div style="display: flex; justify-content: flex-end;"><div style="display: inline-block; margin: 10px 0; padding: 8px 12px 8px 12px; background-color: #3d4450; border-radius: 22px; color: white;">{prompt}</div></div>',
unsafe_allow_html=True)
messages.append({"role": "user", "content": prompt[-st.session_state.max_new_tokens:]})
st.session_state.chat_messages.append({"role": "user", "content": prompt[-st.session_state.max_new_tokens:]})
with st.chat_message("assistant", avatar=image_url):
placeholder = st.empty()
placeholder = st.empty()
if model_source == "API":
try:
from openai import OpenAI
random_seed = random.randint(0, 2 ** 32 - 1)
setup_seed(random_seed)
client = OpenAI(
api_key=api_key,
base_url=api_url
)
history_num = st.session_state.history_chat_num + 1 # +1 是为了包含当前的用户消息
conversation_history = system_prompt + st.session_state.chat_messages[-history_num:]
answer = ""
response = client.chat.completions.create(
model=api_model_id,
messages=conversation_history,
stream=True,
temperature=st.session_state.temperature
)
tools = [t for t in TOOLS if t['function']['name'] in st.session_state.get('selected_tools', [])] or None
sys_prompt = [] if tools else [{"role": "system", "content": "你是MiniMind,一个乐于助人、知识渊博的AI助手。请用完整且友好的方式回答用户问题。"}]
st.session_state.chat_messages = sys_prompt + st.session_state.chat_messages[-(st.session_state.history_chat_num + 1):]
template_kwargs = {"tokenize": False, "add_generation_prompt": True}
if st.session_state.get('enable_thinking', False):
template_kwargs["open_thinking"] = True
if tools:
template_kwargs["tools"] = tools
new_prompt = tokenizer.apply_chat_template(st.session_state.chat_messages, **template_kwargs)
for chunk in response:
content = chunk.choices[0].delta.content or ""
answer += content
placeholder.markdown(process_assistant_content(answer), unsafe_allow_html=True)
inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)
except Exception as e:
answer = f"API调用出错: {str(e)}"
placeholder.markdown(answer, unsafe_allow_html=True)
else:
random_seed = random.randint(0, 2 ** 32 - 1)
setup_seed(random_seed)
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
generation_kwargs = {
"input_ids": inputs.input_ids,
"max_length": inputs.input_ids.shape[1] + st.session_state.max_new_tokens,
"num_return_sequences": 1,
"do_sample": True,
"attention_mask": inputs.attention_mask,
"pad_token_id": tokenizer.pad_token_id,
"eos_token_id": tokenizer.eos_token_id,
"temperature": st.session_state.temperature,
"top_p": 0.85,
"streamer": streamer,
}
st.session_state.chat_messages = system_prompt + st.session_state.chat_messages[
-(st.session_state.history_chat_num + 1):]
new_prompt = tokenizer.apply_chat_template(
st.session_state.chat_messages,
tokenize=False,
add_generation_prompt=True
)
Thread(target=model.generate, kwargs=generation_kwargs).start()
inputs = tokenizer(
new_prompt,
return_tensors="pt",
truncation=True
).to(device)
answer = ""
for new_text in streamer:
answer += new_text
placeholder.markdown(process_assistant_content(answer, is_streaming=True), unsafe_allow_html=True)
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
generation_kwargs = {
"input_ids": inputs.input_ids,
"max_length": inputs.input_ids.shape[1] + st.session_state.max_new_tokens,
"num_return_sequences": 1,
"do_sample": True,
"attention_mask": inputs.attention_mask,
"pad_token_id": tokenizer.pad_token_id,
"eos_token_id": tokenizer.eos_token_id,
"temperature": st.session_state.temperature,
"top_p": 0.85,
"streamer": streamer,
}
Thread(target=model.generate, kwargs=generation_kwargs).start()
answer = ""
for new_text in streamer:
answer += new_text
placeholder.markdown(process_assistant_content(answer), unsafe_allow_html=True)
messages.append({"role": "assistant", "content": answer})
full_answer = answer
for _ in range(16):
tool_calls = re.findall(r'<tool_call>(.*?)</tool_call>', answer, re.DOTALL)
if not tool_calls:
break
st.session_state.chat_messages.append({"role": "assistant", "content": answer})
with st.empty():
if st.button("×", key=f"delete_{len(messages) - 1}"):
st.session_state.messages = st.session_state.messages[:-2]
st.session_state.chat_messages = st.session_state.chat_messages[:-2]
st.rerun()
tool_results = []
for tc_str in tool_calls:
try:
tc = json.loads(tc_str.strip())
result = execute_tool(tc.get('name', ''), tc.get('arguments', {}))
st.session_state.chat_messages.append({"role": "tool", "content": json.dumps(result, ensure_ascii=False)})
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>')
except:
pass
full_answer += "\n" + "\n".join(tool_results) + "\n"
placeholder.markdown(process_assistant_content(full_answer, is_streaming=True), unsafe_allow_html=True)
new_prompt = tokenizer.apply_chat_template(st.session_state.chat_messages, **template_kwargs)
inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
generation_kwargs["input_ids"] = inputs.input_ids
generation_kwargs["attention_mask"] = inputs.attention_mask
generation_kwargs["max_length"] = inputs.input_ids.shape[1] + st.session_state.max_new_tokens
generation_kwargs["streamer"] = streamer
Thread(target=model.generate, kwargs=generation_kwargs).start()
answer = ""
for new_text in streamer:
answer += new_text
placeholder.markdown(process_assistant_content(full_answer + answer, is_streaming=True), unsafe_allow_html=True)
full_answer += answer
answer = full_answer
messages.append({"role": "assistant", "content": answer})
st.session_state.chat_messages.append({"role": "assistant", "content": answer})
if __name__ == "__main__":
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
main()