Spaces:
Paused
Paused
| import sys | |
| import os | |
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| import json | |
| import uuid | |
| import shutil | |
| import time | |
| import re | |
| from fastapi import FastAPI, UploadFile, File | |
| from fastapi.responses import HTMLResponse | |
| from pydantic import BaseModel | |
| from sqlalchemy import text | |
| from src.core.engine import ModelEngine | |
| from src.core.memory import MemoryManager | |
| from src.core.saas_api import SaasAPI | |
| from src.core.integrations import IntegrationManager | |
| from src.agents.manager import ManagerAgent | |
| from src.agents.coder import CoderAgent | |
| from src.agents.vision import VisionAgent | |
| try: | |
| if 'engine' not in globals(): engine = ModelEngine() | |
| except: engine = None | |
| memory = MemoryManager() | |
| saas = SaasAPI() | |
| integrations = IntegrationManager(memory) | |
| manager = ManagerAgent(engine, memory) | |
| coder = CoderAgent(engine, memory) | |
| vision = VisionAgent() | |
| app = FastAPI() | |
| class ChatRequest(BaseModel): | |
| user_id: int | |
| store_id: int | |
| message: str | |
| class FeedbackRequest(BaseModel): | |
| prompt: str | |
| response: str | |
| feedback: str | |
| correction: str = "" | |
| def clean_output(text): | |
| text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL) | |
| return text.replace("</think>", "").replace("<think>", "").strip() | |
| # --- HTML UI (Minified) --- | |
| html_content = """ | |
| <!DOCTYPE html> | |
| <html lang="vi"> | |
| <head> | |
| <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Project A</title><script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script><link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"><style>:root{--primary:#0084ff;--bg:#ffffff;--chat-bg:#f0f2f5;--user-msg:#0084ff;--ai-msg:#e4e6eb}body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:var(--bg);display:flex;justify-content:center;height:100vh;margin:0}.chat-widget{width:100%;max-width:700px;height:100vh;background:#fff;display:flex;flex-direction:column;box-shadow:0 0 20px rgba(0,0,0,0.1)}.header{padding:15px 20px;border-bottom:1px solid #eee;font-weight:700;font-size:18px;color:#333;display:flex;align-items:center;gap:10px}.status-dot{width:10px;height:10px;background:#31a24c;border-radius:50%}.messages{flex:1;padding:20px;overflow-y:auto;display:flex;flex-direction:column;gap:10px}.msg-row{display:flex;flex-direction:column;max-width:85%}.msg-row.user{align-self:flex-end;align-items:flex-end}.msg-row.ai{align-self:flex-start;align-items:flex-start}.msg{padding:10px 16px;border-radius:18px;font-size:15px;line-height:1.5}.msg.user{background:var(--user-msg);color:white;border-bottom-right-radius:4px}.msg.ai{background:var(--ai-msg);color:#050505;border-bottom-left-radius:4px}.attachment-area{display:flex;gap:10px;padding:10px 15px;background:#fff;overflow-x:auto;min-height:60px;display:none}.att-card{display:flex;align-items:center;gap:10px;background:#2b2d31;color:white;padding:8px 12px;border-radius:8px;font-size:13px;min-width:150px;box-shadow:0 2px 5px rgba(0,0,0,0.2)}.att-thumb{width:30px;height:30px;border-radius:4px;object-fit:cover;background:#444}.att-info{display:flex;flex-direction:column;flex:1;overflow:hidden}.att-name{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.att-size{font-size:10px;color:#aaa}.att-close{cursor:pointer;color:#aaa;padding:5px}.att-close:hover{color:white}.input-wrapper{padding:15px;border-top:1px solid #eee;background:#fff}.input-bar{display:flex;align-items:center;gap:10px;background:#f0f2f5;padding:8px 12px;border-radius:25px}.attach-btn{background:none;border:none;cursor:pointer;color:#65676b;font-size:20px}.attach-btn:hover{color:var(--primary)}input[type="text"]{flex:1;background:transparent;border:none;outline:none;font-size:15px}.send-btn{background:var(--primary);color:white;border:none;width:36px;height:36px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center}#typing{font-size:12px;color:#888;margin-left:20px;margin-bottom:5px;display:none}.feedback-actions{display:flex;gap:10px;margin-top:5px;margin-left:5px;font-size:12px;color:#65676b;opacity:0;transition:opacity 0.2s}.msg-row.ai:hover .feedback-actions{opacity:1}.fb-btn{cursor:pointer}.fb-btn:hover{color:var(--primary)}.correction-box{display:none;margin-top:5px;width:100%}.correction-box input{width:100%;padding:5px;border:1px solid #ddd;border-radius:5px;font-size:12px}</style> | |
| </head> | |
| <body> | |
| <div class="chat-widget"><div class="header"><div class="status-dot"></div> Project A</div><div class="messages" id="messages"></div><div id="typing">Project A đang suy nghĩ...</div><div id="attachment-area" class="attachment-area"></div><div class="input-wrapper"><div class="input-bar"><input type="file" id="fileInput" accept="image/*" style="display:none;" onchange="handleFileSelect()"><button class="attach-btn" onclick="document.getElementById('fileInput').click()"><i class="fa-solid fa-paperclip"></i></button><input type="text" id="input" placeholder="Nhập tin nhắn..." onkeypress="handleEnter(event)"><button class="send-btn" onclick="sendMessage()"><i class="fa-solid fa-paper-plane"></i></button></div></div></div> | |
| <script> | |
| const API_URL="";let currentFile=null;function handleFileSelect(){const e=document.getElementById("fileInput").files[0];e&&(currentFile=e,(new FileReader).onload=function(t){showAttachmentCard(e.name,t.target.result)},(new FileReader).readAsDataURL(e))}function showAttachmentCard(e,t){const n=document.getElementById("attachment-area");n.style.display="flex",n.innerHTML=`<div class="att-card"><img src="${t}" class="att-thumb"><div class="att-info"><div class="att-name">${e}</div><div class="att-size">Ready to upload</div></div><div class="att-close" onclick="clearFile()">✕</div></div>`}function clearFile(){currentFile=null,document.getElementById("fileInput").value="",document.getElementById("attachment-area").style.display="none",document.getElementById("attachment-area").innerHTML=""}async function sendMessage(){const e=document.getElementById("input"),t=e.value.trim();if(!t&&!currentFile)return;t&&addMessage(t,"user"),currentFile&&addMessage(`📎 Đã gửi ảnh: ${currentFile.name}`,"user"),e.value="",clearFile(),document.getElementById("typing").style.display="block";try{if(currentFile){const e=new FormData;e.append("file",currentFile),await fetch(`${API_URL}/upload`,{method:"POST",body:e}),clearFile()}const n=t||"Hãy mô tả ảnh tôi vừa gửi.",a=await fetch(`${API_URL}/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:1,store_id:1,message:n})}),s=await a.json();document.getElementById("typing").style.display="none",addAiMessage(s.response,n)}catch(e){document.getElementById("typing").style.display="none",addMessage("Lỗi: "+e.message,"ai")}}function addMessage(e,t){const n=document.createElement("div");n.className=`msg-row ${t}`;const a=document.createElement("div");a.className=`msg ${t}`,a.innerHTML=marked.parse(e),n.appendChild(a),document.getElementById("messages").appendChild(n),document.getElementById("messages").scrollTop=document.getElementById("messages").scrollHeight}function addAiMessage(e,t){const n=document.createElement("div");n.className="msg-row ai";const a=document.createElement("div");a.className="msg ai",a.innerHTML=marked.parse(e);const s=document.createElement("div");s.className="feedback-actions",s.innerHTML=`<span class="fb-btn" onclick="sendFeedback(this, 'up')">👍</span><span class="fb-btn" onclick="showCorrection(this)">👎</span>`;const i=document.createElement("div");i.className="correction-box",i.innerHTML=`<input placeholder="Góp ý sửa lỗi..." onkeypress="if(event.key==='Enter') submitCorrection('${t}', '${e.replace(/'/g,"\'").replace(/\n/g," ")}', this)">`,n.appendChild(a),n.appendChild(s),n.appendChild(i),document.getElementById("messages").appendChild(n),document.getElementById("messages").scrollTop=document.getElementById("messages").scrollHeight}function showCorrection(e){e.parentElement.nextElementSibling.style.display="block"}function sendFeedback(e,t){e.parentElement.innerHTML="<span style='color:green'>✓ Đã ghi nhận</span>"}async function submitCorrection(e,t,n){const a=n.value;n.parentElement.innerHTML="<span style='color:green; font-size:12px'>✓ Cảm ơn bạn!</span>",await fetch(`${API_URL}/feedback`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt:e,response:t,feedback:"down",correction:a})})}function handleEnter(e){"Enter"===e.key&&sendMessage()} | |
| </script></body></html> | |
| """ | |
| async def root(): | |
| return html_content | |
| async def upload_file(file: UploadFile = File(...)): | |
| file_ext = file.filename.split(".")[-1].lower() | |
| filename = f"{uuid.uuid4()}.{file_ext}" | |
| save_path = f"src/data/{filename}" | |
| os.makedirs("src/data", exist_ok=True) | |
| with open(save_path, "wb") as buffer: shutil.copyfileobj(file.file, buffer) | |
| analysis = f"File {file.filename}" | |
| if file_ext in ['jpg', 'png', 'jpeg', 'webp']: | |
| analysis = vision.analyze_media(save_path) | |
| memory.save_attachment(1, 1, file.filename, file_ext, analysis) | |
| return {"status": "success", "vision_analysis": analysis} | |
| async def save_feedback(req: FeedbackRequest): | |
| # --- SAVE TO NEON DB --- | |
| try: | |
| with memory.get_conn() as conn: | |
| conn.execute(text(""" | |
| INSERT INTO feedback_logs (user_id, prompt, ai_response, user_correction, feedback_type) | |
| VALUES (:uid, :p, :r, :c, :f) | |
| """), { | |
| "uid": 1, | |
| "p": req.prompt, | |
| "r": req.response, | |
| "c": req.correction, | |
| "f": req.feedback | |
| }) | |
| conn.commit() | |
| return {"status": "recorded_in_db"} | |
| except Exception as e: | |
| print(f"Feedback Error: {e}") | |
| return {"status": "error", "message": str(e)} | |
| async def chat_endpoint(req: ChatRequest): | |
| memory.add_message(req.user_id, req.store_id, "user", req.message) | |
| history = memory.get_context_string(req.user_id) | |
| decision = manager.analyze_task(req.message, history) | |
| cat = decision.get("category", "GENERAL") | |
| if "ảnh" in req.message.lower() or "hình" in req.message.lower(): | |
| cat = "GENERAL" | |
| resp = "" | |
| if cat == "TECHNICAL": | |
| plan = manager.plan(req.message, history) | |
| code = coder.write_code(req.message, plan) | |
| match = re.search(r"```json\n(.*?)\n```", code, re.DOTALL) | |
| if match: | |
| integrations.deploy_internal(req.store_id, match.group(1)) | |
| resp = f"Đã thiết kế quy trình:\n{code}" | |
| elif cat == "DATA_INTERNAL": | |
| data = saas.get_sales_report(req.store_id) | |
| resp = manager.consult(req.message, str(data), history) | |
| else: | |
| resp = manager.consult(req.message, "", history) | |
| final = clean_output(resp) | |
| memory.add_message(req.user_id, req.store_id, "assistant", final) | |
| return {"response": final} | |