Spaces:
Sleeping
Sleeping
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 | |
| 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 --- | |
| def page_not_found(e): | |
| return render_template('index.html'), 200 # SPA fallback or error page | |
| def internal_server_error(e): | |
| return jsonify(error="Internal Server Error", message=str(e)), 500 | |
| def request_entity_too_large(e): | |
| return jsonify(error="File too large", message="File exceeds 16MB limit"), 413 | |
| # --- Routes --- | |
| def index(): | |
| return render_template('index.html') | |
| 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) | |
| 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'}) | |
| 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 --- | |
| 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) | |
| 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 --- | |
| 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 --- | |
| 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) | |