Commit fbd1e348 authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

Update chatbot UI and memo embeddings

parent 32666a41
""" """
CiCi Fashion Consultant - System Prompt CiCi Fashion Consultant - System Prompt
Tư vấn thời trang CANIFA chuyên nghiệp
Version 3.0 - Dynamic from File Version 3.0 - Dynamic from File
""" """
...@@ -9,29 +8,35 @@ from datetime import datetime ...@@ -9,29 +8,35 @@ from datetime import datetime
PROMPT_FILE_PATH = os.path.join(os.path.dirname(__file__), "system_prompt.txt") PROMPT_FILE_PATH = os.path.join(os.path.dirname(__file__), "system_prompt.txt")
def _ensure_json_instruction(prompt_text: str) -> str:
if "json" in prompt_text.lower():
return prompt_text
return f"{prompt_text}\n\nReturn JSON (json) object with keys: ai_response, product_ids."
def get_system_prompt() -> str: def get_system_prompt() -> str:
""" """
System prompt cho CiCi Fashion Agent System prompt for CiCi Fashion Agent.
Đọc từ file system_prompt.txt để có thể update dynamic.
Returns: Returns:
str: System prompt với ngày hiện tại str: System prompt with the current date.
""" """
now = datetime.now() date_str = datetime.now().strftime("%d/%m/%Y")
date_str = now.strftime("%d/%m/%Y")
try: try:
if os.path.exists(PROMPT_FILE_PATH): if os.path.exists(PROMPT_FILE_PATH):
with open(PROMPT_FILE_PATH, "r", encoding="utf-8") as f: with open(PROMPT_FILE_PATH, "r", encoding="utf-8") as handle:
prompt_template = f.read() prompt_template = handle.read()
return prompt_template.replace("{date_str}", date_str) rendered = prompt_template.replace("{date_str}", date_str)
except Exception as e: return _ensure_json_instruction(rendered)
print(f"Error reading system prompt file: {e}") except Exception as exc:
print(f"Error reading system prompt file: {exc}")
# Fallback default prompt if file error
return f"""# VAI TRÒ fallback = f"""# ROLE
Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA. You are CiCi, a CANIFA fashion assistant.
Hôm nay: {date_str} Today: {date_str}
KHÔNG BAO GIỜ BỊA ĐẶT. TRẢ LỜI NGẮN GỌN. Never fabricate. Keep responses concise.
""" """
return _ensure_json_instruction(fallback)
# VAI TRÒ # ROLE
You are CiCi, a CANIFA fashion assistant.
Bạn là CiCi - Chuyên viên tư vấn thời trang CANIFA. Today: {date_str}
- Nhiệt tình, thân thiện, chuyên nghiệp
- CANIFA BÁN QUẦN ÁO: áo, quần, váy, đầm, phụ kiện thời trang # STYLE
- Hôm nay: {date_str} - Friendly, concise, and professional.
- If the user writes in Vietnamese, respond in Vietnamese. If the user writes in English, respond in English.
--- - Never fabricate. If data is missing, say the shop does not have it.
# QUY TẮC TRUNG THỰC - BẮT BUỘC # TOOL USAGE
- Use data_retrieval_tool for product search or product detail questions.
KHÔNG BAO GIỜ BỊA ĐẶT - CHỈ NÓI THEO DỮ LIỆU - If the tool returns 0 products, say the shop does not have that item.
- Do not call tools for simple greetings.
**ĐÚNG:**
- Tool trả về áo thun → Giới thiệu áo thun # OUTPUT (json)
- Tool trả về 0 sản phẩm → Nói "Shop chưa có sản phẩm này" Return a JSON object only (json).
- Tool trả về quần nỉ mà khách hỏi bikini → Nói "Shop chưa có bikini" Keys:
- ai_response: string
**CẤM:** - product_ids: array of objects with sku, name, price, sale_price, url, thumbnail_image_url
- Tool trả về quần nỉ → Gọi là "đồ bơi" No markdown and no extra keys.
- Tool trả về 0 kết quả → Nói "shop có sản phẩm X"
- Tự bịa mã sản phẩm, giá tiền, chính sách
Không có trong data = Không nói = Không tư vấn láo
---
# NGÔN NGỮ & XƯNG HÔ
- Mặc định: Xưng "mình" - gọi "bạn"
- Khi khách xưng anh/chị: Xưng "em" - gọi "anh/chị"
- Khách nói tiếng Việt → Trả lời tiếng Việt
- Khách nói tiếng Anh → Trả lời tiếng Anh
- Ngắn gọn, đi thẳng vào vấn đề
---
# KHI NÀO GỌI TOOL
**Gọi data_retrieval_tool khi:**
- Khách tìm sản phẩm: "Tìm áo...", "Có màu gì..."
- Khách hỏi sản phẩm cụ thể: "Mã 8TS24W001 có không?"
- Tư vấn phong cách: "Mặc gì đi cưới?", "Đồ công sở?"
**⚠️ QUY TẮC SINH QUERY (BẮT BUỘC):**
- **Query chỉ chứa MÔ TẢ SẢN PHẨM** (tên, chất liệu, màu, phong cách).
- **TUYỆT ĐỐI KHÔNG đưa giá tiền vào chuỗi `query`**.
- Giá tiền phải đưa vào tham số riêng: `price_min`, `price_max`.
Ví dụ ĐÚNG:
- Query: "Áo thun nam cotton thoáng mát basic"
- Price_max: 300000
Ví dụ SAI (Cấm):
- Query: "Áo thun nam giá dưới 300k" (SAI vì có giá trong query)
**Gọi canifa_knowledge_search khi:**
- Hỏi chính sách: freeship, đổi trả, bảo hành
- Hỏi thương hiệu: Canifa là gì, lịch sử
- Tìm cửa hàng: địa chỉ, giờ mở cửa
**Không gọi tool khi:**
- Chào hỏi đơn giản: "Hi", "Hello"
- Hỏi lại về sản phẩm vừa show
---
# XỬ LÝ KẾT QUẢ TỪ TOOL
## Sau khi gọi tool, kiểm tra kết quả:
**Trường hợp 1: CÓ sản phẩm phù hợp (đúng loại, đúng yêu cầu)**
- DỪNG LẠI, giới thiệu sản phẩm
- KHÔNG GỌI TOOL LẦN 2
**Trường hợp 2: CÓ kết quả NHƯNG SAI LOẠI**
Ví dụ: Khách hỏi bikini, tool trả về quần nỉ
→ Trả lời thẳng:
"Dạ shop chưa có bikini ạ. Shop chuyên về quần áo thời trang (áo, quần, váy). Bạn có muốn tìm sản phẩm nào khác không?"
CẤM TUYỆT ĐỐI:
- Giới thiệu quần nỉ như thể nó là bikini
- Nói "shop có đồ bơi này bạn tham khảo" khi thực tế là áo/quần thường
**Trường hợp 3: KHÔNG CÓ kết quả (count = 0)**
- Thử lại 1 LẦN với filter rộng hơn
- Nếu vẫn không có:
"Dạ shop chưa có sản phẩm [X] ạ. Bạn có thể tham khảo [loại gần nhất] hoặc ghé shop sau nhé!"
---
# FORMAT ĐẦU RA
Trả về JSON (KHÔNG có markdown backticks):
```json
{{
"ai_response": "Câu trả lời ngắn gọn, mô tả bằng [SKU]",
"product_ids": [
{{
"sku": "8TS24W001",
"name": "Áo thun nam basic",
"price": 200000,
"sale_price": 160000,
"url": "https://canifa.com/...",
"thumbnail_image_url": "https://..."
}}
]
}}
```
**Quy tắc ai_response:**
- Mô tả ngắn gọn, nhắc sản phẩm bằng [SKU]
- Nói qua giá, chất liệu, điểm nổi bật
- KHÔNG tạo bảng markdown
- KHÔNG đưa link, ảnh (frontend tự render)
---
# VÍ DỤ
## Example 1: Chào hỏi
Input: "Chào shop"
Output:
```json
{{
"ai_response": "Chào bạn! Mình là CiCi, tư vấn thời trang CANIFA. Mình có thể giúp gì cho bạn?",
"product_ids": []
}}
```
## Example 2: Tìm sản phẩm CÓ
Input: "Tìm áo thun nam dưới 300k"
Tool trả về: 2 sản phẩm áo thun phù hợp
Output:
```json
{{
"ai_response": "Shop có 2 mẫu áo thun nam giá dưới 300k:
- [8TS24W009]: Áo thun cotton basic, giá 250k đang sale 200k
- [6TN24W012]: Áo thun trơn thoải mái, giá 280k
Bạn kéo xuống xem ảnh nhé!",
"product_ids": [
{{"sku": "8TS24W009", "name": "Áo thun cotton basic", "price": 250000, "sale_price": 200000, "url": "...", "thumbnail_image_url": "..."}},
{{"sku": "6TN24W012", "name": "Áo thun trơn", "price": 280000, "sale_price": null, "url": "...", "thumbnail_image_url": "..."}}
]
}}
```
## Example 3: Khách hỏi KHÔNG CÓ trong kho
Input: "Shop có bikini không?"
Tool trả về: 0 sản phẩm
Output:
```json
{{
"ai_response": "Dạ shop chưa có bikini ạ. CANIFA chuyên về quần áo thời trang như áo, quần, váy, đầm. Bạn có muốn tìm mẫu nào khác không?",
"product_ids": []
}}
```
## Example 4: Tool trả về SAI LOẠI
Input: "Cho tôi xem đồ bơi"
Tool trả về: Quần nỉ, áo nỉ (SAI HOÀN TOÀN so với đồ bơi)
Output:
```json
{{
"ai_response": "Dạ shop chưa có đồ bơi ạ. Shop chuyên bán quần áo thời trang (áo, quần, váy, áo khoác). Bạn có muốn tìm loại sản phẩm nào khác không?",
"product_ids": []
}}
```
TUYỆT ĐỐI KHÔNG giới thiệu sản phẩm sai loại
## Example 5: Khách xưng anh/chị
Input: "Chào em, anh muốn tìm áo sơ mi"
Output:
```json
{{
"ai_response": "Chào anh ạ! Em là CiCi. Anh đang tìm áo sơ mi dài tay hay ngắn tay ạ? Để em tư vấn mẫu phù hợp nhất cho anh nhé!",
"product_ids": []
}}
```
---
# TÓM TẮT
1. CANIFA bán quần áo (áo, quần, váy, đầm, phụ kiện)
2. Không có trong data = Không nói
3. Kiểm tra kỹ tên sản phẩm trước khi giới thiệu
4. Nếu sai loại → Nói thẳng "shop chưa có X"
5. Không bịa giá, mã sản phẩm, chính sách
6. Có kết quả phù hợp = DỪNG, không gọi tool lần 2
7. Trả lời ngắn gọn, dựa 100% vào dữ liệu tool trả về
---
Luôn thành thật, khéo léo, và chuyên nghiệp.
\ No newline at end of file
...@@ -8,6 +8,7 @@ from .attachment_routes import router as attachment_router ...@@ -8,6 +8,7 @@ from .attachment_routes import router as attachment_router
from .shortcut_routes import router as shortcut_router from .shortcut_routes import router as shortcut_router
from .activity_routes import router as activity_router from .activity_routes import router as activity_router
from .idp_routes import router as idp_router from .idp_routes import router as idp_router
from .embedding_routes import router as embedding_router
router = APIRouter(prefix="/api/v1") router = APIRouter(prefix="/api/v1")
...@@ -21,5 +22,6 @@ router.include_router(attachment_router, tags=["attachments"]) ...@@ -21,5 +22,6 @@ router.include_router(attachment_router, tags=["attachments"])
router.include_router(shortcut_router, tags=["shortcuts"]) router.include_router(shortcut_router, tags=["shortcuts"])
router.include_router(activity_router, tags=["activities"]) router.include_router(activity_router, tags=["activities"])
router.include_router(idp_router, tags=["idp"]) router.include_router(idp_router, tags=["idp"])
router.include_router(embedding_router, tags=["memo-embeddings"])
from typing import List
from fastapi import APIRouter, Body, Depends, HTTPException
from memos_core.schemas import MemoEmbeddingCreate, MemoEmbeddingResponse
from memos_core.services import get_memo_embedding_service
router = APIRouter(prefix="/memo-embeddings", tags=["memo-embeddings"])
@router.post("", response_model=MemoEmbeddingResponse)
async def create_embedding(
payload: MemoEmbeddingCreate = Body(...),
svc=Depends(get_memo_embedding_service),
):
try:
return await svc.create_embedding(payload)
except Exception as exc: # pragma: no cover
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/search", response_model=List[MemoEmbeddingResponse])
async def search_embeddings(
payload: dict = Body(...),
svc=Depends(get_memo_embedding_service),
):
"""
Payload:
{
"queryEmbedding": [float],
"topK": 5,
"dateKey": "YYYY-MM-DD" (optional),
"memoId": 1 (optional)
}
"""
try:
query_embedding = payload.get("queryEmbedding") or payload.get("embedding")
top_k = int(payload.get("topK", 5))
date_key = payload.get("dateKey")
memo_id = payload.get("memoId")
return await svc.search_embeddings(query_embedding, top_k=top_k, date_key=date_key, memo_id=memo_id)
except Exception as exc: # pragma: no cover
raise HTTPException(status_code=400, detail=str(exc)) from exc
import asyncio
import json import json
import logging import logging
import asyncio import os
from datetime import datetime, date from datetime import datetime, date
from typing import Any from typing import Any
import aiosqlite
import psycopg import psycopg
from psycopg_pool import AsyncConnectionPool from psycopg_pool import AsyncConnectionPool
from config import CHECKPOINT_POSTGRES_URL from config import CHECKPOINT_POSTGRES_URL, SQLITE_DB_PATH, USE_SQLITE_HISTORY
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -235,14 +237,185 @@ class ConversationManager: ...@@ -235,14 +237,185 @@ class ConversationManager:
await self._pool.close() await self._pool.close()
class SQLiteConversationManager:
"""SQLite-based chat history storage for local/dev testing."""
def __init__(self, db_path: str = SQLITE_DB_PATH, table_name: str = "langgraph_chat_histories"):
self.db_path = db_path
self.table_name = table_name
db_dir = os.path.dirname(self.db_path)
if db_dir:
os.makedirs(db_dir, exist_ok=True)
async def initialize_table(self):
"""Create table and index if not exists."""
try:
async with aiosqlite.connect(self.db_path) as db:
await db.execute(
f"""
CREATE TABLE IF NOT EXISTS {self.table_name} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
identity_key TEXT NOT NULL,
message TEXT NOT NULL,
is_human INTEGER NOT NULL,
timestamp TEXT DEFAULT (DATETIME('now'))
)
"""
)
await db.execute(
f"""
CREATE INDEX IF NOT EXISTS idx_{self.table_name}_identity_timestamp
ON {self.table_name} (identity_key, timestamp)
"""
)
await db.commit()
logger.info(f"SQLite table {self.table_name} initialized at {self.db_path}")
except Exception as e:
logger.error(f"Error initializing SQLite table: {e}", exc_info=True)
raise
async def save_conversation_turn(self, identity_key: str, human_message: str, ai_message: str):
"""Save both human and AI messages in a single transaction."""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
async with aiosqlite.connect(self.db_path) as db:
await db.execute(
f"""INSERT INTO {self.table_name} (identity_key, message, is_human, timestamp)
VALUES (?, ?, ?, ?), (?, ?, ?, ?)""",
(
identity_key,
human_message,
1,
timestamp,
identity_key,
ai_message,
0,
timestamp,
),
)
await db.commit()
logger.debug(f"Saved conversation turn for {identity_key} (SQLite)")
except Exception as e:
logger.error(f"Failed to save conversation for {identity_key} (SQLite): {e}", exc_info=True)
raise
async def get_chat_history(
self, identity_key: str, limit: int | None = None, before_id: int | None = None
) -> list[dict[str, Any]]:
"""Retrieve chat history with optional cursor pagination."""
try:
query = f"""
SELECT id, message, is_human, timestamp
FROM {self.table_name}
WHERE identity_key = ?
"""
params: list[Any] = [identity_key]
if before_id:
query += " AND id < ?"
params.append(before_id)
query += " ORDER BY id DESC"
if limit:
query += " LIMIT ?"
params.append(limit)
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
async with db.execute(query, tuple(params)) as cursor:
rows = await cursor.fetchall()
history: list[dict[str, Any]] = []
for row in rows:
message_content = row["message"]
is_human = bool(row["is_human"])
entry = {
"is_human": is_human,
"timestamp": row["timestamp"],
"id": row["id"],
}
if is_human:
entry["message"] = message_content
else:
try:
parsed = json.loads(message_content)
entry["message"] = parsed.get("ai_response", message_content)
entry["product_ids"] = parsed.get("product_ids", [])
except (json.JSONDecodeError, TypeError):
entry["message"] = message_content
entry["product_ids"] = []
history.append(entry)
return history
except Exception as e:
logger.error(f"Error retrieving chat history (SQLite): {e}", exc_info=True)
return []
async def clear_history(self, identity_key: str):
"""Clear all chat history for an identity."""
try:
async with aiosqlite.connect(self.db_path) as db:
await db.execute(f"DELETE FROM {self.table_name} WHERE identity_key = ?", (identity_key,))
await db.commit()
logger.info(f"Cleared chat history for {identity_key} (SQLite)")
except Exception as e:
logger.error(f"Error clearing chat history for {identity_key} (SQLite): {e}", exc_info=True)
async def get_user_count(self) -> int:
"""Get total number of unique identities."""
try:
async with aiosqlite.connect(self.db_path) as db:
async with db.execute(
f"SELECT COUNT(DISTINCT identity_key) FROM {self.table_name}"
) as cursor:
row = await cursor.fetchone()
return row[0] if row else 0
except Exception as e:
logger.error(f"Error getting user count (SQLite): {e}", exc_info=True)
return 0
async def get_message_count_today(self, identity_key: str) -> int:
"""Count user messages today (for potential rate limiting)."""
try:
async with aiosqlite.connect(self.db_path) as db:
async with db.execute(
f"""
SELECT COUNT(*) FROM {self.table_name}
WHERE identity_key = ?
AND is_human = 1
AND DATE(timestamp) = DATE('now', 'localtime')
""",
(identity_key,),
) as cursor:
row = await cursor.fetchone()
return row[0] if row else 0
except Exception as e:
logger.error(f"Error counting messages for {identity_key} (SQLite): {e}", exc_info=True)
return 0
async def close(self):
# No persistent connection to close when using aiosqlite per-call
return
# --- Singleton --- # --- Singleton ---
_instance: ConversationManager | None = None _instance: ConversationManager | SQLiteConversationManager | None = None
async def get_conversation_manager() -> ConversationManager: async def get_conversation_manager() -> ConversationManager | SQLiteConversationManager:
"""Get or create async ConversationManager singleton""" """Get or create async ConversationManager singleton (Postgres or SQLite based on config)."""
global _instance global _instance
if _instance is None: if _instance is None:
use_sqlite = USE_SQLITE_HISTORY or not CHECKPOINT_POSTGRES_URL
if use_sqlite:
logger.warning("Using SQLite for chat history (USE_SQLITE_HISTORY=true or no Postgres URL).")
_instance = SQLiteConversationManager()
else:
_instance = ConversationManager() _instance = ConversationManager()
await _instance.initialize_table() await _instance.initialize_table()
return _instance return _instance
...@@ -9,6 +9,8 @@ from collections.abc import Callable ...@@ -9,6 +9,8 @@ from collections.abc import Callable
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from fastapi import HTTPException, Request, status from fastapi import HTTPException, Request, status
from config import DISABLE_AUTH
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
if TYPE_CHECKING: if TYPE_CHECKING:
...@@ -62,6 +64,10 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware): ...@@ -62,6 +64,10 @@ class CanifaAuthMiddleware(BaseHTTPMiddleware):
path = request.url.path path = request.url.path
method = request.method method = request.method
# Temporary bypass: skip auth/rate-limit when DISABLE_AUTH=true
if DISABLE_AUTH:
return await call_next(request)
# ✅ Allow OPTIONS requests (CORS preflight) # ✅ Allow OPTIONS requests (CORS preflight)
if method == "OPTIONS": if method == "OPTIONS":
return await call_next(request) return await call_next(request)
......
...@@ -55,6 +55,10 @@ __all__ = [ ...@@ -55,6 +55,10 @@ __all__ = [
"USE_MONGO_CONVERSATION", "USE_MONGO_CONVERSATION",
"RATE_LIMIT_GUEST", "RATE_LIMIT_GUEST",
"RATE_LIMIT_USER", "RATE_LIMIT_USER",
"USE_SQLITE_HISTORY",
"SQLITE_DB_PATH",
"DISABLE_AUTH",
"MEMO_DB_PATH",
] ]
# ====================== SUPABASE CONFIGURATION ====================== # ====================== SUPABASE CONFIGURATION ======================
...@@ -140,4 +144,20 @@ OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES = os.getenv("OTEL_EXPORTER_JA ...@@ -140,4 +144,20 @@ OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES = os.getenv("OTEL_EXPORTER_JA
RATE_LIMIT_GUEST: int = int(os.getenv("RATE_LIMIT_GUEST", "10")) RATE_LIMIT_GUEST: int = int(os.getenv("RATE_LIMIT_GUEST", "10"))
RATE_LIMIT_USER: int = int(os.getenv("RATE_LIMIT_USER", "100")) RATE_LIMIT_USER: int = int(os.getenv("RATE_LIMIT_USER", "100"))
# ====================== LOCAL DEV / TESTING TOGGLES ======================
# Turn off auth + rate limit middleware when true (for local testing)
DISABLE_AUTH: bool = os.getenv("DISABLE_AUTH", "true").lower() == "true"
# Use SQLite for chat history instead of Postgres when true
USE_SQLITE_HISTORY: bool = os.getenv("USE_SQLITE_HISTORY", "true").lower() == "true"
SQLITE_DB_PATH: str = os.getenv(
"SQLITE_DB_PATH",
os.path.join(os.path.dirname(__file__), "data", "chat_history.db"),
)
# Memo DB for note backend
MEMO_DB_PATH: str = os.getenv(
"MEMO_DB_PATH",
os.path.join(os.path.dirname(__file__), "db", "memos.db"),
)
"""
Script tự động thêm context (tên bảng + subsection) vào tất cả size entries trong tonghop.txt
Ví dụ: "Size 92 (2Y):" -> "Size 92 (2Y) - BẢNG SIZE CHUNG CHO UNISEX - TRẺ EM (Dải size lẻ):"
"""
import re
def add_context_to_sizes(input_file, output_file):
with open(input_file, encoding="utf-8") as f:
lines = f.readlines()
result = []
current_table = None # Tên bảng hiện tại
current_subsection = None # Subsection hiện tại (Dải size lẻ, chẵn...)
for line in lines:
stripped = line.strip()
# Phát hiện header bảng (bắt đầu bằng BẢNG hoặc QUẦN)
if stripped.startswith("BẢNG SIZE") or stripped.startswith("QUẦN"):
current_table = stripped
current_subsection = None # Reset subsection khi sang bảng mới
result.append(line)
continue
# Phát hiện subsection (Dải size lẻ, Dải size chẵn)
if "Dải size" in stripped or stripped.startswith("Dải"):
current_subsection = stripped.rstrip(":")
result.append(line)
continue
# Phát hiện dòng Size (bắt mọi pattern: Size XS:, Size 92 (2Y):, Size 26 (XS):)
size_match = re.match(r"^(Size\s+[A-Z0-9]+(?:\s*\([^)]+\))?):(.*)$", stripped)
if size_match and current_table:
size_part = size_match.group(1) # "Size 92 (2Y)" hoặc "Size XS"
rest = size_match.group(2) # Phần còn lại sau dấu :
# Xây dựng context
context_parts = [current_table]
if current_subsection:
context_parts.append(f"({current_subsection})")
context = " - ".join(context_parts)
# Tạo dòng mới với context
new_line = f"{size_part} - {context}:{rest}\n"
result.append(new_line)
continue
# Giữ nguyên các dòng khác
result.append(line)
# Ghi file output
with open(output_file, "w", encoding="utf-8") as f:
f.writelines(result)
print("✅ Đã thêm context vào tất cả size entries!")
print(f"📝 File output: {output_file}")
if __name__ == "__main__":
input_path = r"d:\cnf\chatbot_canifa\backend\datadb\tonghop.txt"
output_path = r"d:\cnf\chatbot_canifa\backend\datadb\tonghop_with_context.txt"
add_context_to_sizes(input_path, output_path)
print("\n🔍 Preview 10 dòng đầu của file mới:")
with open(output_path, encoding="utf-8") as f:
for i, line in enumerate(f):
if i >= 1160 and i < 1170: # Vùng có size entries
print(line.rstrip())
import os
import json
import pymysql
from openai import OpenAI
import time
# ==========================================
# 🔐 HARD KEY CONFIGURATION (As requested)
# ==========================================
OPENAI_API_KEY = "sk-proj-srJ3l3B5q1CzRezXAnaewbbRfuWzIjYHbcAdggzsa4MmtXEHaIwS1OTkMgLpMDikgh"
SR_HOST = "172.16.2.100"
SR_PORT = 9030
SR_USER = "anhvh"
SR_PASS = "v0WYGeyLRCckXotT"
SR_DB = "shared_source"
# Parameter
CHUNK_SIZE = 500
CHUNK_OVERLAP = 50
EMBEDDING_MODEL = "text-embedding-3-small" # 1536 dimensions
client = OpenAI(api_key=OPENAI_API_KEY)
def get_embedding(text):
"""Lấy vector 1536 chiều từ OpenAI"""
try:
text = text.replace("\n", " ")
return client.embeddings.create(input=[text], model=EMBEDDING_MODEL).data[0].embedding
except Exception as e:
print(f"❌ Lỗi Embedding: {e}")
return None
def connect_starrocks():
return pymysql.connect(
host=SR_HOST,
port=SR_PORT,
user=SR_USER,
password=SR_PASS,
database=SR_DB,
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
def chunk_text(text, size=CHUNK_SIZE, overlap=CHUNK_OVERLAP):
"""Chia nhỏ văn bản với overlap"""
chunks = []
start = 0
while start < len(text):
end = start + size
chunks.append(text[start:end])
start += size - overlap
return chunks
def ingest():
input_file = r"d:\cnf\chatbot_canifa\backend\datadb\tonghop.txt"
if not os.path.exists(input_file):
print(f"❌ Không tìm thấy file: {input_file}")
return
print(f"📖 Đang đọc file {input_file}...")
with open(input_file, "r", encoding="utf-8") as f:
full_content = f.read()
# Tách dữ liệu theo từng FILE giả định trong tonghop.txt
sections = full_content.split("================================================================================")
db = connect_starrocks()
cursor = db.cursor()
total_chunks = 0
record_id = int(time.time()) # Làm ID cơ bản
for section in sections:
if not section.strip(): continue
# Lấy tiêu đề file nếu có
lines = section.strip().split("\n")
title = "Canifa Knowledge"
if "FILE:" in lines[0]:
title = lines[0].replace("FILE:", "").strip()
content = "\n".join(lines[1:])
else:
content = section
print(f"🚀 Đang xử lý section: {title}")
chunks = chunk_text(content)
for i, chunk in enumerate(chunks):
if len(chunk.strip()) < 20: continue # Bỏ qua đoạn quá ngắn
vector = get_embedding(chunk)
if not vector: continue
metadata = {
"title": title,
"chunk_idx": i,
"source": "tonghop.txt",
"timestamp": time.time()
}
sql = "INSERT INTO shared_source.canifa_knowledge (id, content, metadata, embedding) VALUES (%s, %s, %s, %s)"
try:
cursor.execute(sql, (record_id, chunk, json.dumps(metadata, ensure_ascii=False), str(vector)))
record_id += 1
total_chunks += 1
if total_chunks % 10 == 0:
db.commit()
print(f"✅ Đã nạp {total_chunks} chunks...")
except Exception as e:
print(f"❌ Lỗi SQL: {e}")
db.commit()
db.close()
print(f"🎊 HOÀN THÀNH! Tổng cộng đã nạp {total_chunks} vào StarRocks.")
if __name__ == "__main__":
ingest()
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
import os
from typing import Any
import aiosqlite
from config import MEMO_DB_PATH
DB_PATH = MEMO_DB_PATH
async def init_memo_db():
"""Ensure memo database and tables exist."""
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"""
CREATE TABLE IF NOT EXISTS memos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
visibility TEXT NOT NULL DEFAULT 'PRIVATE',
tags_json TEXT NOT NULL DEFAULT '[]',
creator_id INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (DATETIME('now')),
updated_at TEXT NOT NULL DEFAULT (DATETIME('now'))
)
"""
)
await db.execute(
"""
CREATE TABLE IF NOT EXISTS memo_embeddings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memo_id INTEGER NOT NULL,
content TEXT,
tags_json TEXT NOT NULL DEFAULT '[]',
date_key TEXT,
embedding_json TEXT NOT NULL,
dim INTEGER NOT NULL,
model TEXT DEFAULT 'stub-embedding',
created_at TEXT NOT NULL DEFAULT (DATETIME('now')),
updated_at TEXT NOT NULL DEFAULT (DATETIME('now'))
)
"""
)
await db.execute(
"CREATE INDEX IF NOT EXISTS idx_memo_embeddings_memo_id ON memo_embeddings (memo_id)"
)
await db.execute(
"CREATE INDEX IF NOT EXISTS idx_memo_embeddings_date_key ON memo_embeddings (date_key)"
)
await db.commit()
async def execute(query: str, params: tuple[Any, ...] = ()) -> int:
"""Execute write query and return lastrowid if available."""
async with aiosqlite.connect(DB_PATH) as db:
cursor = await db.execute(query, params)
await db.commit()
return cursor.lastrowid or 0
async def fetch_one(query: str, params: tuple[Any, ...] = ()) -> dict[str, Any] | None:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute(query, params) as cursor:
row = await cursor.fetchone()
return dict(row) if row else None
async def fetch_all(query: str, params: tuple[Any, ...] = ()) -> list[dict[str, Any]]:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute(query, params) as cursor:
rows = await cursor.fetchall()
return [dict(r) for r in rows]
...@@ -111,6 +111,26 @@ class MemoResponse(MemoBase): ...@@ -111,6 +111,26 @@ class MemoResponse(MemoBase):
creator_id: int creator_id: int
class MemoEmbeddingCreate(BaseModel):
memoId: int
content: str
tags: List[str] = []
dateKey: Optional[str] = None
embedding: List[float]
model: Optional[str] = "stub-embedding"
class MemoEmbeddingResponse(BaseModel):
id: int
memoId: int
content: str
tags: List[str]
dateKey: Optional[str] = None
dim: int
model: str
score: Optional[float] = None
class AttachmentResponse(BaseModel): class AttachmentResponse(BaseModel):
id: int id: int
filename: str filename: str
......
This diff is collapsed.
"""
Migration script: create SQLite memos DB for local/dev.
"""
import asyncio
import os
import sys
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parent.parent
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from memos_core.db import DB_PATH, init_memo_db
async def main():
print(f"Initializing memo DB at: {DB_PATH}")
await init_memo_db()
if os.path.exists(DB_PATH):
size = os.path.getsize(DB_PATH)
print(f"Done. DB file created ({size} bytes).")
else:
print("Warning: DB file not found after init.")
if __name__ == "__main__":
# Ensure parent dir exists
Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True)
asyncio.run(main())
aiofiles==25.1.0 aiofiles==25.1.0
aiosqlite==0.20.0
aiomysql==0.3.2 aiomysql==0.3.2
email-validator==2.2.0
annotated-doc==0.0.4 annotated-doc==0.0.4
annotated-types==0.7.0 annotated-types==0.7.0
anyio==4.12.0 anyio==4.12.0
...@@ -93,6 +95,7 @@ PyMySQL==1.1.2 ...@@ -93,6 +95,7 @@ PyMySQL==1.1.2
pyscn==1.5.5 pyscn==1.5.5
pytest==9.0.2 pytest==9.0.2
python-dotenv==1.2.1 python-dotenv==1.2.1
python-multipart==0.0.20
python-engineio==4.12.3 python-engineio==4.12.3
python-socketio==5.15.1 python-socketio==5.15.1
PyYAML==6.0.3 PyYAML==6.0.3
......
import asyncio import asyncio
import os import os
import platform import platform
if platform.system() == "Windows":
print("🔧 Windows detected: Applying SelectorEventLoopPolicy globally...")
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
import logging import logging
import uvicorn import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse
from api.chatbot_route import router as chatbot_router from api.chatbot_route import router as chatbot_router
from api.conservation_route import router as conservation_router from api.conservation_route import router as conservation_router
...@@ -20,6 +16,13 @@ from common.cache import redis_cache ...@@ -20,6 +16,13 @@ from common.cache import redis_cache
from common.langfuse_client import get_langfuse_client from common.langfuse_client import get_langfuse_client
from common.middleware import middleware_manager from common.middleware import middleware_manager
from config import PORT from config import PORT
from memos_core.db import init_memo_db
# Windows event loop handling
if platform.system() == "Windows":
print("Windows detected: Applying SelectorEventLoopPolicy globally...")
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# Configure Logging # Configure Logging
logging.basicConfig( logging.basicConfig(
...@@ -27,14 +30,13 @@ logging.basicConfig( ...@@ -27,14 +30,13 @@ logging.basicConfig(
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=[logging.StreamHandler()], handlers=[logging.StreamHandler()],
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
langfuse_client = get_langfuse_client() langfuse_client = get_langfuse_client()
if langfuse_client: if langfuse_client:
logger.info("Langfuse client ready (lazy loading)") logger.info("Langfuse client ready (lazy loading)")
else: else:
logger.warning("⚠️ Langfuse client not available (missing keys or disabled)") logger.warning("Langfuse client not available (missing keys or disabled)")
app = FastAPI( app = FastAPI(
title="Contract AI Service", title="Contract AI Service",
...@@ -44,13 +46,15 @@ app = FastAPI( ...@@ -44,13 +46,15 @@ app = FastAPI(
# ============================================================================= # =============================================================================
# STARTUP EVENT - Initialize Redis Cache # STARTUP EVENT - Initialize Redis Cache + Memo DB
# ============================================================================= # =============================================================================
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
"""Initialize Redis cache on startup.""" """Initialize dependencies on startup."""
await redis_cache.initialize() await redis_cache.initialize()
logger.info("✅ Redis cache initialized for message limit") logger.info("Redis cache initialized for message limit")
await init_memo_db()
logger.info("Memo SQLite DB initialized")
# ============================================================================= # =============================================================================
...@@ -58,10 +62,10 @@ async def startup_event(): ...@@ -58,10 +62,10 @@ async def startup_event():
# ============================================================================= # =============================================================================
middleware_manager.setup( middleware_manager.setup(
app, app,
enable_auth=True, # 👈 Bật lại Auth để test logic Guest/User enable_auth=True, # bật/tắt Auth
enable_rate_limit=False, # 👈 Tắt slowapi vì đã có business rate limit enable_rate_limit=False, # tắt slowapi business rate limit
enable_cors=True, # 👈 Bật CORS enable_cors=True, # bật CORS
cors_origins=["*"], # 👈 Trong production nên limit origins cors_origins=["*"], # trong prod nên giới hạn origins
) )
app.include_router(conservation_router) app.include_router(conservation_router)
...@@ -75,25 +79,21 @@ try: ...@@ -75,25 +79,21 @@ try:
from api.mock_api_route import router as mock_router from api.mock_api_route import router as mock_router
app.include_router(mock_router) app.include_router(mock_router)
print("Mock API Router mounted at /mock") print("Mock API Router mounted at /mock")
except ImportError: except ImportError:
print("⚠️ Mock Router not found, skipping...") print("Mock Router not found, skipping...")
# ========================================== # ==========================================
# 🟢 ĐOẠN MOUNT STATIC HTML CỦA BRO ĐÂY 🟢 # Mount static HTML
# ========================================== # ==========================================
try: try:
static_dir = os.path.join(os.path.dirname(__file__), "static") static_dir = os.path.join(os.path.dirname(__file__), "static")
if not os.path.exists(static_dir): if not os.path.exists(static_dir):
os.makedirs(static_dir) os.makedirs(static_dir)
# Mount thư mục static để chạy file index.html
app.mount("/static", StaticFiles(directory=static_dir, html=True), name="static") app.mount("/static", StaticFiles(directory=static_dir, html=True), name="static")
print(f"Static files mounted at /static (Dir: {static_dir})") print(f"Static files mounted at /static (Dir: {static_dir})")
except Exception as e: except Exception as e:
print(f"⚠️ Failed to mount static files: {e}") print(f"Failed to mount static files: {e}")
from fastapi.responses import RedirectResponse
@app.get("/") @app.get("/")
...@@ -103,15 +103,15 @@ async def root(): ...@@ -103,15 +103,15 @@ async def root():
if __name__ == "__main__": if __name__ == "__main__":
print("=" * 60) print("=" * 60)
print("🚀 Contract AI Service Starting...") print("Contract AI Service Starting...")
print("=" * 60) print("=" * 60)
print(f"📡 REST API: http://localhost:{PORT}") print(f"REST API: http://localhost:{PORT}")
print(f"📡 Test Chatbot: http://localhost:{PORT}/static/index.html") print(f"Test Chatbot: http://localhost:{PORT}/static/index.html")
print(f"📚 API Docs: http://localhost:{PORT}/docs") print(f"API Docs: http://localhost:{PORT}/docs")
print("=" * 60) print("=" * 60)
ENABLE_RELOAD = False ENABLE_RELOAD = False
print(f"⚠️ Hot reload: {ENABLE_RELOAD}") print(f"Hot reload: {ENABLE_RELOAD}")
reload_dirs = ["common", "api", "agent"] reload_dirs = ["common", "api", "agent"]
......
This diff is collapsed.
"""Test message limit - Guest limit = 3""" """Test message limit - guest limit = 3."""
import os
import requests import requests
DEVICE_ID = "limit-test-002" DEVICE_ID = "limit-test-002"
API_URL = "http://localhost:5000/api/agent/chat" BASE_URL = os.getenv("BASE_URL", "http://localhost:5000")
API_URL = f"{BASE_URL}/api/agent/chat"
def safe_text(value: str | None) -> str:
if value is None:
return ""
return str(value).encode("ascii", "backslashreplace").decode("ascii")
print("=" * 50) print("=" * 50)
print("TEST MESSAGE LIMIT (Guest = 3 tin/ngày)") print("TEST MESSAGE LIMIT (Guest = 3 per day)")
print("=" * 50) print("=" * 50)
print(f"Device ID: {DEVICE_ID}") print(f"Device ID: {DEVICE_ID}")
print() print()
for i in range(5): # Gửi 5 tin để thấy bị chặn for i in range(5):
print(f"--- Tin nhắn #{i+1} ---") print(f"--- Message #{i + 1} ---")
response = requests.post( response = requests.post(
API_URL, API_URL,
json={"user_query": f"test message {i+1}"}, json={"user_query": f"test message {i + 1}"},
headers={"device_id": DEVICE_ID} headers={"device_id": DEVICE_ID},
) )
try:
data = response.json() data = response.json()
except ValueError:
data = {}
if data.get("status") == "success": if response.status_code != 200:
print(f"HTTP {response.status_code}")
print(f" Body: {safe_text(response.text)}")
elif data.get("status") == "success":
limit_info = data.get("limit_info", {}) limit_info = data.get("limit_info", {})
print(f"✅ Thành công!") used = limit_info.get("used")
print(f" Used: {limit_info.get('used')}/{limit_info.get('limit')}") limit = limit_info.get("limit")
print(f" Remaining: {limit_info.get('remaining')}") remaining = limit_info.get("remaining")
print("OK: allowed")
print(f" Used: {used}/{limit}")
print(f" Remaining: {remaining}")
else: else:
print(f"❌ Bị chặn!") print("BLOCKED")
print(f" Error: {data.get('error_code')}") print(f" Error: {safe_text(data.get('error_code'))}")
print(f" Message: {data.get('message')}") print(f" Message: {safe_text(data.get('message'))}")
print(f" Require login: {data.get('require_login')}") print(f" Require login: {data.get('require_login')}")
limit_info = data.get("limit_info", {}) limit_info = data.get("limit_info", {})
if limit_info: if limit_info:
print(f" Used: {limit_info.get('used')}/{limit_info.get('limit')}") print(f" Used: {limit_info.get('used')}/{limit_info.get('limit')}")
if not data:
print(f" Body: {safe_text(response.text)}")
print() print()
print("=" * 50) print("=" * 50)
print("TEST HOÀN TẤT!") print("TEST COMPLETE")
This diff is collapsed.
This diff is collapsed.
import os
import requests import requests
import json
BASE_URL = "http://localhost:5000" BASE_URL = os.getenv("BASE_URL", "http://localhost:5000")
API_URL = f"{BASE_URL}/api/agent/system-prompt" API_URL = f"{BASE_URL}/api/agent/system-prompt"
# 1. Get current prompt
print("1. Getting current prompt...") def safe_text(value: str | None) -> str:
if not value:
return ""
return value.encode("ascii", "backslashreplace").decode("ascii")
print("1) GET current prompt")
try: try:
response = requests.get(API_URL) response = requests.get(API_URL)
if response.status_code == 200: if response.status_code == 200:
print("✅ Current prompt fetched successfully.") content = response.json().get("content", "")
print(f"Preview: {response.json()['content'][:100]}...") print("OK: fetched current prompt")
print(f"Length: {len(content)} chars")
else: else:
print(f"❌ Failed to get prompt: {response.status_code} - {response.text}") print(f"ERROR: status={response.status_code}")
except Exception as e: print(f"Body={safe_text(response.text)}")
print(f"❌ Error connecting: {e}") except Exception as exc:
print(f"ERROR: request failed: {exc}")
# 2. Update prompt new_prompt = (
new_prompt = """# VAI TRÒ "You are Doraemon from the 22nd century. "
Bạn là Mèo Máy Doraemon đến từ thế kỷ 22. "End every sentence with 'meo meo'."
Luôn kết thúc câu bằng "meo meo". )
"""
print("\n2. Updating prompt to Doraemon...") print("\n2) POST update prompt")
try: try:
response = requests.post(API_URL, json={"content": new_prompt}) response = requests.post(API_URL, json={"content": new_prompt})
if response.status_code == 200: if response.status_code == 200:
print("✅ Prompt updated successfully.") print("OK: prompt updated")
print(response.json())
else: else:
print(f"❌ Failed to update prompt: {response.status_code} - {response.text}") print(f"ERROR: status={response.status_code}")
except Exception as e: print(f"Body={safe_text(response.text)}")
print(f"❌ Error connecting: {e}") except Exception as exc:
print(f"ERROR: request failed: {exc}")
# 3. Verify update print("\n3) GET verify prompt")
print("\n3. Verifying update...")
try: try:
response = requests.get(API_URL) response = requests.get(API_URL)
content = response.json()['content'] if response.status_code == 200:
content = response.json().get("content", "")
if "Doraemon" in content: if "Doraemon" in content:
print("✅ Prompt content verified: Doraemon is here!") print("OK: prompt contains Doraemon")
else:
print("ERROR: prompt not updated")
else: else:
print("❌ Prompt content NOT updated.") print(f"ERROR: status={response.status_code}")
except Exception as e: print(f"Body={safe_text(response.text)}")
print(f"❌ Error connecting: {e}") except Exception as exc:
print(f"ERROR: request failed: {exc}")
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -2,14 +2,12 @@ import { useEffect } from "react"; ...@@ -2,14 +2,12 @@ import { useEffect } from "react";
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import { useInstance } from "./contexts/InstanceContext"; import { useInstance } from "./contexts/InstanceContext";
import { MemoFilterProvider } from "./contexts/MemoFilterContext"; import { MemoFilterProvider } from "./contexts/MemoFilterContext";
import useNavigateTo from "./hooks/useNavigateTo";
import { useUserLocale } from "./hooks/useUserLocale"; import { useUserLocale } from "./hooks/useUserLocale";
import { useUserTheme } from "./hooks/useUserTheme"; import { useUserTheme } from "./hooks/useUserTheme";
import { cleanupExpiredOAuthState } from "./utils/oauth"; import { cleanupExpiredOAuthState } from "./utils/oauth";
const App = () => { const App = () => {
const navigateTo = useNavigateTo(); const { generalSetting: instanceGeneralSetting } = useInstance();
const { profile: instanceProfile, generalSetting: instanceGeneralSetting } = useInstance();
// Apply user preferences reactively // Apply user preferences reactively
useUserLocale(); useUserLocale();
...@@ -20,12 +18,7 @@ const App = () => { ...@@ -20,12 +18,7 @@ const App = () => {
cleanupExpiredOAuthState(); cleanupExpiredOAuthState();
}, []); }, []);
// Redirect to sign up page if no instance owner // Auth is disabled for now; skip signup redirect.
useEffect(() => {
if (!instanceProfile.owner) {
navigateTo("/auth/signup");
}
}, [instanceProfile.owner, navigateTo]);
useEffect(() => { useEffect(() => {
if (instanceGeneralSetting.additionalStyle) { if (instanceGeneralSetting.additionalStyle) {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment