Upload 14 files
Browse files- .env +3 -0
- Dockerfile +16 -1
- app.py +39 -0
- backend/__init__.py +0 -0
- backend/config.py +17 -0
- backend/models.py +25 -0
- backend/routes/chat.py +96 -0
- backend/services/__init__.py +0 -0
- backend/services/embeddings.py +50 -0
- backend/services/nlu.py +113 -0
- backend/services/qdrant.py +8 -0
- backend/services/utils.py +11 -0
- requirements_product.txt +18 -0
- server.py +15 -0
.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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"\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)
|