Trae Assistant
feat: complete project setup with chinese localization, datasets support, and mock mode
9990299
import os
import json
import sqlite3
import requests
import random
import time
from flask import Flask, render_template, request, jsonify, g, send_from_directory
from werkzeug.utils import secure_filename
from werkzeug.exceptions import HTTPException
app = Flask(__name__)
# Config
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB Limit
app.config['UPLOAD_FOLDER'] = os.path.join(app.instance_path, 'uploads')
DB_PATH = os.path.join(app.instance_path, 'material_mind.db')
# API Configuration (SiliconFlow)
SILICONFLOW_API_KEY = os.environ.get("SILICONFLOW_API_KEY", "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi")
SILICONFLOW_API_URL = "https://api.siliconflow.cn/v1/chat/completions"
# Ensure directories exist
try:
os.makedirs(app.instance_path, exist_ok=True)
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
except OSError:
pass
# Database Helpers
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(DB_PATH)
db.row_factory = sqlite3.Row
return db
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
def init_db():
with app.app_context():
db = get_db()
# Experiments Table
db.execute('''
CREATE TABLE IF NOT EXISTS experiments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
composition TEXT NOT NULL, -- JSON string
properties TEXT, -- JSON string
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Datasets Table (New)
db.execute('''
CREATE TABLE IF NOT EXISTS datasets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
filepath TEXT NOT NULL,
description TEXT,
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
db.commit()
# Initialize DB
init_db()
# --- Global Error Handlers ---
@app.errorhandler(404)
def page_not_found(e):
return render_template('index.html'), 200 # SPA fallback or error page
@app.errorhandler(500)
def internal_server_error(e):
return jsonify(error="Internal Server Error", message=str(e)), 500
@app.errorhandler(413)
def request_entity_too_large(e):
return jsonify(error="File too large", message="File exceeds 16MB limit"), 413
# --- Routes ---
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/experiments', methods=['GET'])
def get_experiments():
db = get_db()
cur = db.execute('SELECT * FROM experiments ORDER BY created_at DESC')
rows = cur.fetchall()
experiments = []
for row in rows:
experiments.append({
'id': row['id'],
'title': row['title'],
'composition': json.loads(row['composition']),
'properties': json.loads(row['properties']) if row['properties'] else {},
'notes': row['notes'],
'created_at': row['created_at']
})
return jsonify(experiments)
@app.route('/api/experiments', methods=['POST'])
def create_experiment():
data = request.json
title = data.get('title', 'Untitled Experiment')
composition = json.dumps(data.get('composition', {}))
properties = json.dumps(data.get('properties', {}))
notes = data.get('notes', '')
db = get_db()
cur = db.execute(
'INSERT INTO experiments (title, composition, properties, notes) VALUES (?, ?, ?, ?)',
(title, composition, properties, notes)
)
db.commit()
return jsonify({'id': cur.lastrowid, 'status': 'success'})
@app.route('/api/experiments/<int:experiment_id>', methods=['DELETE'])
def delete_experiment(experiment_id):
db = get_db()
db.execute('DELETE FROM experiments WHERE id = ?', (experiment_id,))
db.commit()
return jsonify({'status': 'success'})
# --- Dataset/File Upload Routes ---
@app.route('/api/datasets', methods=['GET'])
def get_datasets():
db = get_db()
cur = db.execute('SELECT * FROM datasets ORDER BY uploaded_at DESC')
rows = cur.fetchall()
datasets = []
for row in rows:
datasets.append({
'id': row['id'],
'filename': row['filename'],
'description': row['description'],
'uploaded_at': row['uploaded_at']
})
return jsonify(datasets)
@app.route('/api/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return jsonify({'error': 'No file part'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No selected file'}), 400
if file:
filename = secure_filename(file.filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
description = request.form.get('description', 'Uploaded Dataset')
db = get_db()
db.execute('INSERT INTO datasets (filename, filepath, description) VALUES (?, ?, ?)',
(filename, filepath, description))
db.commit()
return jsonify({'status': 'success', 'filename': filename})
# --- Simulation Logic ---
@app.route('/api/simulate', methods=['POST'])
def simulate_properties():
data = request.json
composition = data.get('composition', {})
# Base Properties
base_strength = 200
base_ductility = 50
base_cost = 10
# Contribution factors (Mock Database)
factors = {
'Fe': {'strength': 2.0, 'ductility': 1.0, 'cost': 1.0, 'corrosion': 1.0},
'C': {'strength': 12.0, 'ductility': -6.0, 'cost': 2.0, 'corrosion': -2.0},
'Ni': {'strength': 4.0, 'ductility': 3.0, 'cost': 15.0, 'corrosion': 8.0},
'Cr': {'strength': 5.0, 'ductility': 0.5, 'cost': 12.0, 'corrosion': 10.0},
'Ti': {'strength': 9.0, 'ductility': -2.0, 'cost': 25.0, 'corrosion': 6.0},
'Al': {'strength': 2.5, 'ductility': 0.0, 'cost': 5.0, 'corrosion': 4.0},
'Cu': {'strength': 1.5, 'ductility': 2.0, 'cost': 8.0, 'corrosion': 3.0},
'Mn': {'strength': 3.0, 'ductility': 1.5, 'cost': 3.0, 'corrosion': 1.0},
}
total_strength = base_strength
total_ductility = base_ductility
total_cost = base_cost
total_corrosion = 50 # Base score
# Normalize composition
total_percent = sum(float(v) for v in composition.values())
if total_percent == 0: total_percent = 1
for elem, amount in composition.items():
try:
amount = float(amount)
except ValueError:
amount = 0
f = factors.get(elem, {'strength': 1, 'ductility': 0, 'cost': 1, 'corrosion': 0})
# Contribution Model
total_strength += f['strength'] * amount * 0.6
total_ductility += f['ductility'] * amount * 0.4
total_cost += f['cost'] * amount * 0.1
total_corrosion += f['corrosion'] * amount * 0.5
# Apply some non-linear interactions (Mocking complex physics)
# E.g., Cr + Ni synergy for corrosion
cr = float(composition.get('Cr', 0))
ni = float(composition.get('Ni', 0))
if cr > 10 and ni > 5:
total_corrosion *= 1.2 # Synergy bonus
# Constraints
total_strength = max(50, round(total_strength, 1))
total_ductility = max(0.1, round(total_ductility, 1))
total_cost = max(1, round(total_cost, 1))
total_corrosion = min(100, max(0, round(total_corrosion, 1)))
return jsonify({
'tensile_strength': total_strength,
'ductility': total_ductility,
'cost_index': total_cost,
'corrosion_resistance': total_corrosion,
'melting_point': 1500 - (float(composition.get('C', 0)) * 50) + (float(composition.get('W', 0)) * 20) # Fake
})
# --- Chat & AI Logic ---
@app.route('/api/chat', methods=['POST'])
def chat():
data = request.json
user_message = data.get('message', '')
history = data.get('history', [])
# System Prompt with specific instruction to return JSON for charts if needed
system_prompt = {
"role": "system",
"content": (
"你是智材灵动(Material Mind)的AI助手,一位资深的材料科学家。"
"请用专业、严谨但易懂的中文回答。"
"如果你需要展示数据趋势或图表,请在回复的最后附加一个JSON代码块,"
"格式为: ```json:chart { \"type\": \"bar|line|pie\", \"data\": { ... }, \"title\": \"...\" } ```。"
"例如展示钢材强度对比:```json:chart { \"type\": \"bar\", \"title\": \"不同合金强度对比\", \"labels\": [\"合金A\", \"合金B\"], \"datasets\": [{ \"label\": \"强度(MPa)\", \"data\": [450, 600] }] } ```"
)
}
messages = [system_prompt] + history + [{"role": "user", "content": user_message}]
headers = {
"Authorization": f"Bearer {SILICONFLOW_API_KEY}",
"Content-Type": "application/json"
}
payload = {
"model": "Qwen/Qwen2.5-7B-Instruct",
"messages": messages,
"stream": False,
"max_tokens": 1024
}
try:
response = requests.post(SILICONFLOW_API_URL, json=payload, headers=headers, timeout=30)
response.raise_for_status()
result = response.json()
ai_content = result['choices'][0]['message']['content']
return jsonify({'response': ai_content})
except Exception as e:
print(f"API Error: {e}")
return mock_chat_response(user_message)
def mock_chat_response(message):
"""
Mock Fallback that returns rich content (Markdown + JSON Charts)
"""
time.sleep(1) # Simulate network delay
base_response = "**Mock Mode (云端连接中断)**: 正在使用本地应急知识库。\n\n"
if "强度" in message or "strength" in message:
return jsonify({'response': base_response +
"关于材料强度,我们通常关注屈服强度和抗拉强度。添加碳(C)通常能显著提高钢的强度,但会降低延展性。\n\n"
"以下是常见合金元素的强化效果对比:\n"
"```json:chart\n"
"{\n"
" \"type\": \"bar\",\n"
" \"title\": \"合金元素强化效果 (Mock Data)\",\n"
" \"labels\": [\"碳 (C)\", \"锰 (Mn)\", \"硅 (Si)\", \"铬 (Cr)\"],\n"
" \"datasets\": [{\n"
" \"label\": \"强化系数\",\n"
" \"data\": [12, 4, 3, 2],\n"
" \"backgroundColor\": [\"#ef4444\", \"#3b82f6\", \"#10b981\", \"#f59e0b\"]\n"
" }]\n"
"}\n"
"```"
})
elif "腐蚀" in message or "corrosion" in message:
return jsonify({'response': base_response +
"提高耐腐蚀性的关键是形成致密的氧化膜。铬(Cr)是实现这一点的关键元素(如不锈钢需含Cr > 10.5%)。\n\n"
"不锈钢耐腐蚀性随Cr含量变化趋势:\n"
"```json:chart\n"
"{\n"
" \"type\": \"line\",\n"
" \"title\": \"Cr含量与耐腐蚀性\",\n"
" \"labels\": [\"0%\", \"5%\", \"10%\", \"15%\", \"20%\"],\n"
" \"datasets\": [{\n"
" \"label\": \"耐腐蚀指数\",\n"
" \"data\": [10, 25, 80, 95, 98],\n"
" \"borderColor\": \"#3b82f6\",\n"
" \"fill\": true\n"
" }]\n"
"}\n"
"```"
})
else:
return jsonify({'response': base_response +
f"收到您的问题:“{message}”。\n"
"作为一个材料科学助手,我可以帮您设计配方、预测性能或分析实验数据。\n"
"尝试问我:“如何提高强度?”或者“不锈钢的配方是什么?”"
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=7860, debug=True)