File size: 11,106 Bytes
2e91995
1804a7a
 
2e91995
 
 
 
 
a0b166b
1804a7a
2e91995
fbc4549
1804a7a
e0c7727
1804a7a
 
 
 
 
 
 
 
 
2e91995
 
1804a7a
 
 
 
 
 
 
 
2e91995
1804a7a
 
 
 
 
 
a0b166b
 
 
 
 
 
1804a7a
 
 
 
d3e87b1
e0c7727
fbc4549
 
 
e0c7727
fbc4549
 
e0c7727
fbc4549
e0c7727
 
fbc4549
 
e0c7727
 
 
 
2e91995
 
 
 
 
 
 
1804a7a
2e91995
a0b166b
 
1804a7a
2e91995
 
 
a0b166b
 
d3e87b1
e0c7727
 
 
 
 
 
d3e87b1
e0c7727
d3e87b1
e0c7727
 
 
 
 
 
 
 
a0b166b
2e91995
 
 
 
1804a7a
2e91995
 
1804a7a
a0b166b
 
2e91995
 
 
 
1804a7a
 
 
2e91995
a0b166b
2e91995
 
 
1804a7a
2e91995
1804a7a
2e91995
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133

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>
"""

@app.get("/", response_class=HTMLResponse)
async def root():
    return html_content

@app.post("/upload")
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}

@app.post("/feedback")
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)}

@app.post("/chat")
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}