sonthaiha commited on
Commit
a0b166b
·
verified ·
1 Parent(s): fbc4549

Final UI: Attachments, Vietnamese, Feedback

Browse files
Files changed (2) hide show
  1. src/core/config.py +4 -4
  2. src/server.py +197 -36
src/core/config.py CHANGED
@@ -7,21 +7,21 @@ class Config:
7
  self.PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8
  self.DATA_DIR = os.path.join(self.PROJECT_ROOT, 'src', 'data')
9
 
10
- # Cloud DB Connection
11
  raw_url = os.getenv("DATABASE_URL", "")
12
  if raw_url.startswith("postgres://"):
13
  self.DB_URL = raw_url.replace("postgres://", "postgresql+psycopg2://")
14
  elif raw_url.startswith("postgresql://"):
15
  self.DB_URL = raw_url.replace("postgresql://", "postgresql+psycopg2://")
16
  else:
17
- self.DB_URL = "sqlite:///:memory:" # Fallback if secret not set
18
 
19
  self.DOCS_DIR = os.path.join(self.DATA_DIR, 'docs')
20
  os.makedirs(self.DATA_DIR, exist_ok=True)
21
 
22
- self.SYSTEM_CONTEXT = "You are Project A, a Retail Automation Architect."
 
23
 
24
- # Model Config
25
  self.model_id = "sonthaiha/project-a-14b"
26
  self.models = { "manager": self.model_id, "coder": self.model_id, "researcher": self.model_id }
27
 
 
7
  self.PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8
  self.DATA_DIR = os.path.join(self.PROJECT_ROOT, 'src', 'data')
9
 
10
+ # Database
11
  raw_url = os.getenv("DATABASE_URL", "")
12
  if raw_url.startswith("postgres://"):
13
  self.DB_URL = raw_url.replace("postgres://", "postgresql+psycopg2://")
14
  elif raw_url.startswith("postgresql://"):
15
  self.DB_URL = raw_url.replace("postgresql://", "postgresql+psycopg2://")
16
  else:
17
+ self.DB_URL = "sqlite:///:memory:"
18
 
19
  self.DOCS_DIR = os.path.join(self.DATA_DIR, 'docs')
20
  os.makedirs(self.DATA_DIR, exist_ok=True)
21
 
22
+ # --- LANGUAGE ENFORCEMENT ---
23
+ self.SYSTEM_CONTEXT = "Bạn là Project A, trợ lý AI chuyên về tự động hóa bán lẻ. Luôn trả lời bằng Tiếng Việt. Giọng điệu chuyên nghiệp, thân thiện."
24
 
 
25
  self.model_id = "sonthaiha/project-a-14b"
26
  self.models = { "manager": self.model_id, "coder": self.model_id, "researcher": self.model_id }
27
 
src/server.py CHANGED
@@ -6,6 +6,7 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
6
  import json
7
  import uuid
8
  import shutil
 
9
  import re
10
  from fastapi import FastAPI, UploadFile, File
11
  from fastapi.responses import HTMLResponse
@@ -18,7 +19,6 @@ from src.agents.manager import ManagerAgent
18
  from src.agents.coder import CoderAgent
19
  from src.agents.vision import VisionAgent
20
 
21
- # Initialize Components
22
  try:
23
  if 'engine' not in globals(): engine = ModelEngine()
24
  except: engine = None
@@ -37,11 +37,18 @@ class ChatRequest(BaseModel):
37
  store_id: int
38
  message: str
39
 
 
 
 
 
 
 
40
  def clean_output(text):
41
  text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
42
  return text.replace("</think>", "").replace("<think>", "").strip()
43
 
44
- # --- ROOT ROUTE (THE FIX) ---
 
45
  @app.get("/", response_class=HTMLResponse)
46
  async def root():
47
  return """
@@ -50,50 +57,133 @@ async def root():
50
  <head>
51
  <meta charset="UTF-8">
52
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
53
- <title>Project A - Agentic Console</title>
54
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
55
  <style>
56
- :root { --primary: #0084ff; --bg: #f0f2f5; --chat-bg: #ffffff; --user-msg: #0084ff; --ai-msg: #f0f0f0; }
57
- body { font-family: sans-serif; background: var(--bg); display: flex; justify-content: center; height: 100vh; margin: 0; }
58
- .chat-widget { width: 100%; max-width: 600px; height: 100vh; background: var(--chat-bg); display: flex; flex-direction: column; }
59
- .header { background: white; padding: 15px; border-bottom: 1px solid #eee; font-weight: bold; display: flex; justify-content: space-between; }
60
- .messages { flex: 1; padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; }
61
- .msg { padding: 10px 15px; border-radius: 15px; max-width: 80%; line-height: 1.5; word-wrap: break-word; }
62
- .msg.user { background: var(--user-msg); color: white; align-self: flex-end; }
63
- .msg.ai { background: var(--ai-msg); color: black; align-self: flex-start; }
64
- .input-area { padding: 15px; border-top: 1px solid #eee; display: flex; gap: 10px; background: white; }
65
- input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 20px; outline: none; }
66
- button { padding: 10px 20px; background: var(--primary); color: white; border: none; border-radius: 20px; cursor: pointer; }
67
- #typing { display: none; margin-left: 20px; color: #888; font-size: 12px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  </style>
69
  </head>
70
  <body>
 
71
  <div class="chat-widget">
72
  <div class="header">
73
- Project A (Agentic Mode)
74
- <select id="user-select"><option value="1">User A</option></select>
75
  </div>
 
76
  <div class="messages" id="messages">
77
- <div class="msg ai">System Online. Connected to Cloud DB.<br>I can see your tables: sales, products, workflows.<br>Try: "Create a workflow to email vip customers."</div>
78
  </div>
79
- <div id="typing">Thinking...</div>
80
- <div class="input-area">
81
- <input type="text" id="input" placeholder="Type a command..." onkeypress="handleEnter(event)">
82
- <button onclick="sendMessage()">Send</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  </div>
84
  </div>
 
85
  <script>
86
- // Empty string means "Use current domain" (Relative path)
87
- // This makes it work automatically on Hugging Face Spaces
88
- const API_URL = "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  async function sendMessage() {
91
  const input = document.getElementById("input");
92
  const text = input.value.trim();
93
  if (!text) return;
94
-
95
  addMessage(text, "user");
96
  input.value = "";
 
 
97
  document.getElementById("typing").style.display = "block";
98
 
99
  try {
@@ -104,20 +194,80 @@ async def root():
104
  });
105
  const data = await res.json();
106
  document.getElementById("typing").style.display = "none";
107
- addMessage(data.response, "ai");
108
  } catch (e) {
109
  document.getElementById("typing").style.display = "none";
110
- addMessage("Error: " + e.message, "ai");
111
  }
112
  }
113
 
114
  function addMessage(text, role) {
115
- const div = document.createElement("div");
116
- div.className = `msg ${role}`;
117
- div.innerHTML = marked.parse(text);
118
- document.getElementById("messages").appendChild(div);
119
- document.getElementById("messages").scrollTop = document.getElementById("messages").scrollHeight;
 
 
 
 
 
120
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  function handleEnter(e) { if (e.key === "Enter") sendMessage(); }
122
  </script>
123
  </body>
@@ -133,11 +283,20 @@ async def upload_file(file: UploadFile = File(...)):
133
  with open(save_path, "wb") as buffer: shutil.copyfileobj(file.file, buffer)
134
 
135
  analysis = f"File {file.filename}"
136
- if file_ext in ['jpg', 'png']: analysis = vision.analyze_media(save_path)
 
137
 
 
138
  memory.save_attachment(1, 1, file.filename, file_ext, analysis)
139
  return {"status": "success", "vision_analysis": analysis}
140
 
 
 
 
 
 
 
 
141
  @app.post("/chat")
142
  async def chat_endpoint(req: ChatRequest):
143
  memory.add_message(req.user_id, req.store_id, "user", req.message)
@@ -146,7 +305,9 @@ async def chat_endpoint(req: ChatRequest):
146
  decision = manager.analyze_task(req.message, history)
147
  cat = decision.get("category", "GENERAL")
148
 
149
- if "ảnh" in req.message.lower(): cat = "GENERAL"
 
 
150
 
151
  resp = ""
152
  if cat == "TECHNICAL":
@@ -155,7 +316,7 @@ async def chat_endpoint(req: ChatRequest):
155
  match = re.search(r"```json\n(.*?)\n```", code, re.DOTALL)
156
  if match:
157
  integrations.deploy_internal(req.store_id, match.group(1))
158
- resp = f"Đã tạo quy trình:\n{code}"
159
  elif cat == "DATA_INTERNAL":
160
  data = saas.get_sales_report(req.store_id)
161
  resp = manager.consult(req.message, str(data), history)
 
6
  import json
7
  import uuid
8
  import shutil
9
+ import time
10
  import re
11
  from fastapi import FastAPI, UploadFile, File
12
  from fastapi.responses import HTMLResponse
 
19
  from src.agents.coder import CoderAgent
20
  from src.agents.vision import VisionAgent
21
 
 
22
  try:
23
  if 'engine' not in globals(): engine = ModelEngine()
24
  except: engine = None
 
37
  store_id: int
38
  message: str
39
 
40
+ class FeedbackRequest(BaseModel):
41
+ prompt: str
42
+ response: str
43
+ feedback: str
44
+ correction: str = ""
45
+
46
  def clean_output(text):
47
  text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
48
  return text.replace("</think>", "").replace("<think>", "").strip()
49
 
50
+ # --- ROUTES ---
51
+
52
  @app.get("/", response_class=HTMLResponse)
53
  async def root():
54
  return """
 
57
  <head>
58
  <meta charset="UTF-8">
59
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
60
+ <title>Project A</title>
61
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
62
  <style>
63
+ :root { --primary: #0084ff; --bg: #ffffff; --chat-bg: #f0f2f5; --user-msg: #0084ff; --ai-msg: #e4e6eb; --text-ai: #050505; }
64
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: var(--bg); display: flex; justify-content: center; height: 100vh; margin: 0; }
65
+
66
+ .chat-widget { width: 100%; max-width: 600px; height: 100vh; background: #fff; display: flex; flex-direction: column; box-shadow: 0 0 20px rgba(0,0,0,0.1); }
67
+
68
+ /* Header */
69
+ .header { padding: 15px 20px; border-bottom: 1px solid #eee; font-weight: 700; font-size: 18px; color: #333; display: flex; align-items: center; gap: 10px; }
70
+ .status-dot { width: 10px; height: 10px; background: #31a24c; border-radius: 50%; }
71
+
72
+ /* Messages */
73
+ .messages { flex: 1; padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; background: #fff; }
74
+ .msg-row { display: flex; flex-direction: column; max-width: 85%; }
75
+ .msg-row.user { align-self: flex-end; align-items: flex-end; }
76
+ .msg-row.ai { align-self: flex-start; align-items: flex-start; }
77
+
78
+ .msg { padding: 10px 16px; border-radius: 18px; font-size: 15px; line-height: 1.5; word-wrap: break-word; }
79
+ .msg.user { background: var(--user-msg); color: white; border-bottom-right-radius: 4px; }
80
+ .msg.ai { background: var(--ai-msg); color: var(--text-ai); border-bottom-left-radius: 4px; }
81
+ .msg p { margin: 0; }
82
+
83
+ /* Feedback */
84
+ .feedback-actions { display: flex; gap: 10px; margin-top: 5px; margin-left: 5px; font-size: 12px; color: #65676b; opacity: 0; transition: opacity 0.2s; }
85
+ .msg-row.ai:hover .feedback-actions { opacity: 1; }
86
+ .fb-btn { cursor: pointer; } .fb-btn:hover { color: var(--primary); }
87
+ .correction-box { display: none; margin-top: 5px; width: 100%; }
88
+ .correction-box input { width: 100%; padding: 5px; border: 1px solid #ddd; border-radius: 5px; font-size: 12px; }
89
+
90
+ /* Input Area */
91
+ .input-wrapper { padding: 15px; border-top: 1px solid #eee; background: #fff; }
92
+ .file-preview { display: none; padding: 8px 12px; background: #e7f3ff; border-radius: 8px; margin-bottom: 10px; font-size: 13px; color: #0084ff; align-items: center; justify-content: space-between; }
93
+ .remove-file { cursor: pointer; font-weight: bold; }
94
+
95
+ .input-bar { display: flex; align-items: center; gap: 10px; background: #f0f2f5; padding: 8px 12px; border-radius: 25px; }
96
+
97
+ .attach-btn { background: none; border: none; cursor: pointer; color: #65676b; padding: 5px; display: flex; align-items: center; }
98
+ .attach-btn:hover { color: var(--primary); }
99
+ .attach-btn svg { width: 24px; height: 24px; fill: currentColor; }
100
+
101
+ input[type="text"] { flex: 1; background: transparent; border: none; outline: none; font-size: 15px; }
102
+
103
+ .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; transition: transform 0.1s; }
104
+ .send-btn:active { transform: scale(0.95); }
105
+ .send-btn svg { width: 18px; height: 18px; fill: white; margin-left: 2px; }
106
+
107
+ #typing { font-size: 12px; color: #888; margin-left: 20px; margin-bottom: 5px; display: none; }
108
  </style>
109
  </head>
110
  <body>
111
+
112
  <div class="chat-widget">
113
  <div class="header">
114
+ <div class="status-dot"></div> Project A
 
115
  </div>
116
+
117
  <div class="messages" id="messages">
118
+ <!-- Chat starts empty -->
119
  </div>
120
+
121
+ <div id="typing">Project A đang soạn tin...</div>
122
+
123
+ <div class="input-wrapper">
124
+ <div id="file-preview" class="file-preview">
125
+ <span id="filename">image.png</span>
126
+ <span class="remove-file" onclick="clearFile()">✕</span>
127
+ </div>
128
+
129
+ <div class="input-bar">
130
+ <input type="file" id="fileInput" accept="image/*" style="display: none;" onchange="handleFileUpload()">
131
+ <button class="attach-btn" onclick="document.getElementById('fileInput').click()" title="Đính kèm ảnh">
132
+ <svg viewBox="0 0 24 24"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5a2.5 2.5 0 0 1 5 0v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5a2.5 2.5 0 0 0 5 0V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z"/></svg>
133
+ </button>
134
+
135
+ <input type="text" id="input" placeholder="Nhập tin nhắn..." onkeypress="handleEnter(event)">
136
+
137
+ <button class="send-btn" onclick="sendMessage()">
138
+ <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
139
+ </button>
140
+ </div>
141
  </div>
142
  </div>
143
+
144
  <script>
145
+ const API_URL = ""; // Relative path for HF Space
146
+
147
+ async function handleFileUpload() {
148
+ const file = document.getElementById('fileInput').files[0];
149
+ if (!file) return;
150
+
151
+ const preview = document.getElementById("file-preview");
152
+ const nameSpan = document.getElementById("filename");
153
+
154
+ preview.style.display = "flex";
155
+ nameSpan.innerText = "⏳ Đang tải: " + file.name;
156
+
157
+ const formData = new FormData();
158
+ formData.append("file", file);
159
+
160
+ try {
161
+ const res = await fetch(`${API_URL}/upload`, { method: "POST", body: formData });
162
+ const data = await res.json();
163
+ if (data.status === "success") {
164
+ nameSpan.innerText = "📎 " + file.name;
165
+ } else {
166
+ nameSpan.innerText = "❌ Lỗi tải file";
167
+ }
168
+ } catch (e) {
169
+ nameSpan.innerText = "❌ Lỗi kết nối";
170
+ }
171
+ }
172
+
173
+ function clearFile() {
174
+ document.getElementById("fileInput").value = "";
175
+ document.getElementById("file-preview").style.display = "none";
176
+ }
177
 
178
  async function sendMessage() {
179
  const input = document.getElementById("input");
180
  const text = input.value.trim();
181
  if (!text) return;
182
+
183
  addMessage(text, "user");
184
  input.value = "";
185
+ clearFile(); // Visual clear, server remembers context
186
+
187
  document.getElementById("typing").style.display = "block";
188
 
189
  try {
 
194
  });
195
  const data = await res.json();
196
  document.getElementById("typing").style.display = "none";
197
+ addAiMessage(data.response, text);
198
  } catch (e) {
199
  document.getElementById("typing").style.display = "none";
200
+ addMessage("Lỗi: " + e.message, "ai");
201
  }
202
  }
203
 
204
  function addMessage(text, role) {
205
+ const row = document.createElement("div");
206
+ row.className = `msg-row ${role}`;
207
+
208
+ const bubble = document.createElement("div");
209
+ bubble.className = `msg ${role}`;
210
+ bubble.innerHTML = marked.parse(text);
211
+
212
+ row.appendChild(bubble);
213
+ document.getElementById("messages").appendChild(row);
214
+ scrollToBottom();
215
  }
216
+
217
+ function addAiMessage(text, prompt) {
218
+ const row = document.createElement("div");
219
+ row.className = "msg-row ai";
220
+
221
+ const bubble = document.createElement("div");
222
+ bubble.className = "msg ai";
223
+ bubble.innerHTML = marked.parse(text);
224
+
225
+ const actions = document.createElement("div");
226
+ actions.className = "feedback-actions";
227
+ actions.innerHTML = `
228
+ <span class="fb-btn" onclick="sendFeedback(this, 'up')">👍</span>
229
+ <span class="fb-btn" onclick="showCorrection(this)">👎</span>
230
+ `;
231
+
232
+ const correction = document.createElement("div");
233
+ correction.className = "correction-box";
234
+ correction.innerHTML = `
235
+ <input placeholder="Góp ý sửa lỗi..." onkeypress="if(event.key==='Enter') submitCorrection('${prompt}', '${text.replace(/'/g, "\'")}', this)">
236
+ `;
237
+
238
+ row.appendChild(bubble);
239
+ row.appendChild(actions);
240
+ row.appendChild(correction);
241
+
242
+ document.getElementById("messages").appendChild(row);
243
+ scrollToBottom();
244
+ }
245
+
246
+ function showCorrection(btn) {
247
+ btn.parentElement.nextElementSibling.style.display = 'block';
248
+ }
249
+
250
+ function sendFeedback(btn, type) {
251
+ btn.parentElement.innerHTML = "<span style='color:green'>✓ Đã ghi nhận</span>";
252
+ // Call API if needed
253
+ }
254
+
255
+ async function submitCorrection(prompt, response, input) {
256
+ const feedback = input.value;
257
+ input.parentElement.innerHTML = "<span style='color:green; font-size:12px'>✓ Cảm ơn bạn!</span>";
258
+
259
+ await fetch(`${API_URL}/feedback`, {
260
+ method: "POST",
261
+ headers: { "Content-Type": "application/json" },
262
+ body: JSON.stringify({ prompt, response, feedback: "down", correction: feedback })
263
+ });
264
+ }
265
+
266
+ function scrollToBottom() {
267
+ const m = document.getElementById("messages");
268
+ m.scrollTop = m.scrollHeight;
269
+ }
270
+
271
  function handleEnter(e) { if (e.key === "Enter") sendMessage(); }
272
  </script>
273
  </body>
 
283
  with open(save_path, "wb") as buffer: shutil.copyfileobj(file.file, buffer)
284
 
285
  analysis = f"File {file.filename}"
286
+ if file_ext in ['jpg', 'png', 'jpeg', 'webp']:
287
+ analysis = vision.analyze_media(save_path)
288
 
289
+ # Save to User 1 (Default)
290
  memory.save_attachment(1, 1, file.filename, file_ext, analysis)
291
  return {"status": "success", "vision_analysis": analysis}
292
 
293
+ @app.post("/feedback")
294
+ async def save_feedback(req: FeedbackRequest):
295
+ entry = { "prompt": req.prompt, "chosen": req.correction, "rejected": req.response, "timestamp": time.time() }
296
+ with open("src/data/learning_queue.jsonl", "a", encoding="utf-8") as f:
297
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
298
+ return {"status": "recorded"}
299
+
300
  @app.post("/chat")
301
  async def chat_endpoint(req: ChatRequest):
302
  memory.add_message(req.user_id, req.store_id, "user", req.message)
 
305
  decision = manager.analyze_task(req.message, history)
306
  cat = decision.get("category", "GENERAL")
307
 
308
+ # Vision Override
309
+ if "ảnh" in req.message.lower() or "hình" in req.message.lower():
310
+ cat = "GENERAL"
311
 
312
  resp = ""
313
  if cat == "TECHNICAL":
 
316
  match = re.search(r"```json\n(.*?)\n```", code, re.DOTALL)
317
  if match:
318
  integrations.deploy_internal(req.store_id, match.group(1))
319
+ resp = f"Đã thiết kế quy trình:\n{code}"
320
  elif cat == "DATA_INTERNAL":
321
  data = saas.get_sales_report(req.store_id)
322
  resp = manager.consult(req.message, str(data), history)