UKielz commited on
Commit
0bbe8e9
·
verified ·
1 Parent(s): f8c2d60

Upload 14 files

Browse files
.env ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ QDRANT_URL=https://88e64c74-84e7-47a9-a40a-3a305d6dc703.us-west-2-0.aws.cloud.qdrant.io
2
+ GEMINI_API_KEY=AIzaSyBQ3_xDvD3cVDICrPT4qlVmGZlPHkKWrYA
3
+ QDRANT_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.n0rHOwXrpWXrhOw9DClu5VjucscB81JLQ40nIwkKuLY
Dockerfile CHANGED
@@ -1 +1,16 @@
1
- FROM ukielz/cocacola-chatbot:latest
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY . /app
6
+
7
+ RUN pip install --no-cache-dir -r requirements_product.txt
8
+
9
+ # Set env để Streamlit & HuggingFace không ghi vào /
10
+ ENV HF_HOME=/app/.cache
11
+ ENV TRANSFORMERS_CACHE=/app/.cache
12
+ ENV STREAMLIT_HOME=/app/.streamlit
13
+
14
+ EXPOSE 7860
15
+
16
+ CMD ["bash", "-c", "uvicorn server:app --host 0.0.0.0 --port 8000 & streamlit run app.py --server.port 7860 --server.address 0.0.0.0"]
app.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+
4
+ API_URL = "http://localhost:8000/chat" # FastAPI server của bạn
5
+
6
+ st.set_page_config(page_title="Coca-Cola Chatbot", page_icon="🥤", layout="wide")
7
+ st.title("🥤 Coca-Cola Vietnam Chatbot")
8
+
9
+ if "messages" not in st.session_state:
10
+ st.session_state["messages"] = []
11
+
12
+ # Hiển thị lịch sử chat
13
+ for msg in st.session_state["messages"]:
14
+ with st.chat_message(msg["role"]):
15
+ st.markdown(msg["content"])
16
+
17
+ # Input từ người dùng
18
+ if user_input := st.chat_input("Nhập tin nhắn..."):
19
+ # Hiển thị ngay trên UI
20
+ st.session_state["messages"].append({"role": "user", "content": user_input})
21
+ with st.chat_message("user"):
22
+ st.markdown(user_input)
23
+
24
+ # Gửi request đến FastAPI
25
+ try:
26
+ response = requests.post(API_URL, json={"message": user_input}, params={"session_id": "default"}, timeout=60)
27
+ if response.status_code == 200:
28
+ data = response.json()
29
+ bot_reply = data.get("response", "⚠️ Không có phản hồi")
30
+ else:
31
+ bot_reply = f"❌ Lỗi API: {response.status_code}"
32
+
33
+ except Exception as e:
34
+ bot_reply = f"⚠️ Không kết nối được API: {e}"
35
+
36
+ # Hiển thị phản hồi bot
37
+ st.session_state["messages"].append({"role": "assistant", "content": bot_reply})
38
+ with st.chat_message("assistant"):
39
+ st.markdown(bot_reply)
backend/__init__.py ADDED
File without changes
backend/config.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ COLLECTION_NAME = "cocacola_vietname_data"
8
+ EMBEDDING_MODEL_NAME = "AITeamVN/Vietnamese_Embedding"
9
+ MAX_LENGTH = 512
10
+ TOP_K = 5
11
+ MIN_SIMILARITY_SCORE = 0.5
12
+
13
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
14
+
15
+ QDRANT_URL = os.getenv("QDRANT_URL")
16
+ QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
17
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
backend/models.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any, List, Optional
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class ChatRequest(BaseModel):
6
+ message: str
7
+ feedback: Optional[str] = None
8
+
9
+
10
+ class NLUResult(BaseModel):
11
+ intent: str
12
+ confidence: float
13
+
14
+
15
+ class ActionResponse(BaseModel):
16
+ type: str
17
+ parameters: Optional[Dict[str, Any]] = {}
18
+
19
+
20
+ class ChatResponse(BaseModel):
21
+ response: str
22
+ context: str
23
+ nlu: NLUResult
24
+ action: Optional[ActionResponse] = None
25
+ images: Optional[List[str]] = None
backend/routes/chat.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Dict, List
3
+ from fastapi import APIRouter, HTTPException
4
+ from backend.models import ChatRequest, ChatResponse, NLUResult
5
+ from backend.services.embeddings import get_embeddings, combine_embeddings, process_chunk_data
6
+ from backend.services.qdrant import search_in_qdrant
7
+ from backend.services.utils import validate_image_base64
8
+ from backend.services.nlu import NLUPipeline
9
+ from backend.config import COLLECTION_NAME, GEMINI_API_KEY, QDRANT_API_KEY, QDRANT_URL, TOP_K
10
+ import google.generativeai as genai
11
+ from qdrant_client import QdrantClient
12
+
13
+
14
+ router = APIRouter(tags=["Chat"])
15
+
16
+ qdrant = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY, timeout=120.0)
17
+ genai.configure(api_key=GEMINI_API_KEY)
18
+
19
+ gemini = genai.GenerativeModel("gemini-2.5-flash")
20
+ nlu_pipeline = NLUPipeline(gemini)
21
+
22
+
23
+ ## Ghi nhớ hội dung cuộc hội thoại
24
+ conversation_history = {}
25
+
26
+
27
+ def get_conversation_history(session_id: str) -> List[Dict]:
28
+ return conversation_history.get(session_id, [])
29
+
30
+
31
+ def add_to_conversation_history(session_id: str, role: str, content: str):
32
+ if session_id not in conversation_history:
33
+ conversation_history[session_id] = []
34
+
35
+ conversation_history[session_id].append({"role": role, "content": content, "timestamp": os.time.time() if hasattr(os, "time") else 0})
36
+
37
+ if len(conversation_history[session_id]) > 10:
38
+ conversation_history[session_id] = conversation_history[session_id][-10:]
39
+
40
+
41
+ @router.post("/chat", response_model=ChatResponse, tags=["Chat"])
42
+ async def chat_endpoint(request: ChatRequest, session_id: str = "default"):
43
+ user_query = request.message
44
+
45
+ try:
46
+ history = get_conversation_history(session_id)
47
+
48
+ nlu_result = nlu_pipeline.analyze_user_input(user_query, history)
49
+
50
+ content_emb = get_embeddings([user_query])
51
+ query_vector = combine_embeddings(content_emb[0])
52
+
53
+ results = qdrant.search(collection_name=COLLECTION_NAME, query_vector=query_vector.tolist(), limit=TOP_K, with_payload=True)
54
+
55
+ context_parts = []
56
+ valid_images = []
57
+
58
+ for idx, r in enumerate(results):
59
+ payload = r.payload or {}
60
+ markdown_content, images = process_chunk_data(payload)
61
+
62
+ context_part = f"### Tài liệu tham khảo #{idx + 1}\n"
63
+ context_part += f"**Độ tương đồng:** {r.score:.2f}\n"
64
+ context_part += markdown_content
65
+
66
+ if images:
67
+ context_part += "\n\n**Ảnh liên quan:**\n"
68
+ for i, img_base64 in enumerate(images):
69
+ if validate_image_base64(img_base64):
70
+ img_ref = f"image_{idx}_{i}"
71
+ valid_images.append(img_base64)
72
+ context_part += f"![{img_ref}](attachment://{img_ref})\n"
73
+ else:
74
+ context_part += "⚠️ Ảnh không hợp lệ (bỏ qua)\n"
75
+
76
+ context_parts.append(context_part)
77
+
78
+ context = "\n---\n".join(context_parts)
79
+
80
+ text_response = nlu_pipeline.generate_response(user_query, context, nlu_result, history)
81
+
82
+ add_to_conversation_history(session_id, "user", user_query)
83
+ add_to_conversation_history(session_id, "assistant", text_response)
84
+
85
+ return {
86
+ "response": text_response,
87
+ "context": context,
88
+ "nlu": NLUResult(
89
+ intent=nlu_result["intent"],
90
+ confidence=nlu_result["confidence"],
91
+ ),
92
+ "images": valid_images if valid_images else None,
93
+ }
94
+
95
+ except Exception as e:
96
+ raise HTTPException(status_code=500, detail=f"Lỗi xử lý yêu cầu: {str(e)}")
backend/services/__init__.py ADDED
File without changes
backend/services/embeddings.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ from transformers import AutoTokenizer, AutoModel
4
+ from typing import Dict, Any, List, Optional, Tuple
5
+ from backend.config import EMBEDDING_MODEL_NAME, MAX_LENGTH, DEVICE
6
+
7
+ tokenizer = AutoTokenizer.from_pretrained(EMBEDDING_MODEL_NAME)
8
+ model = AutoModel.from_pretrained(EMBEDDING_MODEL_NAME).to(DEVICE).eval()
9
+
10
+
11
+ def mean_pooling(model_output, attention_mask):
12
+ token_embeddings = model_output[0]
13
+ mask = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
14
+ return torch.sum(token_embeddings * mask, 1) / torch.clamp(mask.sum(1), min=1e-9)
15
+
16
+
17
+ def get_embeddings(texts):
18
+ inputs = tokenizer(texts, padding=True, truncation=True, max_length=MAX_LENGTH, return_tensors="pt").to(DEVICE)
19
+ with torch.no_grad():
20
+ outputs = model(**inputs)
21
+ emb = mean_pooling(outputs, inputs["attention_mask"])
22
+ return torch.nn.functional.normalize(emb, p=2, dim=1).cpu().numpy()
23
+
24
+
25
+ def combine_embeddings(content_emb: np.ndarray) -> np.ndarray:
26
+ combined = content_emb
27
+ norm = np.linalg.norm(combined)
28
+ if norm > 1e-8:
29
+ return combined / norm
30
+ return combined
31
+
32
+
33
+ def process_chunk_data(payload: Dict[str, Any]) -> Tuple[str, List[str]]:
34
+ markdown_content = ""
35
+ images = []
36
+
37
+ if payload.get("source_file"):
38
+ markdown_content += f"\n\n**File gốc:** {payload['source_file']}\n\n"
39
+ if payload.get("markdown_data"):
40
+ markdown_content += payload["markdown_data"]
41
+
42
+ if payload.get("images"):
43
+ if isinstance(payload["images"], list):
44
+ for img_data in payload["images"]:
45
+ if isinstance(img_data, str):
46
+ images.append(img_data)
47
+ elif isinstance(img_data, dict) and img_data.get("data"):
48
+ images.append(img_data["data"])
49
+
50
+ return markdown_content, images
backend/services/nlu.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any, List, Optional, Tuple
2
+ import json
3
+ import re
4
+
5
+
6
+ class NLUPipeline:
7
+ def __init__(self, llm_model):
8
+ self.llm = llm_model
9
+
10
+ def analyze_user_input(self, text: str, conversation_history: Optional[List] = None) -> Dict[str, Any]:
11
+
12
+ history_context = ""
13
+ if conversation_history:
14
+ history_context = "Lịch sử hội thoại gần đây:\n"
15
+ for i, msg in enumerate(conversation_history[-3:]):
16
+ history_context += f"- {msg.get('role', 'user')}: {msg.get('content', '')}\n"
17
+ history_context += "\n"
18
+
19
+ nlu_prompt = f"""
20
+ Bạn là một chuyên gia phân tích ngôn ngữ tự nhiên cho hệ thống AI hỗ trợ bán hàng Coca-Cola Việt Nam.
21
+
22
+ {history_context}
23
+
24
+ Tin nhắn cần phân tích: "{text}"
25
+
26
+ Hãy phân tích và trả về kết quả theo định dạng JSON chính xác sau:
27
+
28
+ {{
29
+ "intent": "tên_intent",
30
+ "confidence": số_từ_0_đến_1,
31
+ "entities": {{
32
+ "product": "tên_sản_phẩm_nếu_có",
33
+ "quantity": "số_lượng_nếu_có",
34
+ "city": "tên_thành_phố_nếu_có",
35
+ "price_range": "khoảng_giá_nếu_có",
36
+ "time": "thời_gian_nếu_có",ml_model.pkl
37
+ "contact_info": "thông_tin_liên_lạc_nếu_có"
38
+ }},
39
+ }}
40
+
41
+ **Danh sách Intent có thể:**
42
+ - "order": Muốn đặt hàng, mua sản phẩm
43
+ - "check_inventory": Kiểm tra tồn kho, hàng còn không
44
+ - "product_info": Hỏi thông tin sản phẩm, giá cả, tính năng
45
+ - "promotion": Hỏi về khuyến mãi, ưu đãi, giảm giá
46
+ - "delivery": Hỏi về giao hàng, vận chuyển, thời gian
47
+ - "complaint": Phàn nàn, khiếu nại, không hài lòng
48
+ - "support": Cần hỗ trợ kỹ thuật, giải đáp thắc mắc
49
+ - "greeting": Chào hỏi, làm quen
50
+ - "goodbye": Chào tạm biệt, kết thúc cuộc trò chuyện
51
+ - "payment": Hỏi về thanh toán, phương thức trả tiền
52
+ - "cancel_order": Hủy đơn hàng
53
+ - "track_order": Theo dõi đơn hàng
54
+ - "feedback": Góp ý, đánh giá
55
+ - "other": Các ý định khác
56
+
57
+ **Sản phẩm Coca-Cola phổ biến:**
58
+ - Coca Cola, Coke (các loại: Classic, Zero, Light)
59
+ - Sprite, 7Up
60
+ - Fanta (cam, nho, dứa)
61
+ - Nutriboost (sữa có gas)
62
+ - Nước suối Dasani
63
+ - Schweppes
64
+ - Aquarius
65
+
66
+ **Thành phố chính:**
67
+ - Hà Nội, Thành phố Hồ Chí Minh, Đà Nẵng, Cần Thơ, Hải Phòng, Nha Trang, Huế, Vũng Tàu
68
+
69
+ CHỈ trả về JSON, không có text khác.
70
+ """
71
+ try:
72
+ result_text = self.llm.generate_content(nlu_prompt).text.strip()
73
+ json_match = re.search(r"\{[\s\S]*\}", result_text)
74
+ return json.loads(json_match.group())
75
+ except Exception as e:
76
+ print(f"Error in LLM NLU analysis: {e}")
77
+ return
78
+
79
+ def generate_response(self, user_query: str, context: str, nlu_result: Dict, conversation_history: Optional[List] = None) -> Tuple[str, Optional[Dict]]:
80
+ entities = nlu_result.get("entities", {})
81
+
82
+ adaptive_prompt = f"""
83
+ Bạn là AI Agent thông minh của Coca-Cola Việt Nam. Hãy trả lời một cách
84
+ "tone": "nhiệt tình và vui vẻ",
85
+ "style": "hỗ trợ năng động",
86
+ "guidelines":
87
+ - Thể hiện sự vui mừng khi được hỗ trợ
88
+ - Sử dụng emoji và ngôn ngữ tích cực
89
+ - Cung cấp thông tin chi tiết và hữu ích
90
+ - Gợi ý thêm sản phẩm/dịch vụ phù hợp
91
+
92
+ **Câu hỏi:** {user_query}
93
+
94
+ **Thông tin tham khảo:**
95
+ {context}
96
+
97
+ **Entities được nhận diện:**
98
+ {json.dumps(entities, ensure_ascii=False, indent=2)}
99
+
100
+
101
+ **Định dạng trả lời:**
102
+ 1. Câu trả lời chính (thích ứng với sentiment)
103
+ Trả lời:
104
+ """
105
+
106
+ try:
107
+ response = self.llm.generate_content(adaptive_prompt)
108
+ raw_output = response.text.strip()
109
+ return raw_output
110
+
111
+ except Exception as e:
112
+ print(f"Error in adaptive response generation: {e}")
113
+ return "Xin lỗi, tôi đang gặp sự cố kỹ thuật. Vui lòng thử lại sau.", None
backend/services/qdrant.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from qdrant_client import QdrantClient
2
+ from backend.config import QDRANT_URL, QDRANT_API_KEY, COLLECTION_NAME, TOP_K
3
+
4
+ qdrant = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY, timeout=120.0)
5
+
6
+
7
+ def search_in_qdrant(query_vector, limit=TOP_K):
8
+ return qdrant.search(collection_name=COLLECTION_NAME, query_vector=query_vector.tolist(), limit=limit, with_payload=True)
backend/services/utils.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ from io import BytesIO
3
+ from PIL import Image
4
+
5
+
6
+ def validate_image_base64(image_base64: str) -> bool:
7
+ try:
8
+ Image.open(BytesIO(base64.b64decode(image_base64)))
9
+ return True
10
+ except:
11
+ return False
requirements_product.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy
2
+ pillow
3
+ python-dotenv
4
+
5
+ # AI / NLP
6
+ torch
7
+ transformers
8
+ google-generativeai
9
+
10
+ # DB / Vector DB
11
+ qdrant-client
12
+
13
+ # Web / API
14
+ fastapi
15
+ uvicorn
16
+
17
+ streamlit
18
+ requests
server.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from backend.routes import chat
3
+
4
+ app = FastAPI(
5
+ title="Coca-Cola Vietnam AI Agent",
6
+ version="1.0.0",
7
+ )
8
+
9
+ # Đăng ký router
10
+ app.include_router(chat.router)
11
+
12
+ # if __name__ == "__main__":
13
+ # import uvicorn
14
+
15
+ # uvicorn.run("server:app", host="127.0.0.1", port=8000, reload=True)