├── .env.example ├── .gitignore ├── README.md ├── chatbot-backend ├── __init__.py ├── app │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ └── v1 │ │ │ ├── __init__.py │ │ │ └── chat │ │ │ ├── __init__.py │ │ │ └── routes.py │ ├── core │ │ ├── __init__.py │ │ └── ai │ │ │ ├── __init__.py │ │ │ ├── ai_service.py │ │ │ └── tools.py │ └── database │ │ ├── __init__.py │ │ ├── chat_history_service.py │ │ ├── models.py │ │ ├── order_service.py │ │ ├── product_service.py │ │ ├── seed.py │ │ └── wallet_service.py ├── main.py ├── requirements.txt └── setup.py ├── chatbot-frontend ├── .env.example ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.jsx │ ├── components │ └── chat │ │ ├── ChatBox.jsx │ │ └── ChatMessage.jsx │ ├── index.js │ ├── pages │ └── ChatPage.jsx │ └── services │ └── chatService.js └── docker-compose.yml /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=your_openai_api_key 2 | 3 | 4 | LANGCHAIN_TRACING_V2=true 5 | LANGCHAIN_ENDPOINT="https://api.smith.langchain.com" 6 | LANGCHAIN_API_KEY="your_langchain_api_key" 7 | LANGCHAIN_PROJECT="your_langchain_project_name" 8 | 9 | DB_HOST=your_db_host 10 | DB_PORT=your_db_port 11 | DB_USERNAME=your_db_username 12 | DB_PASSWORD=your_db_password 13 | DB_NAME=your_db_name 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | .env 3 | .env.local 4 | .env.*.local 5 | 6 | # Dependencies 7 | node_modules/ 8 | venv/ 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | *.so 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # IDE 31 | .idea/ 32 | .vscode/ 33 | *.swp 34 | *.swo 35 | .DS_Store 36 | .env.local 37 | .env.development.local 38 | .env.test.local 39 | .env.production.local 40 | 41 | # Logs 42 | npm-debug.log* 43 | yarn-debug.log* 44 | yarn-error.log* 45 | *.log 46 | 47 | # Testing 48 | coverage/ 49 | .coverage 50 | htmlcov/ 51 | 52 | # Production build 53 | /build 54 | /dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automation-Chatbot 2 | 3 | ## Setup 4 | 5 | 1. Run Frontend: 6 | 7 | - Step 1: Tạo file .env từ file .env.example: `cp .env.example .env` 8 | - Step 2: CD vào folder chatbot-frontend: `cd chatbot-frontend` 9 | - Step 3: Install dependencies: `npm install` 10 | - Step 4: Run frontend: `npm start` 11 | 12 | 2. Run Backend: 13 | 14 | - Step 1: Tạo file .env từ file .env.example: `cp .env.example .env` 15 | - Step 2: Run docker compose: `docker compose up --build` or `docker compose up -d` 16 | - Step 3: CD vào folder chatbot-backend: `cd chatbot-backend` 17 | - Step 4: Tạo môi trường với anaconda qua lệnh: `conda create -n myenv python=3.11` 18 | - Step 5: Activate môi trường: `conda activate myenv` 19 | - Step 6: Install dependencies: `pip install -r requirements.txt` 20 | - Step 7: Seed data vào database: `python app/database/seed.py` 21 | - Step 8: Run backend: `uvicorn main:app --reload --host 0.0.0.0 --port 8030` 22 | -------------------------------------------------------------------------------- /chatbot-backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaizenX209/Automation-Chatbot/74ef0ef2ec6f1ea2073dc9da28b598cd32882107/chatbot-backend/__init__.py -------------------------------------------------------------------------------- /chatbot-backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | from .core.ai.ai_service import get_answer, get_answer_stream 2 | 3 | __all__ = [ 4 | "get_answer", 5 | "get_answer_stream" 6 | ] 7 | -------------------------------------------------------------------------------- /chatbot-backend/app/api/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from .v1 import router as v1_router 3 | 4 | router = APIRouter() 5 | router.include_router(v1_router) -------------------------------------------------------------------------------- /chatbot-backend/app/api/v1/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from .chat.routes import router as chat_router 3 | 4 | router = APIRouter(prefix="/v1") 5 | router.include_router(chat_router, prefix="/chat", tags=["chat"]) -------------------------------------------------------------------------------- /chatbot-backend/app/api/v1/chat/__init__.py: -------------------------------------------------------------------------------- 1 | from .routes import router -------------------------------------------------------------------------------- /chatbot-backend/app/api/v1/chat/routes.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException 2 | from fastapi.responses import StreamingResponse 3 | from pydantic import BaseModel 4 | from app.core.ai.ai_service import get_answer, get_answer_stream 5 | import logging 6 | import json 7 | from typing import AsyncGenerator 8 | 9 | router = APIRouter() 10 | 11 | logging.basicConfig(level=logging.INFO) 12 | logger = logging.getLogger(__name__) 13 | 14 | class ChatRequest(BaseModel): 15 | question: str 16 | thread_id: str 17 | 18 | class ChatResponse(BaseModel): 19 | answer: str 20 | 21 | @router.post("/chat") 22 | async def chat(request: ChatRequest): 23 | try: 24 | logger.info(f"Received question: {request.question} for thread: {request.thread_id}") 25 | result = get_answer(request.question, request.thread_id) 26 | logger.info(f"Got result: {result}") 27 | 28 | if not isinstance(result, dict) or "output" not in result: 29 | raise ValueError("Invalid response format from get_answer") 30 | 31 | return ChatResponse(answer=result["output"]) 32 | except Exception as e: 33 | logger.error(f"Error in chat endpoint: {str(e)}", exc_info=True) 34 | raise HTTPException( 35 | status_code=500, 36 | detail=f"Internal server error: {str(e)}" 37 | ) 38 | 39 | async def event_generator(question: str, thread_id: str) -> AsyncGenerator[str, None]: 40 | try: 41 | async for chunk in get_answer_stream(question, thread_id): 42 | if chunk: # Only yield if there's content 43 | yield f"data: {json.dumps({'content': chunk})}\n\n" 44 | except Exception as e: 45 | logger.error(f"Error in stream: {str(e)}", exc_info=True) 46 | yield f"data: {json.dumps({'error': str(e)})}\n\n" 47 | 48 | @router.post("/chat/stream") 49 | async def chat_stream(request: ChatRequest): 50 | return StreamingResponse( 51 | event_generator(request.question, request.thread_id), 52 | media_type="text/event-stream" 53 | ) -------------------------------------------------------------------------------- /chatbot-backend/app/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .ai.ai_service import get_answer, get_answer_stream 2 | 3 | __all__ = [ 4 | "get_answer", 5 | "get_answer_stream" 6 | ] -------------------------------------------------------------------------------- /chatbot-backend/app/core/ai/__init__.py: -------------------------------------------------------------------------------- 1 | from .ai_service import get_answer, get_answer_stream 2 | 3 | __all__ = [ 4 | "get_answer", 5 | "get_answer_stream" 6 | ] -------------------------------------------------------------------------------- /chatbot-backend/app/core/ai/ai_service.py: -------------------------------------------------------------------------------- 1 | from langchain.tools import BaseTool 2 | from langchain_openai import ChatOpenAI 3 | from langchain.agents import AgentExecutor, create_openai_functions_agent 4 | from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder 5 | import os 6 | from typing import List, Dict, AsyncGenerator, Any 7 | from dotenv import load_dotenv 8 | from app.database.chat_history_service import save_chat_history, get_recent_chat_history, format_chat_history 9 | from pydantic import BaseModel, Field 10 | from langchain_core.messages import AIMessageChunk 11 | from langchain.callbacks.base import BaseCallbackHandler 12 | from .tools import ProductSearchTool, CreateOrderTool, UpdateOrderStatusTool 13 | 14 | load_dotenv() 15 | 16 | OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") 17 | if not OPENAI_API_KEY: 18 | raise ValueError("OPENAI_API_KEY not found in environment variables") 19 | 20 | # Create tools 21 | product_search_tool = ProductSearchTool() 22 | create_order_tool = CreateOrderTool() 23 | update_order_status_tool = UpdateOrderStatusTool() 24 | 25 | class CustomHandler(BaseCallbackHandler): 26 | """ 27 | Lớp xử lý callback tùy chỉnh để theo dõi và xử lý các sự kiện trong quá trình chat 28 | """ 29 | def __init__(self): 30 | super().__init__() 31 | 32 | def get_llm_and_agent() -> AgentExecutor: 33 | system_message = """You are a friendly and professional AI sales assistant. Your task is to help customers with their inquiries and purchases. 34 | 35 | For general questions or greetings: 36 | - Respond naturally without using any tools 37 | - Be friendly and professional 38 | - Keep responses concise and helpful 39 | 40 | For product-related questions or purchase intentions: 41 | 1. When customer asks about products: 42 | - Use product_search tool to find product information 43 | - Present product details in a clear format 44 | - If they show interest in buying, ask for quantity 45 | 46 | 2. When customer confirms purchase quantity: 47 | - Use product_search again to get latest information 48 | - From the search result, get: 49 | + product_id 50 | + price = result["price"] 51 | - Calculate total = price × quantity 52 | - Use create_order tool with: 53 | + user_id="user1" 54 | + product_id= 55 | + quantity= 56 | + total_amount= 57 | - Handle insufficient funds or out of stock cases 58 | - Confirm successful order and payment deduction 59 | 60 | 3. When customer confirms payment: 61 | - Use update_order_status tool to set order status to "paid" 62 | - Confirm successful payment to customer 63 | 64 | IMPORTANT RULES: 65 | - Only use product_search when questions are about products or purchases 66 | - NEVER use product_id without getting it from product_search result first 67 | - All product information (id, price, etc.) MUST come from latest product_search result 68 | - Format money amounts in VND format (e.g., 31,990,000 VND) 69 | 70 | Example flow: 71 | 1. Customer: "I want to buy Samsung S24" 72 | 2. Bot: 73 | - Call product_search("Samsung S24") 74 | - Result: {{"id": 2, "name": "Samsung S24", "price": 31990000, ...}} 75 | - Show product info and ask quantity 76 | 3. Customer: "I want 1" 77 | 4. Bot: 78 | - Call product_search("Samsung S24") again for latest info 79 | - From result: {{"id": 2, "price": 31990000}} 80 | - Call create_order with: 81 | user_id="user1" 82 | product_id=2 # From search result 83 | quantity=1 84 | total_amount=31990000 # price × quantity 85 | - Inform customer of the result""" 86 | 87 | chat = ChatOpenAI( 88 | temperature=0, 89 | streaming=True, 90 | model="gpt-4", 91 | api_key=OPENAI_API_KEY, 92 | callbacks=[CustomHandler()] 93 | ) 94 | 95 | tools = [ 96 | product_search_tool, 97 | create_order_tool, 98 | update_order_status_tool 99 | ] 100 | 101 | prompt = ChatPromptTemplate.from_messages([ 102 | ("system", system_message), 103 | MessagesPlaceholder(variable_name="chat_history"), 104 | ("human", "{input}"), 105 | MessagesPlaceholder(variable_name="agent_scratchpad"), 106 | ]) 107 | 108 | agent = create_openai_functions_agent( 109 | llm=chat, 110 | tools=tools, 111 | prompt=prompt 112 | ) 113 | 114 | agent_executor = AgentExecutor( 115 | agent=agent, 116 | tools=tools, 117 | verbose=False, 118 | return_intermediate_steps=True 119 | ) 120 | 121 | return agent_executor 122 | 123 | def get_answer(question: str, thread_id: str) -> Dict: 124 | """ 125 | Hàm lấy câu trả lời cho một câu hỏi 126 | 127 | Args: 128 | question (str): Câu hỏi của người dùng 129 | thread_id (str): ID của cuộc trò chuyện 130 | 131 | Returns: 132 | str: Câu trả lời từ AI 133 | """ 134 | agent = get_llm_and_agent() 135 | 136 | # Get recent chat history 137 | history = get_recent_chat_history(thread_id) 138 | chat_history = format_chat_history(history) 139 | 140 | result = agent.invoke({ 141 | "input": question, 142 | "chat_history": chat_history 143 | }) 144 | 145 | # Save chat history to database 146 | if isinstance(result, dict) and "output" in result: 147 | save_chat_history(thread_id, question, result["output"]) 148 | 149 | return result 150 | 151 | async def get_answer_stream(question: str, thread_id: str) -> AsyncGenerator[Dict, None]: 152 | """ 153 | Hàm lấy câu trả lời dạng stream cho một câu hỏi 154 | 155 | Quy trình xử lý: 156 | 1. Khởi tạo agent với các tools cần thiết 157 | 2. Lấy lịch sử chat gần đây 158 | 3. Gọi agent để xử lý câu hỏi 159 | 4. Stream từng phần của câu trả lời về client 160 | 5. Lưu câu trả lời hoàn chỉnh vào database 161 | 162 | Args: 163 | question (str): Câu hỏi của người dùng 164 | thread_id (str): ID phiên chat 165 | 166 | Returns: 167 | AsyncGenerator[str, None]: Generator trả về từng phần của câu trả lời 168 | """ 169 | # Khởi tạo agent với các tools cần thiết 170 | agent = get_llm_and_agent() 171 | 172 | # Lấy lịch sử chat gần đây 173 | history = get_recent_chat_history(thread_id) 174 | chat_history = format_chat_history(history) 175 | 176 | # Biến lưu câu trả lời hoàn chỉnh 177 | final_answer = "" 178 | 179 | # Stream từng phần của câu trả lời 180 | async for event in agent.astream_events( 181 | { 182 | "input": question, 183 | "chat_history": chat_history, 184 | }, 185 | version="v2" 186 | ): 187 | # Lấy loại sự kiện 188 | kind = event["event"] 189 | # Nếu là sự kiện stream từ model 190 | if kind == "on_chat_model_stream": 191 | # Lấy nội dung token 192 | content = event['data']['chunk'].content 193 | if content: # Chỉ yield nếu có nội dung 194 | # Cộng dồn vào câu trả lời hoàn chỉnh 195 | final_answer += content 196 | # Trả về token cho client 197 | yield content 198 | 199 | # Lưu câu trả lời hoàn chỉnh vào database 200 | if final_answer: 201 | save_chat_history(thread_id, question, final_answer) 202 | 203 | if __name__ == "__main__": 204 | import asyncio 205 | 206 | async def test(): 207 | # answer = get_answer_stream("hi", "test-session") 208 | # print(answer) 209 | async for event in get_answer_stream("hi", "test-session"): 210 | print('event:', event) 211 | print('done') 212 | 213 | 214 | asyncio.run(test()) 215 | 216 | 217 | -------------------------------------------------------------------------------- /chatbot-backend/app/core/ai/tools.py: -------------------------------------------------------------------------------- 1 | from langchain.tools import BaseTool 2 | from pydantic import BaseModel, Field 3 | from typing import Optional, Dict, Annotated 4 | from app.database.product_service import get_product_by_name, update_product_stock, check_product_stock 5 | from app.database.order_service import create_order, update_order_status 6 | from app.database.wallet_service import get_wallet, update_balance 7 | from decimal import Decimal 8 | 9 | class ProductSearchInput(BaseModel): 10 | product_name: str = Field(..., description="The name of the product to search for") 11 | 12 | class ProductSearchTool(BaseTool): 13 | name: Annotated[str, Field(description="Tool name")] = "product_search" 14 | description: Annotated[str, Field(description="Tool description")] = "Search for product information by name" 15 | args_schema: type[BaseModel] = ProductSearchInput 16 | 17 | def _run(self, product_name: str) -> Optional[Dict]: 18 | return get_product_by_name(product_name) 19 | 20 | class CreateOrderInput(BaseModel): 21 | user_id: str = Field(..., description="The ID of the user placing the order") 22 | product_id: int = Field(..., description="The ID of the product being ordered") 23 | quantity: int = Field(..., description="The quantity of the product being ordered") 24 | total_amount: float = Field(..., description="The total amount of the order") 25 | 26 | class CreateOrderTool(BaseTool): 27 | name: Annotated[str, Field(description="Tool name")] = "create_order" 28 | description: Annotated[str, Field(description="Tool description")] = "Create a new order for a product" 29 | args_schema: type[BaseModel] = CreateOrderInput 30 | 31 | def _run(self, user_id: str, product_id: int, quantity: int, total_amount: float) -> Optional[Dict]: 32 | # Check if product has enough stock 33 | if not check_product_stock(product_id, quantity): 34 | print('product_id: ', product_id) 35 | print('quantity: ', quantity) 36 | return { 37 | "error": "Insufficient stock", 38 | "message": "Product is out of stock" 39 | } 40 | 41 | # Check wallet balance 42 | wallet = get_wallet(user_id) 43 | if not wallet: 44 | return { 45 | "error": "Wallet not found", 46 | "message": "Wallet not found" 47 | } 48 | 49 | if wallet['balance'] < Decimal(str(total_amount)): 50 | return { 51 | "error": "Insufficient balance", 52 | "message": f"Insufficient balance. Current balance: {wallet['balance']:,.0f} VND", 53 | "balance": wallet['balance'] 54 | } 55 | 56 | # Deduct money from wallet 57 | updated_wallet = update_balance(user_id, Decimal(str(-total_amount))) 58 | if not updated_wallet: 59 | return { 60 | "error": "Payment failed", 61 | "message": "Cannot process payment" 62 | } 63 | 64 | # Update product stock 65 | if not update_product_stock(product_id, quantity): 66 | # Refund money if stock update fails 67 | update_balance(user_id, Decimal(str(total_amount))) 68 | return { 69 | "error": "Stock update failed", 70 | "message": "Cannot update stock" 71 | } 72 | 73 | # Create order 74 | order = create_order( 75 | user_id=user_id, 76 | product_id=product_id, 77 | quantity=quantity, 78 | total_amount=Decimal(str(total_amount)) 79 | ) 80 | 81 | if order: 82 | return { 83 | "success": True, 84 | "order": order, 85 | "message": f"Order created and payment successful. Remaining balance: {updated_wallet['balance']:,.0f} VND" 86 | } 87 | 88 | # If order creation fails, refund money and revert stock 89 | update_balance(user_id, Decimal(str(total_amount))) 90 | update_product_stock(product_id, -quantity) # Add back to stock 91 | return { 92 | "error": "Order creation failed", 93 | "message": "Cannot create order" 94 | } 95 | 96 | class UpdateOrderStatusInput(BaseModel): 97 | order_id: int = Field(..., description="The ID of the order to update") 98 | status: str = Field(..., description="The new status of the order") 99 | 100 | class UpdateOrderStatusTool(BaseTool): 101 | name: Annotated[str, Field(description="Tool name")] = "update_order_status" 102 | description: Annotated[str, Field(description="Tool description")] = "Update the status of an order" 103 | args_schema: type[BaseModel] = UpdateOrderStatusInput 104 | 105 | def _run(self, order_id: int, status: str) -> bool: 106 | return update_order_status(order_id, status) -------------------------------------------------------------------------------- /chatbot-backend/app/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaizenX209/Automation-Chatbot/74ef0ef2ec6f1ea2073dc9da28b598cd32882107/chatbot-backend/app/database/__init__.py -------------------------------------------------------------------------------- /chatbot-backend/app/database/chat_history_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | import psycopg 4 | from psycopg.rows import dict_row 5 | from datetime import datetime 6 | from typing import List, Dict 7 | 8 | load_dotenv() 9 | 10 | DB_NAME = os.getenv('DB_NAME') 11 | DB_USER = os.getenv('DB_USERNAME') 12 | DB_PASSWORD = os.getenv('DB_PASSWORD') 13 | DB_HOST = os.getenv('DB_HOST') 14 | DB_PORT = os.getenv('DB_PORT') 15 | 16 | def get_db_connection(): 17 | """ 18 | Tạo kết nối đến cơ sở dữ liệu PostgreSQL 19 | 20 | Returns: 21 | Connection: Đối tượng kết nối đến database 22 | """ 23 | return psycopg.connect( 24 | dbname=DB_NAME, 25 | user=DB_USER, 26 | password=DB_PASSWORD, 27 | host=DB_HOST, 28 | port=DB_PORT, 29 | row_factory=dict_row 30 | ) 31 | 32 | def init_chat_history_table(): 33 | """ 34 | Khởi tạo bảng message trong database nếu chưa tồn tại 35 | Bảng này lưu trữ lịch sử chat bao gồm: 36 | - ID tin nhắn (UUID) 37 | - ID cuộc trò chuyện 38 | - Câu hỏi 39 | - Câu trả lời 40 | - Thời gian tạo 41 | """ 42 | with get_db_connection() as conn: 43 | with conn.cursor() as cur: 44 | # Enable UUID extension if not exists 45 | cur.execute("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"") 46 | 47 | # Create table if not exists 48 | cur.execute(""" 49 | CREATE TABLE IF NOT EXISTS message ( 50 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 51 | thread_id VARCHAR(255) NOT NULL, 52 | question TEXT NOT NULL, 53 | answer TEXT NOT NULL, 54 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 55 | ) 56 | """) 57 | 58 | # Create index if not exists 59 | cur.execute(""" 60 | CREATE INDEX IF NOT EXISTS idx_message_thread_id 61 | ON message(thread_id) 62 | """) 63 | conn.commit() 64 | 65 | def save_chat_history(thread_id: str, question: str, answer: str) -> Dict: 66 | """ 67 | Lưu lịch sử chat vào database 68 | 69 | Args: 70 | thread_id (str): ID của cuộc trò chuyện 71 | question (str): Câu hỏi của người dùng 72 | answer (str): Câu trả lời của chatbot 73 | 74 | Returns: 75 | Dict: Thông tin lịch sử chat vừa được lưu 76 | """ 77 | with get_db_connection() as conn: 78 | with conn.cursor() as cur: 79 | cur.execute( 80 | "INSERT INTO message (thread_id, question, answer) VALUES (%s, %s, %s) RETURNING id::text", 81 | (thread_id, question, answer) 82 | ) 83 | result = cur.fetchone() 84 | conn.commit() 85 | return result['id'] 86 | 87 | def get_recent_chat_history(thread_id: str, limit: int = 10) -> List[Dict]: 88 | """ 89 | Lấy lịch sử chat gần đây của một cuộc trò chuyện 90 | 91 | Args: 92 | thread_id (str): ID của cuộc trò chuyện 93 | limit (int): Số lượng tin nhắn tối đa cần lấy, mặc định là 10 94 | 95 | Returns: 96 | List[Dict]: Danh sách các tin nhắn gần đây 97 | """ 98 | with get_db_connection() as conn: 99 | with conn.cursor() as cur: 100 | cur.execute( 101 | """ 102 | SELECT 103 | id::text, 104 | thread_id, 105 | question, 106 | answer, 107 | created_at 108 | FROM message 109 | WHERE thread_id = %s 110 | ORDER BY created_at DESC 111 | LIMIT %s 112 | """, 113 | (thread_id, limit) 114 | ) 115 | return cur.fetchall() 116 | 117 | def format_chat_history(chat_history: List[Dict]) -> str: 118 | """ 119 | Định dạng lịch sử chat thành chuỗi văn bản 120 | 121 | Args: 122 | chat_history (List[Dict]): Danh sách các tin nhắn 123 | 124 | Returns: 125 | str: Chuỗi văn bản đã được định dạng 126 | """ 127 | formatted_history = [] 128 | for msg in reversed(chat_history): # Reverse to get chronological order 129 | formatted_history.extend([ 130 | {"role": "human", "content": msg["question"]}, 131 | {"role": "assistant", "content": msg["answer"]} 132 | ]) 133 | return formatted_history 134 | 135 | # Initialize table when module is imported 136 | init_chat_history_table() -------------------------------------------------------------------------------- /chatbot-backend/app/database/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pydantic import BaseModel 3 | from typing import Optional, List 4 | from decimal import Decimal 5 | 6 | class ChatHistory(BaseModel): 7 | id: int 8 | thread_id: str 9 | question: str 10 | answer: str 11 | created_at: datetime = datetime.now() 12 | 13 | class Product(BaseModel): 14 | id: int 15 | name: str 16 | description: str 17 | price: Decimal 18 | stock: int 19 | specifications: dict 20 | created_at: datetime = datetime.now() 21 | updated_at: datetime = datetime.now() 22 | 23 | class Order(BaseModel): 24 | id: int 25 | user_id: str 26 | product_id: int 27 | quantity: int 28 | total_amount: Decimal 29 | status: str # pending, confirmed, paid, cancelled 30 | created_at: datetime = datetime.now() 31 | updated_at: datetime = datetime.now() 32 | 33 | class UserWallet(BaseModel): 34 | id: int 35 | user_id: str 36 | balance: Decimal 37 | created_at: datetime = datetime.now() 38 | updated_at: datetime = datetime.now() -------------------------------------------------------------------------------- /chatbot-backend/app/database/order_service.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Optional 2 | from .chat_history_service import get_db_connection 3 | from decimal import Decimal 4 | 5 | def init_order_table(): 6 | """ 7 | Khởi tạo bảng order trong database nếu chưa tồn tại 8 | Bảng này lưu trữ thông tin về các đơn hàng bao gồm: 9 | - ID người dùng 10 | - ID sản phẩm 11 | - Số lượng 12 | - Tổng tiền 13 | - Trạng thái đơn hàng 14 | """ 15 | with get_db_connection() as conn: 16 | with conn.cursor() as cur: 17 | cur.execute(""" 18 | CREATE TABLE IF NOT EXISTS "order" ( 19 | id SERIAL PRIMARY KEY, 20 | user_id VARCHAR(255) NOT NULL, 21 | product_id INTEGER NOT NULL REFERENCES product(id), 22 | quantity INTEGER NOT NULL, 23 | total_amount DECIMAL(10,2) NOT NULL, 24 | status VARCHAR(50) NOT NULL DEFAULT 'pending', 25 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 26 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 27 | ) 28 | """) 29 | conn.commit() 30 | 31 | def create_order(user_id: str, product_id: int, quantity: int, total_amount: Decimal) -> Optional[Dict]: 32 | """ 33 | Tạo đơn hàng mới 34 | 35 | Args: 36 | user_id (str): ID của người dùng 37 | product_id (int): ID của sản phẩm 38 | quantity (int): Số lượng sản phẩm 39 | total_amount (Decimal): Tổng tiền đơn hàng 40 | 41 | Returns: 42 | Optional[Dict]: Thông tin đơn hàng nếu tạo thành công, None nếu thất bại 43 | """ 44 | with get_db_connection() as conn: 45 | with conn.cursor() as cur: 46 | cur.execute( 47 | """ 48 | INSERT INTO "order" (user_id, product_id, quantity, total_amount) 49 | VALUES (%s, %s, %s, %s) 50 | RETURNING 51 | id, 52 | user_id, 53 | product_id, 54 | quantity, 55 | total_amount::text, 56 | status, 57 | created_at, 58 | updated_at 59 | """, 60 | (user_id, product_id, quantity, total_amount) 61 | ) 62 | result = cur.fetchone() 63 | if result: 64 | result['total_amount'] = Decimal(result['total_amount']) 65 | conn.commit() 66 | return result 67 | 68 | def update_order_status(order_id: int, status: str) -> Optional[Dict]: 69 | """ 70 | Cập nhật trạng thái đơn hàng 71 | 72 | Args: 73 | order_id (int): ID của đơn hàng 74 | status (str): Trạng thái mới (pending, confirmed, paid, cancelled) 75 | 76 | Returns: 77 | Optional[Dict]: Thông tin đơn hàng sau khi cập nhật, None nếu thất bại 78 | """ 79 | with get_db_connection() as conn: 80 | with conn.cursor() as cur: 81 | cur.execute( 82 | """ 83 | UPDATE "order" 84 | SET status = %s, 85 | updated_at = CURRENT_TIMESTAMP 86 | WHERE id = %s 87 | RETURNING id 88 | """, 89 | (status, order_id) 90 | ) 91 | result = cur.fetchone() 92 | conn.commit() 93 | return bool(result) -------------------------------------------------------------------------------- /chatbot-backend/app/database/product_service.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Optional 2 | from .chat_history_service import get_db_connection 3 | from decimal import Decimal 4 | 5 | def init_product_table(): 6 | """ 7 | Khởi tạo bảng product trong database nếu chưa tồn tại 8 | Bảng này lưu trữ thông tin về các sản phẩm bao gồm: 9 | - Tên sản phẩm 10 | - Mô tả 11 | - Giá 12 | - Số lượng tồn kho 13 | - Thông số kỹ thuật 14 | """ 15 | with get_db_connection() as conn: 16 | with conn.cursor() as cur: 17 | cur.execute(""" 18 | CREATE TABLE IF NOT EXISTS product ( 19 | id SERIAL PRIMARY KEY, 20 | name VARCHAR(255) NOT NULL, 21 | description TEXT, 22 | price DECIMAL(10,2) NOT NULL, 23 | stock INTEGER NOT NULL DEFAULT 0, 24 | specifications JSONB, 25 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 26 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 27 | ) 28 | """) 29 | conn.commit() 30 | 31 | def get_product_by_name(name: str) -> Optional[Dict]: 32 | """ 33 | Tìm kiếm sản phẩm theo tên 34 | 35 | Args: 36 | name (str): Tên sản phẩm cần tìm 37 | 38 | Returns: 39 | Optional[Dict]: Thông tin sản phẩm nếu tìm thấy, None nếu không tìm thấy 40 | """ 41 | with get_db_connection() as conn: 42 | with conn.cursor() as cur: 43 | cur.execute( 44 | """ 45 | SELECT 46 | id, 47 | name, 48 | description, 49 | price::text, 50 | stock, 51 | specifications, 52 | created_at, 53 | updated_at 54 | FROM product 55 | WHERE LOWER(name) LIKE LOWER(%s) 56 | """, 57 | (f"%{name}%",) 58 | ) 59 | result = cur.fetchone() 60 | if result: 61 | result['price'] = Decimal(result['price']) 62 | return result 63 | 64 | def check_product_stock(product_id: int, quantity: int) -> bool: 65 | """ 66 | Kiểm tra số lượng tồn kho của sản phẩm 67 | 68 | Args: 69 | product_id (int): ID của sản phẩm 70 | quantity (int): Số lượng cần kiểm tra 71 | 72 | Returns: 73 | bool: True nếu đủ số lượng, False nếu không đủ 74 | """ 75 | with get_db_connection() as conn: 76 | with conn.cursor() as cur: 77 | cur.execute( 78 | """ 79 | SELECT stock 80 | FROM product 81 | WHERE id = %s 82 | """, 83 | (product_id,) 84 | ) 85 | result = cur.fetchone() 86 | if result and result['stock'] >= quantity: 87 | return True 88 | return False 89 | 90 | def update_product_stock(product_id: int, quantity: int) -> bool: 91 | """ 92 | Cập nhật số lượng tồn kho của sản phẩm 93 | 94 | Args: 95 | product_id (int): ID của sản phẩm 96 | quantity (int): Số lượng cần trừ đi (số âm để thêm vào) 97 | 98 | Returns: 99 | bool: True nếu cập nhật thành công, False nếu thất bại 100 | """ 101 | with get_db_connection() as conn: 102 | with conn.cursor() as cur: 103 | cur.execute( 104 | """ 105 | UPDATE product 106 | SET stock = stock - %s, 107 | updated_at = CURRENT_TIMESTAMP 108 | WHERE id = %s AND stock >= %s 109 | RETURNING id 110 | """, 111 | (quantity, product_id, quantity) 112 | ) 113 | result = cur.fetchone() 114 | conn.commit() 115 | return bool(result) 116 | 117 | def main(): 118 | quantity = check_product_stock(1, 1) 119 | print('check_product_stock(1, 1): ', quantity) 120 | 121 | if __name__ == '__main__': 122 | main() -------------------------------------------------------------------------------- /chatbot-backend/app/database/seed.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict 2 | from app.database.chat_history_service import get_db_connection 3 | import json 4 | from app.database.product_service import init_product_table 5 | from app.database.order_service import init_order_table 6 | from app.database.wallet_service import init_wallet_table, create_wallet 7 | from decimal import Decimal 8 | 9 | SAMPLE_PRODUCTS = [ 10 | { 11 | "name": "iPhone 16 Pro Max", 12 | "description": "iPhone 16 Pro Max mới nhất với nhiều tính năng đột phá", 13 | "price": 35990000, 14 | "stock": 50, 15 | "specifications": { 16 | "màn_hình": "6.9 inch OLED ProMotion 144Hz", 17 | "chip": "A18 Bionic", 18 | "ram": "12GB", 19 | "bộ_nhớ": "1TB", 20 | "camera": "108MP + 48MP + 12MP", 21 | "pin": "5000mAh", 22 | "sạc": "45W", 23 | "màu_sắc": ["Titan Đen", "Titan Trắng", "Titan Vàng", "Titan Xanh"] 24 | } 25 | }, 26 | { 27 | "name": "Samsung Galaxy S24 Ultra", 28 | "description": "Samsung Galaxy S24 Ultra với bút S-Pen và camera zoom 100x", 29 | "price": 31990000, 30 | "stock": 40, 31 | "specifications": { 32 | "màn_hình": "6.8 inch Dynamic AMOLED 2X 120Hz", 33 | "chip": "Snapdragon 8 Gen 3", 34 | "ram": "12GB", 35 | "bộ_nhớ": "512GB", 36 | "camera": "200MP + 50MP + 12MP + 10MP", 37 | "pin": "5000mAh", 38 | "sạc": "45W", 39 | "màu_sắc": ["Đen", "Cream", "Violet", "Xanh"] 40 | } 41 | }, 42 | { 43 | "name": "Xiaomi 14 Pro", 44 | "description": "Xiaomi 14 Pro với camera Leica và sạc siêu nhanh", 45 | "price": 22990000, 46 | "stock": 60, 47 | "specifications": { 48 | "màn_hình": "6.7 inch AMOLED 120Hz", 49 | "chip": "Snapdragon 8 Gen 3", 50 | "ram": "12GB", 51 | "bộ_nhớ": "256GB", 52 | "camera": "50MP + 50MP + 50MP", 53 | "pin": "4800mAh", 54 | "sạc": "120W", 55 | "màu_sắc": ["Đen", "Trắng", "Xanh"] 56 | } 57 | }, 58 | { 59 | "name": "OPPO Find X7 Ultra", 60 | "description": "OPPO Find X7 Ultra với camera Hasselblad", 61 | "price": 24990000, 62 | "stock": 45, 63 | "specifications": { 64 | "màn_hình": "6.8 inch AMOLED 120Hz", 65 | "chip": "Dimensity 9300", 66 | "ram": "16GB", 67 | "bộ_nhớ": "512GB", 68 | "camera": "50MP + 50MP + 50MP + 50MP", 69 | "pin": "5000mAh", 70 | "sạc": "100W", 71 | "màu_sắc": ["Đen", "Bạc", "Xanh"] 72 | } 73 | }, 74 | { 75 | "name": "Google Pixel 8 Pro", 76 | "description": "Google Pixel 8 Pro với AI và camera đỉnh cao", 77 | "price": 25990000, 78 | "stock": 35, 79 | "specifications": { 80 | "màn_hình": "6.7 inch LTPO OLED 120Hz", 81 | "chip": "Google Tensor G3", 82 | "ram": "12GB", 83 | "bộ_nhớ": "256GB", 84 | "camera": "50MP + 48MP + 48MP", 85 | "pin": "5000mAh", 86 | "sạc": "30W", 87 | "màu_sắc": ["Obsidian", "Bay", "Porcelain"] 88 | } 89 | } 90 | ] 91 | 92 | # Sample users with initial balance 93 | SAMPLE_USERS = [ 94 | { 95 | "user_id": "user1", 96 | "balance": Decimal("200000000") # 200 triệu 97 | }, 98 | { 99 | "user_id": "user2", 100 | "balance": Decimal("150000000") # 150 triệu 101 | }, 102 | { 103 | "user_id": "user3", 104 | "balance": Decimal("300000000") # 300 triệu 105 | } 106 | ] 107 | 108 | def seed_products(): 109 | """Seed products into database""" 110 | with get_db_connection() as conn: 111 | with conn.cursor() as cur: 112 | # Clear existing products 113 | cur.execute("TRUNCATE TABLE product CASCADE") 114 | 115 | # Insert new products 116 | for product in SAMPLE_PRODUCTS: 117 | cur.execute( 118 | """ 119 | INSERT INTO product (name, description, price, stock, specifications) 120 | VALUES (%s, %s, %s, %s, %s) 121 | """, 122 | ( 123 | product["name"], 124 | product["description"], 125 | product["price"], 126 | product["stock"], 127 | json.dumps(product["specifications"]) 128 | ) 129 | ) 130 | conn.commit() 131 | 132 | def seed_wallets(): 133 | """Seed user wallets into database""" 134 | for user in SAMPLE_USERS: 135 | create_wallet(user["user_id"], user["balance"]) 136 | 137 | def init_and_seed_database(): 138 | """Initialize tables and seed data""" 139 | print("Initializing tables...") 140 | init_product_table() 141 | init_order_table() 142 | init_wallet_table() 143 | 144 | print("Seeding products...") 145 | seed_products() 146 | 147 | print("Seeding user wallets...") 148 | seed_wallets() 149 | 150 | print("Database initialization and seeding completed!") 151 | 152 | if __name__ == "__main__": 153 | init_and_seed_database() -------------------------------------------------------------------------------- /chatbot-backend/app/database/wallet_service.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict 2 | from .chat_history_service import get_db_connection 3 | from decimal import Decimal 4 | 5 | def init_wallet_table(): 6 | """ 7 | Khởi tạo bảng user_wallet trong database nếu chưa tồn tại 8 | Bảng này lưu trữ thông tin về ví tiền của người dùng bao gồm: 9 | - ID người dùng 10 | - Số dư 11 | - Thời gian tạo và cập nhật 12 | """ 13 | with get_db_connection() as conn: 14 | with conn.cursor() as cur: 15 | cur.execute(""" 16 | CREATE TABLE IF NOT EXISTS user_wallet ( 17 | id SERIAL PRIMARY KEY, 18 | user_id VARCHAR(255) NOT NULL UNIQUE, 19 | balance DECIMAL(15,2) NOT NULL DEFAULT 0, 20 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 21 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 22 | ) 23 | """) 24 | conn.commit() 25 | 26 | def get_wallet(user_id: str) -> Optional[Dict]: 27 | """ 28 | Lấy thông tin ví của người dùng 29 | 30 | Args: 31 | user_id (str): ID của người dùng 32 | 33 | Returns: 34 | Optional[Dict]: Thông tin ví nếu tìm thấy, None nếu không tìm thấy 35 | """ 36 | with get_db_connection() as conn: 37 | with conn.cursor() as cur: 38 | cur.execute( 39 | """ 40 | SELECT 41 | id, 42 | user_id, 43 | balance::text, 44 | created_at, 45 | updated_at 46 | FROM user_wallet 47 | WHERE user_id = %s 48 | """, 49 | (user_id,) 50 | ) 51 | result = cur.fetchone() 52 | if result: 53 | result['balance'] = Decimal(result['balance']) 54 | return result 55 | 56 | def create_wallet(user_id: str, initial_balance: Decimal = Decimal('0')) -> Optional[Dict]: 57 | """ 58 | Tạo ví mới cho người dùng 59 | 60 | Args: 61 | user_id (str): ID của người dùng 62 | initial_balance (Decimal): Số dư ban đầu, mặc định là 0 63 | 64 | Returns: 65 | Optional[Dict]: Thông tin ví nếu tạo thành công, None nếu thất bại 66 | """ 67 | with get_db_connection() as conn: 68 | with conn.cursor() as cur: 69 | cur.execute( 70 | """ 71 | INSERT INTO user_wallet (user_id, balance) 72 | VALUES (%s, %s) 73 | ON CONFLICT (user_id) DO UPDATE 74 | SET balance = EXCLUDED.balance 75 | RETURNING 76 | id, 77 | user_id, 78 | balance::text, 79 | created_at, 80 | updated_at 81 | """, 82 | (user_id, initial_balance) 83 | ) 84 | result = cur.fetchone() 85 | if result: 86 | result['balance'] = Decimal(result['balance']) 87 | conn.commit() 88 | return result 89 | 90 | def update_balance(user_id: str, amount: Decimal) -> Optional[Dict]: 91 | """ 92 | Cập nhật số dư trong ví của người dùng 93 | 94 | Args: 95 | user_id (str): ID của người dùng 96 | amount (Decimal): Số tiền cần thay đổi (dương để thêm vào, âm để trừ đi) 97 | 98 | Returns: 99 | Optional[Dict]: Thông tin ví sau khi cập nhật, None nếu thất bại hoặc số dư không đủ 100 | """ 101 | with get_db_connection() as conn: 102 | with conn.cursor() as cur: 103 | cur.execute( 104 | """ 105 | UPDATE user_wallet 106 | SET balance = balance + %s, 107 | updated_at = CURRENT_TIMESTAMP 108 | WHERE user_id = %s AND balance + %s >= 0 109 | RETURNING 110 | id, 111 | user_id, 112 | balance::text, 113 | created_at, 114 | updated_at 115 | """, 116 | (amount, user_id, amount) 117 | ) 118 | result = cur.fetchone() 119 | if result: 120 | result['balance'] = Decimal(result['balance']) 121 | conn.commit() 122 | return result -------------------------------------------------------------------------------- /chatbot-backend/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from app.api import router as api_router 4 | 5 | app = FastAPI() 6 | 7 | app.add_middleware( 8 | CORSMiddleware, 9 | allow_origins=["http://localhost:3000"], # Frontend URL 10 | allow_credentials=True, 11 | allow_methods=["*"], 12 | allow_headers=["*"], 13 | ) 14 | 15 | app.include_router(api_router, prefix="/api") 16 | -------------------------------------------------------------------------------- /chatbot-backend/requirements.txt: -------------------------------------------------------------------------------- 1 | langchain==0.3.7 2 | langchain-openai==0.2.9 3 | psycopg 4 | pydantic 5 | python-dotenv==1.0.1 6 | fastapi 7 | uvicorn 8 | -------------------------------------------------------------------------------- /chatbot-backend/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="chatbot-backend", 5 | version="0.1", 6 | packages=find_packages(), 7 | install_requires=[ 8 | "fastapi", 9 | "uvicorn", 10 | "python-dotenv", 11 | "langchain", 12 | "langchain-openai", 13 | "psycopg", 14 | "pydantic" 15 | ], 16 | ) -------------------------------------------------------------------------------- /chatbot-frontend/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=http://localhost:8030 -------------------------------------------------------------------------------- /chatbot-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatbot-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.13.3", 7 | "@emotion/styled": "^11.13.0", 8 | "@mui/icons-material": "^6.1.9", 9 | "@mui/material": "^6.1.6", 10 | "@testing-library/jest-dom": "^5.17.0", 11 | "@testing-library/react": "^13.4.0", 12 | "@testing-library/user-event": "^13.5.0", 13 | "axios": "^1.7.7", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.3.1", 16 | "react-markdown": "^9.0.1", 17 | "react-router-dom": "^6.28.0", 18 | "react-scripts": "5.0.1", 19 | "react-syntax-highlighter": "^15.6.1", 20 | "styled-components": "^6.1.13", 21 | "uuid": "^11.0.3", 22 | "web-vitals": "^2.1.4" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /chatbot-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaizenX209/Automation-Chatbot/74ef0ef2ec6f1ea2073dc9da28b598cd32882107/chatbot-frontend/public/favicon.ico -------------------------------------------------------------------------------- /chatbot-frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /chatbot-frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaizenX209/Automation-Chatbot/74ef0ef2ec6f1ea2073dc9da28b598cd32882107/chatbot-frontend/public/logo192.png -------------------------------------------------------------------------------- /chatbot-frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaizenX209/Automation-Chatbot/74ef0ef2ec6f1ea2073dc9da28b598cd32882107/chatbot-frontend/public/logo512.png -------------------------------------------------------------------------------- /chatbot-frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /chatbot-frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /chatbot-frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ChatPage from "./pages/ChatPage"; 3 | 4 | /** 5 | * Component gốc của ứng dụng 6 | * Render trang ChatPage làm trang chính 7 | */ 8 | function App() { 9 | return ; 10 | } 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /chatbot-frontend/src/components/chat/ChatBox.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import { chatService } from '../../services/chatService'; 4 | import ChatMessage from './ChatMessage'; 5 | import { v4 as uuidv4 } from 'uuid'; 6 | 7 | const ChatContainer = styled.div` 8 | width: 100%; 9 | height: 100%; 10 | border: 1px solid #e0e0e0; 11 | border-radius: 12px; 12 | display: flex; 13 | flex-direction: column; 14 | margin: 0 auto; 15 | background-color: #ffffff; 16 | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); 17 | `; 18 | 19 | const MessagesContainer = styled.div` 20 | flex: 1; 21 | overflow-y: auto; 22 | padding: 24px; 23 | background-color: #f8f9fa; 24 | 25 | &::-webkit-scrollbar { 26 | width: 8px; 27 | } 28 | 29 | &::-webkit-scrollbar-track { 30 | background: #f1f1f1; 31 | } 32 | 33 | &::-webkit-scrollbar-thumb { 34 | background: #888; 35 | border-radius: 4px; 36 | } 37 | 38 | &::-webkit-scrollbar-thumb:hover { 39 | background: #555; 40 | } 41 | `; 42 | 43 | const InputContainer = styled.div` 44 | display: flex; 45 | padding: 24px; 46 | border-top: 1px solid #e0e0e0; 47 | background-color: #ffffff; 48 | `; 49 | 50 | const Input = styled.input` 51 | flex: 1; 52 | padding: 16px; 53 | border: 2px solid #e0e0e0; 54 | border-radius: 8px; 55 | margin-right: 16px; 56 | font-size: 16px; 57 | transition: border-color 0.3s ease; 58 | 59 | &:focus { 60 | outline: none; 61 | border-color: #1a237e; 62 | } 63 | 64 | &::placeholder { 65 | color: #9e9e9e; 66 | } 67 | `; 68 | 69 | const Button = styled.button` 70 | padding: 16px 32px; 71 | background: #1a237e; 72 | color: white; 73 | border: none; 74 | border-radius: 8px; 75 | cursor: pointer; 76 | font-size: 16px; 77 | font-weight: 600; 78 | transition: background-color 0.3s ease; 79 | 80 | &:hover { 81 | background: #283593; 82 | } 83 | 84 | &:disabled { 85 | background: #9e9e9e; 86 | cursor: not-allowed; 87 | } 88 | `; 89 | 90 | const PageWrapper = styled.div` 91 | height: 100%; 92 | display: flex; 93 | flex-direction: column; 94 | `; 95 | 96 | const Title = styled.h1` 97 | color: #1a237e; 98 | text-align: center; 99 | margin-bottom: 24px; 100 | font-size: 2.5rem; 101 | font-weight: bold; 102 | `; 103 | 104 | /** 105 | * Component chính xử lý giao diện chat 106 | * Bao gồm: 107 | * - Hiển thị danh sách tin nhắn 108 | * - Ô nhập tin nhắn 109 | * - Nút gửi tin nhắn 110 | * - Xử lý gửi/nhận tin nhắn với backend 111 | */ 112 | function ChatBox() { 113 | // State lưu trữ danh sách tin nhắn 114 | const [messages, setMessages] = useState([]); 115 | // State lưu trữ nội dung đang nhập 116 | const [input, setInput] = useState(''); 117 | // State đánh dấu đang gửi tin nhắn 118 | const [isLoading, setIsLoading] = useState(false); 119 | // Ref để scroll đến tin nhắn cuối cùng 120 | const messagesEndRef = useRef(null); 121 | // ID phiên chat 122 | const [threadId] = useState(uuidv4()); 123 | // Ref lưu nội dung tin nhắn đang stream 124 | const streamedMessageRef = useRef(''); 125 | 126 | /** 127 | * Xử lý khi người dùng gửi tin nhắn 128 | * - Thêm tin nhắn người dùng vào danh sách 129 | * - Gọi API gửi tin nhắn 130 | * - Xử lý phản hồi stream từ server 131 | * - Cập nhật UI với từng token nhận được 132 | */ 133 | const handleSubmit = async () => { 134 | // Kiểm tra input rỗng hoặc đang trong quá trình gửi 135 | if (!input.trim() || isLoading) return; 136 | 137 | // Lấy nội dung tin nhắn và reset input 138 | const userMessage = input.trim(); 139 | setInput(''); 140 | // Đánh dấu đang gửi tin nhắn 141 | setIsLoading(true); 142 | // Reset nội dung tin nhắn đang stream 143 | streamedMessageRef.current = ''; 144 | 145 | // Thêm tin nhắn của người dùng vào danh sách 146 | setMessages(prev => [...prev, { text: userMessage, isUser: true }]); 147 | 148 | try { 149 | // Thêm một tin nhắn rỗng của bot để hiển thị streaming 150 | setMessages(prev => [...prev, { text: '', isUser: false }]); 151 | 152 | // Gọi API stream và xử lý từng token nhận được 153 | await chatService.sendMessageStream( 154 | userMessage, 155 | threadId, 156 | // Callback xử lý mỗi khi nhận được token mới 157 | (token) => { 158 | // Cộng dồn token vào nội dung tin nhắn 159 | streamedMessageRef.current += token; 160 | // Cập nhật tin nhắn cuối cùng với nội dung mới 161 | setMessages(prev => { 162 | const newMessages = [...prev]; 163 | const lastMessage = newMessages[newMessages.length - 1]; 164 | lastMessage.text = streamedMessageRef.current; 165 | return newMessages; 166 | }); 167 | }, 168 | // Callback xử lý khi có lỗi 169 | (error) => { 170 | console.error('Stream error:', error); 171 | // Cập nhật tin nhắn cuối thành thông báo lỗi 172 | setMessages(prev => { 173 | const newMessages = [...prev]; 174 | newMessages[newMessages.length - 1].text = 'Sorry, I am not able to process your request.'; 175 | return newMessages; 176 | }); 177 | } 178 | ); 179 | } catch (error) { 180 | // Xử lý lỗi chung 181 | console.error('Error:', error); 182 | setMessages(prev => [...prev, { 183 | text: 'Sorry, I am not able to process your request.', 184 | isUser: false 185 | }]); 186 | } finally { 187 | // Reset trạng thái loading 188 | setIsLoading(false); 189 | } 190 | }; 191 | 192 | /** 193 | * Effect tự động scroll đến tin nhắn cuối cùng 194 | * khi có tin nhắn mới 195 | */ 196 | useEffect(() => { 197 | messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); 198 | }, [messages]); 199 | 200 | return ( 201 | 202 | Sales Assistant AI 203 | 204 | 205 | {messages.map((message, index) => ( 206 | 211 | ))} 212 |
213 | 214 | 215 | setInput(e.target.value)} 218 | onKeyPress={(e) => e.key === 'Enter' && handleSubmit()} 219 | placeholder="Enter your message..." 220 | disabled={isLoading} 221 | /> 222 | 228 | 229 | 230 | 231 | ); 232 | } 233 | 234 | export default ChatBox; -------------------------------------------------------------------------------- /chatbot-frontend/src/components/chat/ChatMessage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import ReactMarkdown from 'react-markdown'; 4 | 5 | const MessageContainer = styled.div` 6 | display: flex; 7 | margin-bottom: 20px; 8 | justify-content: ${props => props.isUser ? 'flex-end' : 'flex-start'}; 9 | `; 10 | 11 | const MessageBubble = styled.div` 12 | max-width: 70%; 13 | padding: 16px 20px; 14 | border-radius: ${props => props.isUser ? '20px 20px 0 20px' : '20px 20px 20px 0'}; 15 | background-color: ${props => props.isUser ? '#1a237e' : '#ffffff'}; 16 | color: ${props => props.isUser ? '#ffffff' : '#000000'}; 17 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); 18 | font-size: 16px; 19 | line-height: 1.5; 20 | 21 | p { 22 | margin: 0; 23 | } 24 | 25 | code { 26 | background-color: ${props => props.isUser ? '#283593' : '#f5f5f5'}; 27 | padding: 2px 6px; 28 | border-radius: 4px; 29 | font-family: 'Courier New', Courier, monospace; 30 | font-size: 14px; 31 | } 32 | 33 | pre { 34 | background-color: ${props => props.isUser ? '#283593' : '#f5f5f5'}; 35 | padding: 12px; 36 | border-radius: 8px; 37 | overflow-x: auto; 38 | margin: 8px 0; 39 | 40 | code { 41 | background-color: transparent; 42 | padding: 0; 43 | } 44 | } 45 | 46 | ul, ol { 47 | margin: 8px 0; 48 | padding-left: 20px; 49 | } 50 | 51 | table { 52 | border-collapse: collapse; 53 | margin: 8px 0; 54 | width: 100%; 55 | 56 | th, td { 57 | border: 1px solid ${props => props.isUser ? '#283593' : '#e0e0e0'}; 58 | padding: 8px; 59 | text-align: left; 60 | } 61 | 62 | th { 63 | background-color: ${props => props.isUser ? '#283593' : '#f5f5f5'}; 64 | } 65 | } 66 | `; 67 | 68 | /** 69 | * Component hiển thị một tin nhắn trong cuộc trò chuyện 70 | * Hỗ trợ: 71 | * - Hiển thị tin nhắn người dùng và bot với style khác nhau 72 | * - Render markdown content 73 | * - Style cho code blocks, tables, lists 74 | * 75 | * @param {Object} props - Props của component 76 | * @param {Object} props.message - Thông tin tin nhắn 77 | * @param {string} props.message.text - Nội dung tin nhắn 78 | * @param {boolean} props.isUser - Đánh dấu tin nhắn của người dùng hay bot 79 | */ 80 | const ChatMessage = ({ message, isUser }) => { 81 | // Chuyển đổi tin nhắn sang string nếu chưa phải 82 | const messageText = typeof message.text === 'string' 83 | ? message.text 84 | : JSON.stringify(message.text); 85 | 86 | return ( 87 | 88 | 89 | {messageText} 90 | 91 | 92 | ); 93 | }; 94 | 95 | export default ChatMessage; -------------------------------------------------------------------------------- /chatbot-frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | import { createGlobalStyle } from "styled-components"; 5 | 6 | const GlobalStyle = createGlobalStyle` 7 | body { 8 | margin: 0; 9 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 10 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 11 | sans-serif; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | } 19 | 20 | html, body, #root { 21 | height: 100%; 22 | } 23 | `; 24 | 25 | const root = ReactDOM.createRoot(document.getElementById("root")); 26 | root.render( 27 | 28 | 29 | 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /chatbot-frontend/src/pages/ChatPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ChatBox from '../components/chat/ChatBox'; 3 | import styled from 'styled-components'; 4 | 5 | const PageWrapper = styled.div` 6 | height: 100%; 7 | padding: 24px; 8 | display: flex; 9 | flex-direction: column; 10 | `; 11 | 12 | /** 13 | * Trang chính của ứng dụng chat 14 | * Bao gồm: 15 | * - Layout trang 16 | * - Component ChatBox để xử lý chat 17 | */ 18 | const ChatPage = () => { 19 | return ( 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default ChatPage; 27 | -------------------------------------------------------------------------------- /chatbot-frontend/src/services/chatService.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | const API_URL = "http://localhost:8030"; 3 | 4 | /** 5 | * Service xử lý các tương tác chat với backend 6 | */ 7 | export const chatService = { 8 | /** 9 | * Gửi tin nhắn đến server và nhận phản hồi một lần 10 | * 11 | * @param {string} message - Nội dung tin nhắn cần gửi 12 | * @param {string} sessionId - ID phiên chat 13 | * @param {function} onChunk - Callback xử lý khi nhận được phản hồi 14 | * @returns {Promise} Phản hồi từ server 15 | */ 16 | sendMessage: async (message, sessionId, onChunk = (chunk) => {}) => { 17 | try { 18 | const response = await fetch(`${API_URL}/api/v1/chat/chat`, { 19 | method: "POST", 20 | headers: { 21 | "Content-Type": "application/json", 22 | }, 23 | body: JSON.stringify({ 24 | question: message, 25 | thread_id: sessionId, 26 | }), 27 | }); 28 | 29 | if (!response.ok) { 30 | throw new Error("Network response was not ok"); 31 | } 32 | 33 | const data = await response.json(); 34 | onChunk(data.answer); 35 | return data.answer; 36 | } catch (error) { 37 | console.error("Chat service error:", error); 38 | throw error; 39 | } 40 | }, 41 | 42 | /** 43 | * Gửi tin nhắn đến server và nhận phản hồi dạng stream 44 | * 45 | * @param {string} message - Nội dung tin nhắn cần gửi 46 | * @param {string} sessionId - ID phiên chat 47 | * @param {function} onToken - Callback xử lý từng token nhận được 48 | * @param {function} onError - Callback xử lý khi có lỗi 49 | */ 50 | sendMessageStream: async ( 51 | message, // Nội dung tin nhắn từ người dùng 52 | sessionId, // ID của phiên chat hiện tại 53 | onToken = (token) => {}, // Callback được gọi mỗi khi nhận được token mới 54 | onError = (error) => {} // Callback xử lý lỗi 55 | ) => { 56 | try { 57 | // Gửi request POST đến API endpoint 58 | const response = await fetch(`${API_URL}/api/v1/chat/chat/stream`, { 59 | method: "POST", 60 | headers: { 61 | "Content-Type": "application/json", 62 | }, 63 | // Đóng gói dữ liệu gửi đi 64 | body: JSON.stringify({ 65 | question: message, 66 | thread_id: sessionId, 67 | }), 68 | }); 69 | 70 | // Kiểm tra response status, ném lỗi nếu không thành công 71 | if (!response.ok) { 72 | throw new Error("Network response was not ok"); 73 | } 74 | 75 | // Khởi tạo reader để đọc dữ liệu stream 76 | const reader = response.body.getReader(); 77 | // Tạo decoder để chuyển đổi dữ liệu nhị phân thành text 78 | const decoder = new TextDecoder(); 79 | 80 | // Vòng lặp vô hạn để đọc stream cho đến khi kết thúc 81 | while (true) { 82 | // Đọc một chunk dữ liệu từ stream 83 | const { value, done } = await reader.read(); 84 | // Nếu đã đọc xong thì thoát vòng lặp 85 | if (done) break; 86 | 87 | // Chuyển đổi chunk dữ liệu thành text 88 | const chunk = decoder.decode(value); 89 | // Tách chunk thành các dòng và lọc 90 | // - Loại bỏ dòng trống 91 | // - Chỉ lấy dòng bắt đầu bằng "data: " 92 | const lines = chunk 93 | .split("\n") 94 | .filter((line) => line.trim() !== "" && line.startsWith("data: ")); 95 | 96 | // Xử lý từng dòng SSE (Server-Sent Events) 97 | for (const line of lines) { 98 | try { 99 | // Bỏ prefix "data: " và parse JSON 100 | const jsonStr = line.replace("data: ", ""); 101 | const json = JSON.parse(jsonStr); 102 | 103 | // Kiểm tra nếu server trả về lỗi 104 | if (json.error) { 105 | onError(json.error); // Gọi callback xử lý lỗi 106 | return; // Kết thúc xử lý 107 | } 108 | 109 | // Nếu có nội dung, gửi token đến UI qua callback 110 | if (json.content) { 111 | onToken(json.content); 112 | } 113 | } catch (e) { 114 | // Xử lý lỗi khi parse JSON thất bại 115 | console.error("Error parsing SSE message:", e); 116 | } 117 | } 118 | } 119 | } catch (error) { 120 | // Xử lý các lỗi khác (network, stream, etc.) 121 | console.error("Stream error:", error); 122 | onError(error.message); // Thông báo lỗi cho UI 123 | } 124 | }, 125 | }; 126 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | backend_database: 5 | image: postgres:latest 6 | container_name: sales-chatbot-backend-postgres 7 | environment: 8 | POSTGRES_USER: ${DB_USERNAME} 9 | POSTGRES_PASSWORD: ${DB_PASSWORD} 10 | POSTGRES_DB: ${DB_NAME} 11 | ports: 12 | - "${DB_PORT}:5432" 13 | volumes: 14 | - sales_chatbot_backend_data:/var/lib/postgresql/data 15 | networks: 16 | - sales-chatbot-network 17 | healthcheck: 18 | test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_NAME}"] 19 | interval: 10s 20 | timeout: 5s 21 | retries: 5 22 | 23 | networks: 24 | sales-chatbot-network: 25 | driver: bridge 26 | 27 | volumes: 28 | sales_chatbot_backend_data: 29 | driver: local 30 | --------------------------------------------------------------------------------