mirror of
https://github.com/jingyaogong/minimind.git
synced 2026-09-12 06:29:55 +00:00
[update] minimind-3
This commit is contained in:
@@ -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
@@ -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')
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user