# ================================================================
# URAL AI - MEGA MODULE v1.0
# File: ural_mega_module.py
#
# GỒM TẤT CẢ TÍNH NĂNG CÒN THIẾU — 1 FILE DUY NHẤT
# HOÀN TOÀN FREE, KHÔNG KEY, KHÔNG PHÍ ẨN
# NẰM TRONG GIỚI HẠN HF FREE TIER
#
# CÁCH DÙNG: Thêm vào app.py (sau các import ural khác):
# import ural_mega_module
#
# CÁC MODULE TRONG FILE NÀY:
# [1] Tin tức realtime — RSS feeds, không cần key
# [2] Giá crypto — CoinGecko API, free không cần key
# [3] Giá vàng/USD/VND — Tự scrape từ web
# [4] Thời tiết chi tiết — wttr.in, free không cần key
# [5] QR Code tạo — qrcode library (nhẹ, offline)
# [6] Nhật ký cảm xúc — SQLite đã có trong app
# [7] Đổi đơn vị — Logic thuần, không cần mạng
# [8] Tra từ điển VN — Free dictionary API
# [9] Tóm tắt URL tốt hơn — Jina.ai đã có, hook tốt hơn
# [10] Câu đố / Trò chơi — Logic thuần, nâng cấp quiz_game cũ
# ================================================================
import os, sys, re, json, random, time, datetime, requests
from typing import Optional
# ── [FIX] Đường dẫn DB persistent — TRƯỚC ĐÂY dùng "ural_memory.db" (path
# tương đối) → mất sạch dữ liệu mỗi khi HF Space restart/redeploy vì thư mục
# làm việc không persistent. GIỜ ưu tiên /data (persistent storage của HF),
# fallback về thư mục hiện tại hoặc /tmp nếu /data không truy cập được
# (vd: chạy local dev không có /data).
def _journal_db_path() -> str:
if os.path.exists("/data") and os.path.isdir("/data") and os.access("/data", os.W_OK):
return "/data/ural_memory.db"
return "./ural_memory.db" if os.access(".", os.W_OK) else "/tmp/ural_memory.db"
_URAL_DB_PATH = _journal_db_path()
# ================================================================
# MODULE 1: TIN TỨC REALTIME — RSS Feeds
# Không cần key, cực nhẹ, ổn định
# ================================================================
RSS_SOURCES = {
"vnexpress": "https://vnexpress.net/rss/tin-moi-nhat.rss",
"tuoitre": "https://tuoitre.vn/rss/tin-moi-nhat.rss",
"thanhnien": "https://thanhnien.vn/rss/home.rss",
"dantri": "https://dantri.com.vn/rss/home.rss",
"the_gioi": "https://vnexpress.net/rss/the-gioi.rss",
"cong_nghe": "https://vnexpress.net/rss/khoa-hoc-cong-nghe.rss",
"kinh_doanh": "https://vnexpress.net/rss/kinh-doanh.rss",
"giai_tri": "https://vnexpress.net/rss/giai-tri.rss",
"the_thao": "https://vnexpress.net/rss/the-thao.rss",
}
NEWS_KEYWORDS = {
"thế giới": "the_gioi",
"quốc tế": "the_gioi",
"công nghệ": "cong_nghe",
"tech": "cong_nghe",
"kinh doanh": "kinh_doanh",
"kinh tế": "kinh_doanh",
"giải trí": "giai_tri",
"showbiz": "giai_tri",
"thể thao": "the_thao",
"bóng đá": "the_thao",
}
_news_cache = {} # {source: (timestamp, articles)}
NEWS_CACHE_TTL = 300 # 5 phút
def get_news(category: str = "", num: int = 5) -> str:
"""Lấy tin tức realtime — ưu tiên brain_memory cache từ RSS-Watcher."""
import datetime as _dt_gn
# ── BƯỚC 1: Dùng brain_memory cache từ RSS-Watcher (đã có sẵn!) ──────
# RSS-Watcher chạy mỗi 15 phút, lưu vào brain_memory[_rss_hot_YYYY-MM-DD]
# Đây là nguồn dữ liệu đúng, không cần fetch lại
try:
today_key = f"_rss_hot_{_dt_gn.date.today()}"
rss_cached = brain_memory.get(today_key, "")
if rss_cached and len(rss_cached) > 50:
lines = [l.strip() for l in rss_cached.strip().split("\n") if l.strip()]
# Lấy num tin đầu (mỗi tin = 1 dòng "• [SOURCE] title")
news_lines = [l for l in lines if l.startswith("•")][:num]
if news_lines:
cat_label = category if category else "mới nhất"
ts = _dt_gn.datetime.now().strftime("%H:%M %d/%m")
result = [f"📰 **Tin tức {cat_label}** ({ts}):\n"]
for i, line in enumerate(news_lines, 1):
# Format: • [VNEXPRESS] Tiêu đề tin → **1. Tiêu đề tin**
clean = re.sub(r"^•\s*\[\w+\]\s*", "", line).strip()
result.append(f"**{i}. {clean}**")
result.append("")
return "\n".join(result)
except Exception as _e_cache:
pass # Fallback xuống fetch trực tiếp
# ── BƯỚC 2: Fallback - fetch RSS trực tiếp ───────────────────────────
source_key = "vnexpress"
cat_lower = category.lower()
for kw, src in NEWS_KEYWORDS.items():
if kw in cat_lower:
source_key = src
break
url = RSS_SOURCES.get(source_key, RSS_SOURCES["vnexpress"])
cached = _news_cache.get(source_key)
if cached and (time.time() - cached[0]) < NEWS_CACHE_TTL:
articles = cached[1]
else:
try:
resp = requests.get(url, timeout=10, headers={
"User-Agent": "Mozilla/5.0 (compatible; URAL-AI/1.0)"
})
resp.encoding = "utf-8"
raw = resp.text
# Parse nhiều format RSS khác nhau
titles = (re.findall(r'
', raw, re.DOTALL)
or re.findall(r'(.*?)', raw, re.DOTALL))
descs = (re.findall(r'', raw, re.DOTALL)
or re.findall(r'(.*?)', raw, re.DOTALL))
links = re.findall(r'(https?://[^<]+)', raw)
if len(titles) > 1:
titles = titles[1:] # Bỏ title của feed
articles = []
for i in range(min(num + 2, len(titles))):
title = re.sub(r'<[^>]+>', '', titles[i]).strip() if i < len(titles) else ""
desc = re.sub(r'<[^>]+>', '', descs[i+1] if i+1 < len(descs) else "").strip()[:200]
link = links[i] if i < len(links) else ""
if title and len(title) > 5:
articles.append({"title": title, "desc": desc, "link": link})
_news_cache[source_key] = (time.time(), articles)
except Exception as e:
return f"⚠️ Không lấy được tin tức: {e}"
if not articles:
return "⚠️ Chưa có tin tức trong cache. Thử lại sau 1 phút."
cat_label = category if category else "mới nhất"
ts = _dt_gn.datetime.now().strftime("%H:%M %d/%m")
result = [f"📰 **Tin tức {cat_label}** ({ts}):\n"]
for i, art in enumerate(articles[:num]):
result.append(f"**{i+1}. {art['title']}**")
if art['desc']:
result.append(f" {art['desc']}...")
if art['link']:
result.append(f" 🔗 {art['link']}")
result.append("")
return "\n".join(result)
# ================================================================
# MODULE 2: GIÁ CRYPTO — CoinGecko (free, không cần key)
# ================================================================
CRYPTO_IDS = {
"bitcoin": "bitcoin", "btc": "bitcoin",
"ethereum": "ethereum", "eth": "ethereum",
"bnb": "binancecoin", "binance": "binancecoin",
"xrp": "ripple", "ripple": "ripple",
"solana": "solana", "sol": "solana",
"doge": "dogecoin", "dogecoin": "dogecoin",
"usdt": "tether", "tether": "tether",
"ada": "cardano", "cardano": "cardano",
"ton": "the-open-network",
"not": "notcoin",
"pepe": "pepe",
}
_crypto_cache = {} # {coin_id: (ts, data)}
CRYPTO_CACHE_TTL = 60 # 1 phút
def get_crypto_price(coins: list = None) -> str:
"""Giá crypto realtime từ CoinGecko API — miễn phí, không cần key."""
if not coins:
coins = ["bitcoin", "ethereum", "binancecoin", "solana", "ripple"]
coin_ids = []
coin_names = {}
for c in coins:
cid = CRYPTO_IDS.get(c.lower(), c.lower())
coin_ids.append(cid)
coin_names[cid] = c.upper()
ids_str = ",".join(set(coin_ids))
url = (f"https://api.coingecko.com/api/v3/simple/price"
f"?ids={ids_str}&vs_currencies=usd,vnd"
f"&include_24hr_change=true&include_market_cap=false")
try:
resp = requests.get(url, timeout=8)
data = resp.json()
now = datetime.datetime.now().strftime("%H:%M %d/%m")
lines = [f"💰 **Giá Crypto** (cập nhật {now}):\n"]
for cid, info in data.items():
name = coin_names.get(cid, cid.upper())
usd = info.get("usd", 0)
vnd = info.get("vnd", 0)
chg = info.get("usd_24h_change", 0) or 0
arrow = "📈" if chg >= 0 else "📉"
sign = "+" if chg >= 0 else ""
usd_str = f"${usd:,.2f}" if usd < 1 else f"${usd:,.0f}"
vnd_str = f"{vnd/1_000_000:.1f}M VND" if vnd > 1_000_000 else f"{vnd:,.0f} VND"
lines.append(f"{arrow} **{name}**: {usd_str} ({vnd_str}) | 24h: {sign}{chg:.1f}%")
return "\n".join(lines)
except Exception as e:
return f"⚠️ Không lấy được giá crypto: {e}"
def detect_crypto_request(text: str) -> Optional[list]:
"""Trả về danh sách coin được hỏi, hoặc None nếu không phải câu hỏi crypto."""
t = text.lower()
crypto_triggers = [
"giá bitcoin", "giá btc", "giá eth", "giá ethereum",
"giá coin", "giá crypto", "giá bnb", "giá sol",
"crypto hôm nay", "coin hôm nay", "thị trường crypto",
"bitcoin bao nhiêu", "eth bao nhiêu", "btc bao nhiêu",
"giá doge", "giá xrp", "giá ada", "giá ton",
]
if not any(kw in t for kw in crypto_triggers):
return None
# Tìm coin cụ thể
found = []
for alias, cid in CRYPTO_IDS.items():
if alias in t and cid not in found:
found.append(cid)
return found if found else ["bitcoin", "ethereum", "binancecoin"]
# ================================================================
# MODULE 3: GIÁ VÀNG & NGOẠI TỆ
# ================================================================
_gold_cache = {}
GOLD_CACHE_TTL = 300 # 5 phút
def get_gold_price() -> str:
"""Giá vàng và tỷ giá USD/VND từ wttr.in và exchangerate.host."""
results = []
# Tỷ giá USD/VND
try:
resp = requests.get(
"https://api.exchangerate-api.com/v4/latest/USD",
timeout=5
)
data = resp.json()
vnd_rate = data["rates"].get("VND", 0)
if vnd_rate:
results.append(f"💵 **USD/VND**: {vnd_rate:,.0f} VND")
except Exception:
pass
# Giá vàng XAU/USD qua exchangerate
try:
resp = requests.get(
"https://api.metals.live/v1/spot/gold",
timeout=5
)
data = resp.json()
gold_usd = data[0].get("price", 0) if isinstance(data, list) else 0
if gold_usd:
gold_vnd = gold_usd * 24000 / 31.1035 # oz → chỉ (3.75g)
results.append(
f"🥇 **Vàng thế giới**: ${gold_usd:,.2f}/oz"
f" (~{gold_vnd/1_000_000:.1f}M VND/chỉ)"
)
except Exception:
pass
now = datetime.datetime.now().strftime("%H:%M %d/%m")
if results:
return f"💹 **Tỷ giá & Vàng** (cập nhật {now}):\n\n" + "\n".join(results)
return "⚠️ Không lấy được tỷ giá lúc này."
GOLD_KEYWORDS = [
"giá vàng", "vàng hôm nay", "vàng bao nhiêu",
"tỷ giá", "đô la", "usd hôm nay", "usd bao nhiêu",
"dollar", "usd/vnd", "vnd/usd",
]
def detect_gold_request(text: str) -> bool:
return any(kw in text.lower() for kw in GOLD_KEYWORDS)
# ================================================================
# MODULE 4: THỜI TIẾT CHI TIẾT — wttr.in (free, không cần key)
# ================================================================
# ================================================================
# 🆕 WEATHER v2 — Open-Meteo (100% free, không cần API key, không
# giới hạn ẩn, không thẻ tín dụng). Thay thế wttr.in cũ vì:
# - wttr.in hay timeout/instability, dữ liệu đôi khi cũ
# - Open-Meteo tổng hợp từ nhiều mô hình khí tượng (ECMWF, GFS,
# ICON...) → chính xác hơn, đúng như Gemini đã chỉ ra khi so
# sánh với nguồn NCHMF chính thống của VN.
# - Có UV index, mưa mm, gió, độ ẩm — đủ field như bản cũ.
# Docs: https://open-meteo.com/en/docs (free, unlimited cho non-
# commercial, không cần đăng ký/API key).
# ================================================================
_OM_GEOCODE_CACHE: dict = {}
def _om_geocode(location: str) -> Optional[dict]:
"""Tra cứu toạ độ (lat/lon) từ tên địa điểm — dùng Open-Meteo
Geocoding API (free, không key). Cache trong RAM để đỡ gọi lại."""
key = location.strip().lower()
if key in _OM_GEOCODE_CACHE:
return _OM_GEOCODE_CACHE[key]
try:
resp = requests.get(
"https://geocoding-api.open-meteo.com/v1/search",
params={"name": location, "count": 1, "language": "vi", "format": "json"},
timeout=8,
)
data = resp.json()
results = data.get("results") or []
if not results:
return None
r = results[0]
info = {
"lat": r.get("latitude"),
"lon": r.get("longitude"),
"name": r.get("name", location),
"admin1": r.get("admin1", ""),
"country": r.get("country", ""),
}
_OM_GEOCODE_CACHE[key] = info
return info
except Exception:
return None
# Mã thời tiết WMO (chuẩn Open-Meteo dùng) → mô tả tiếng Việt + emoji
_WMO_CODES = {
0: ("Trời quang", "☀️"), 1: ("Ít mây", "🌤️"), 2: ("Có mây", "⛅"),
3: ("Nhiều mây/âm u", "☁️"), 45: ("Sương mù", "🌫️"), 48: ("Sương mù đóng băng", "🌫️"),
51: ("Mưa phùn nhẹ", "🌦️"), 53: ("Mưa phùn", "🌦️"), 55: ("Mưa phùn dày", "🌧️"),
61: ("Mưa nhẹ", "🌦️"), 63: ("Mưa vừa", "🌧️"), 65: ("Mưa to", "🌧️"),
66: ("Mưa đá nhẹ", "🌨️"), 67: ("Mưa đá to", "🌨️"),
71: ("Tuyết nhẹ", "🌨️"), 73: ("Tuyết vừa", "🌨️"), 75: ("Tuyết to", "❄️"),
80: ("Mưa rào nhẹ", "🌦️"), 81: ("Mưa rào vừa", "🌧️"), 82: ("Mưa rào dữ dội", "⛈️"),
95: ("Dông", "⛈️"), 96: ("Dông kèm mưa đá nhẹ", "⛈️"), 99: ("Dông kèm mưa đá to", "⛈️"),
}
def get_weather_detail(location: str = "Ho Chi Minh City") -> str:
"""
Thời tiết chi tiết từ Open-Meteo — miễn phí 100%, không cần key,
không thẻ, không giới hạn ẩn. Chính xác hơn wttr.in (bản cũ),
dùng dữ liệu tổng hợp từ nhiều mô hình khí tượng (ECMWF/GFS/ICON).
"""
geo = _om_geocode(location)
if not geo:
return f"⚠️ Không tìm thấy địa điểm '{location}' — Boss thử ghi rõ hơn (VD: 'Đà Nẵng, Việt Nam') nhé."
try:
resp = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": geo["lat"], "longitude": geo["lon"],
"current": "temperature_2m,relative_humidity_2m,apparent_temperature,"
"precipitation,weather_code,wind_speed_10m,wind_gusts_10m,uv_index",
# 🆕 minutely_15: dữ liệu cập nhật mỗi 15 phút — trễ pha THẤP
# HƠN NHIỀU so với chỉ dùng "current" (nội suy theo giờ chẵn
# của mô hình, có thể lệch 30-60+ phút khi thời tiết biến
# động nhanh — VD mưa dông/bão đến đột ngột, đúng tình huống
# Gemini phát hiện: Yui báo "nhiều mây" trong khi thực tế
# đang mưa to do hoàn lưu bão). Lấy điểm dữ liệu 15-phút GẦN
# NHẤT với hiện tại làm nguồn chính, "current" chỉ dùng dự
# phòng nếu minutely_15 không trả về được.
"minutely_15": "temperature_2m,relative_humidity_2m,apparent_temperature,"
"precipitation,weather_code,wind_speed_10m,wind_gusts_10m",
"daily": "temperature_2m_max,temperature_2m_min,weather_code,uv_index_max,precipitation_sum",
"timezone": "auto",
"forecast_days": 2,
"forecast_minutely_15": 4, # chỉ cần vài điểm gần nhất, đỡ payload
},
timeout=8,
)
data = resp.json()
daily = data["daily"]
# 🆕 Ưu tiên điểm minutely_15 GẦN THỜI ĐIỂM HIỆN TẠI NHẤT (index 0
# của mảng minutely_15 luôn là mốc 15-phút gần nhất đã qua/đang tới,
# trễ tối đa ~15 phút thay vì tối đa ~60 phút như "current" mặc định)
cur_source = "minutely_15"
m15 = data.get("minutely_15")
if m15 and m15.get("temperature_2m") and m15["temperature_2m"][0] is not None:
cur = {
"temperature_2m": m15["temperature_2m"][0],
"relative_humidity_2m": m15["relative_humidity_2m"][0],
"apparent_temperature": m15["apparent_temperature"][0],
"precipitation": m15.get("precipitation", [0])[0] or 0,
"weather_code": m15["weather_code"][0],
"wind_speed_10m": m15["wind_speed_10m"][0],
"wind_gusts_10m": (m15.get("wind_gusts_10m") or [None])[0],
"uv_index": data.get("current", {}).get("uv_index"), # UV không có ở minutely_15
}
else:
cur_source = "current (fallback — minutely_15 không khả dụng)"
cur = data["current"]
cur.setdefault("wind_gusts_10m", None)
temp_c = cur["temperature_2m"]
feels_c = cur["apparent_temperature"]
humidity = cur["relative_humidity_2m"]
wind_kmh = cur["wind_speed_10m"]
gust_kmh = cur.get("wind_gusts_10m")
precip = cur.get("precipitation", 0) or 0
uv = cur.get("uv_index")
wcode = cur["weather_code"]
desc, emoji = _WMO_CODES.get(wcode, ("Không rõ", "🌡️"))
place_full = geo["name"]
if geo.get("admin1") and geo["admin1"] != geo["name"]:
place_full += f", {geo['admin1']}"
now_str = datetime.datetime.now().strftime("%H:%M %d/%m")
result = [
f"{emoji} **Thời tiết {place_full}** ({now_str}):\n",
f"🌡️ Nhiệt độ hiện tại: **{temp_c}°C** (cảm giác {feels_c}°C)",
f"☁️ Tình trạng: **{desc}**",
f"💧 Độ ẩm: {humidity}% | 💨 Gió: {wind_kmh} km/h" + (f" (giật {gust_kmh} km/h)" if gust_kmh else ""),
]
if precip and precip > 0:
result.append(f"🌧️ Lượng mưa: {precip}mm")
if uv is not None:
result.append(f"☀️ UV index: {uv}")
try:
tmax_today = daily["temperature_2m_max"][0]
tmin_today = daily["temperature_2m_min"][0]
psum_today = (daily.get("precipitation_sum") or [None])[0]
today_line = f"\n📅 Hôm nay: {tmin_today}°C – {tmax_today}°C"
if psum_today:
today_line += f" | 🌧️ mưa dự kiến {psum_today}mm"
result.append(today_line)
if len(daily["temperature_2m_max"]) > 1:
tmax_tmr = daily["temperature_2m_max"][1]
tmin_tmr = daily["temperature_2m_min"][1]
result.append(f"📅 Ngày mai: {tmin_tmr}°C – {tmax_tmr}°C")
except (KeyError, IndexError):
pass
# 🆕 Cảnh báo PHÂN CẤP theo mức độ nghiêm trọng thật của weather_code
# — trước đây MỌI wcode>=51 (từ mưa phùn nhẹ đến dông bão dữ dội)
# đều chỉ nhận 1 câu nhẹ "nhớ mang dù", nên khi có dông/bão thật
# (wcode 95-99) hoặc mưa rào dữ dội (82), Yui vẫn nói nhẹ như mưa
# phùn bình thường — đúng lỗi Gemini phát hiện (báo "nhiều mây"
# nhẹ nhàng trong khi thực tế đang bão). Giờ tách rõ 3 cấp.
if temp_c >= 35:
result.append("\n⚠️ *Trời nóng — nhớ uống nước nhiều nha!*")
elif temp_c <= 20:
result.append("\n🧥 *Trời lạnh — mặc thêm áo vào nha!*")
if wcode in (95, 96, 99) or (gust_kmh and gust_kmh >= 50):
result.append("\n🚨 **CẢNH BÁO: Dông/bão — gió giật mạnh, sấm chớp. Boss hạn chế ra ngoài, tránh trú dưới cây to/cột điện!**")
elif wcode in (65, 82) or (precip and precip >= 15):
result.append("\n⛈️ *Mưa to/mưa rào dữ dội — nếu ra ngoài nhớ áo mưa, cẩn thận đường trơn trượt và ngập cục bộ!*")
elif wcode >= 51:
result.append("☂️ *Có mưa — nhớ mang dù nha!*")
if uv is not None and uv >= 8:
result.append("🕶️ *UV cao — che chắn kỹ khi ra ngoài nha!*")
result.append(f"\n_(Nguồn: Open-Meteo, dữ liệu {cur_source})_")
return "\n".join(result)
except Exception as e:
return f"⚠️ Không lấy được thời tiết: {e}"
WEATHER_KEYWORDS = [
"thời tiết", "trời hôm nay", "nhiệt độ",
"mưa không", "nắng không", "có mưa",
"weather", "dự báo thời tiết",
]
def detect_weather_request(text: str) -> Optional[str]:
"""Trả về địa điểm nếu là câu hỏi thời tiết, None nếu không."""
t = text.lower()
# Cũng trigger nếu tên thành phố + từ thời tiết xuất hiện
city_weather = any(city in t for city in [
"hà nội", "sài gòn", "hcm", "đà nẵng", "huế", "cần thơ",
"nha trang", "đà lạt", "vũng tàu", "hải phòng", "bình dương"
]) and any(kw in t for kw in ["hôm nay", "thế nào", "ra sao", "mưa", "nắng"])
if not any(kw in t for kw in WEATHER_KEYWORDS) and not city_weather:
return None
# Tìm địa điểm
loc_patterns = [
r'thời tiết\s+(?:ở\s+|tại\s+)?(.+?)(?:\s+hôm|$|\?|!)',
r'(?:ở|tại|tại\s+thành\s+phố)\s+(.+?)(?:\s+thời tiết|\s+mưa|\s+nắng|$)',
r'(.+?)\s+(?:thời tiết|trời hôm nay|có mưa|nắng không)',
]
for pat in loc_patterns:
m = re.search(pat, t)
if m:
loc = m.group(1).strip()
if 2 < len(loc) < 30:
# Map alias → canonical ngay tại đây
loc_map_inline = {
"hcm": "Ho Chi Minh City", "sài gòn": "Ho Chi Minh City",
"sg": "Ho Chi Minh City", "hồ chí minh": "Ho Chi Minh City",
"ho chi minh": "Ho Chi Minh City", "tphcm": "Ho Chi Minh City",
"hà nội": "Hanoi", "ha noi": "Hanoi", "hn": "Hanoi",
"đà nẵng": "Da Nang", "huế": "Hue", "cần thơ": "Can Tho",
"nha trang": "Nha Trang", "đà lạt": "Da Lat",
"vũng tàu": "Vung Tau", "hải phòng": "Hai Phong",
}
loc_lower2 = loc.lower()
for alias2, canonical2 in loc_map_inline.items():
if alias2 in loc_lower2:
return canonical2
return loc
return "Ho Chi Minh City" # Mặc định
# ================================================================
# MODULE 5: QR CODE — thư viện qrcode (offline, cực nhẹ)
# ================================================================
def generate_qr(content: str) -> str:
"""
Tạo QR code từ text/link.
Trả về base64 PNG để Gradio hiển thị, hoặc link QR online.
"""
if not content:
return "⚠️ Bạn muốn tạo QR cho nội dung gì?"
# Dùng API online (không cần install thêm gì)
encoded = requests.utils.quote(content)
qr_url = f"https://api.qrserver.com/v1/create-qr-code/?data={encoded}&size=300x300&ecc=M"
return (
f"📱 **QR Code đã tạo:**\n\n"
f"\n\n"
f"📋 **Nội dung:** `{content[:100]}`\n"
f"🔗 **Link ảnh:** {qr_url}\n\n"
f"*Dùng camera quét QR ở trên nhé!*"
)
QR_KEYWORDS = [
"tạo qr", "tạo mã qr", "qr code", "qrcode",
"mã qr cho", "qr cho link", "qr cho text",
"generate qr", "make qr",
]
def detect_qr_request(text: str) -> Optional[str]:
"""Trả về nội dung cần tạo QR, hoặc None."""
t = text.lower()
if not any(kw in t for kw in QR_KEYWORDS):
return None
# Tách nội dung
patterns = [
r'(?:tạo qr|tạo mã qr|qr code|qr cho)\s+(?:cho\s+|link\s+|text\s+)?(.+)',
r'(?:generate|make)\s+qr\s+(?:for\s+|code\s+for\s+)?(.+)',
]
for pat in patterns:
m = re.search(pat, t)
if m:
content = m.group(1).strip()
if content:
return content
# Tìm URL trong text
url_m = re.search(r'https?://\S+', text)
if url_m:
return url_m.group(0)
return None
# ================================================================
# MODULE 6: NHẬT KÝ CẢM XÚC — dùng SQLite đã có sẵn
# ================================================================
_journal_db_init = False
def _ensure_journal_db():
"""Tạo bảng journal nếu chưa có."""
global _journal_db_init
if _journal_db_init:
return
import sqlite3
try:
conn = sqlite3.connect(_URAL_DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS mood_journal (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
timestamp TEXT NOT NULL,
mood INTEGER NOT NULL,
note TEXT,
keywords TEXT
)
""")
conn.commit()
conn.close()
_journal_db_init = True
except Exception as e:
print(f"⚠️ [Journal] DB init lỗi: {e}")
def save_mood_entry(username: str, mood: int, note: str = "") -> str:
"""Lưu trạng thái cảm xúc vào nhật ký."""
_ensure_journal_db()
import sqlite3
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
try:
conn = sqlite3.connect(_URAL_DB_PATH)
conn.execute(
"INSERT INTO mood_journal (username, timestamp, mood, note) VALUES (?,?,?,?)",
(username, now, mood, note[:500])
)
conn.commit()
conn.close()
mood_labels = {0: "😔 Rất buồn", 1: "😕 Buồn", 2: "😐 Bình thường",
3: "😊 Vui", 4: "🥳 Rất vui"}
label = mood_labels.get(mood, "😐")
return f"✅ Đã lưu nhật ký cảm xúc!\n📅 {now}\n{label}\n📝 {note[:100] if note else '(không có ghi chú)'}"
except Exception as e:
return f"⚠️ Lỗi lưu nhật ký: {e}"
def get_mood_history(username: str, days: int = 7) -> str:
"""Lấy lịch sử cảm xúc 7 ngày gần nhất."""
_ensure_journal_db()
import sqlite3
try:
cutoff = (datetime.datetime.now() -
datetime.timedelta(days=days)).strftime("%Y-%m-%d")
conn = sqlite3.connect(_URAL_DB_PATH)
rows = conn.execute(
"SELECT timestamp, mood, note FROM mood_journal "
"WHERE username=? AND timestamp>=? ORDER BY timestamp DESC LIMIT 20",
(username, cutoff)
).fetchall()
conn.close()
if not rows:
return "Chưa có nhật ký cảm xúc nào trong 7 ngày qua."
mood_labels = {0: "😔", 1: "😕", 2: "😐", 3: "😊", 4: "🥳"}
lines = [f"📔 **Nhật ký cảm xúc {days} ngày qua:**\n"]
for ts, mood, note in rows:
icon = mood_labels.get(mood, "😐")
note_str = f" — {note[:60]}" if note else ""
lines.append(f"{icon} `{ts}` {note_str}")
# Tính trung bình
avg = sum(r[1] for r in rows) / len(rows)
avg_label = mood_labels.get(round(avg), "😐")
lines.append(f"\n📊 Trung bình: {avg_label} ({avg:.1f}/4)")
return "\n".join(lines)
except Exception as e:
return f"⚠️ Lỗi đọc nhật ký: {e}"
JOURNAL_KEYWORDS_SAVE = [
"ghi lại cảm xúc", "lưu nhật ký", "tôi đang",
"hôm nay mình", "hôm nay tôi", "cảm thấy",
]
JOURNAL_KEYWORDS_READ = [
"nhật ký cảm xúc", "lịch sử cảm xúc", "cảm xúc gần đây",
"tôi hay cảm thấy", "xem nhật ký",
]
# ================================================================
# MODULE 7: ĐỔI ĐƠN VỊ — Logic thuần, không cần mạng
# ================================================================
UNIT_CONVERSIONS = {
# Độ dài
("km", "m"): 1000, ("m", "km"): 0.001,
("m", "cm"): 100, ("cm", "m"): 0.01,
("cm", "mm"): 10, ("mm", "cm"): 0.1,
("km", "mile"): 0.621371, ("mile", "km"): 1.60934,
("m", "ft"): 3.28084, ("ft", "m"): 0.3048,
("m", "inch"): 39.3701, ("inch", "m"): 0.0254,
# Khối lượng
("kg", "g"): 1000, ("g", "kg"): 0.001,
("kg", "lb"): 2.20462, ("lb", "kg"): 0.453592,
("kg", "ton"): 0.001, ("ton", "kg"): 1000,
# Nhiệt độ — xử lý riêng
# Diện tích
("m2", "cm2"): 10000, ("cm2", "m2"): 0.0001,
("km2", "m2"): 1000000, ("m2", "km2"): 0.000001,
("ha", "m2"): 10000, ("m2", "ha"): 0.0001,
# Dung tích
("l", "ml"): 1000, ("ml", "l"): 0.001,
("l", "m3"): 0.001, ("m3", "l"): 1000,
# Tốc độ
("kmh", "ms"): 0.277778, ("ms", "kmh"): 3.6,
("kmh", "mph"): 0.621371, ("mph", "kmh"): 1.60934,
}
def convert_unit(text: str) -> Optional[str]:
"""
Đổi đơn vị từ câu hỏi tự nhiên.
VD: "100 km bằng bao nhiêu mile?" → "100 km = 62.14 mile"
"""
t = text.lower()
# Nhiệt độ
c_to_f = re.search(r'(\d+\.?\d*)\s*°?c\s+(?:ra|sang|bằng|=|to)\s+(?:°?f|fahrenheit)', t)
f_to_c = re.search(r'(\d+\.?\d*)\s*°?f\s+(?:ra|sang|bằng|=|to)\s+(?:°?c|celsius)', t)
if c_to_f:
c = float(c_to_f.group(1))
return f"🌡️ **{c}°C** = **{c * 9/5 + 32:.1f}°F**"
if f_to_c:
f = float(f_to_c.group(1))
return f"🌡️ **{f}°F** = **{(f - 32) * 5/9:.1f}°C**"
# Đơn vị thông thường
pattern = r'(\d+\.?\d*)\s*([a-zA-Záàảãạăắặẳẵấầẩẫậéèẻẽẹêếềểễệíìỉĩịóòỏõọôốồổỗộơớờởỡợúùủũụưứừửữựýỳỷỹỵđ²³/]+)\s+(?:ra|sang|bằng|=|to|thành)\s+([a-zA-Záàảãạ²³/]+)'
m = re.search(pattern, t)
if not m:
# Thử pattern đơn giản hơn: "100 km sang m"
m = re.search(r'(\d+\.?\d*)\s*(km|m|cm|mm|kg|g|lb|l|ml|kmh|mph|mile|ft|inch|ha|ton)\s+(?:sang|ra|đổi|thành|=|to)\s+(km|m|cm|mm|kg|g|lb|l|ml|kmh|mph|mile|ft|inch|ha|ton)', t)
if not m:
# Pattern "100km to m"
m = re.search(r'(\d+\.?\d*)\s*(km|m|cm|mm|kg|g|lb|l|ml|kmh|mph|mile|ft|inch|ha|ton)\s+.*?\s+(km|m|cm|mm|kg|g|lb|l|ml|kmh|mph|mile|ft|inch|ha|ton)\b', t)
if m:
try:
value = float(m.group(1))
from_unit = m.group(2).strip().lower().replace('²', '2').replace('³', '3')
to_unit = m.group(3).strip().lower().replace('²', '2').replace('³', '3')
factor = UNIT_CONVERSIONS.get((from_unit, to_unit))
if factor:
result = value * factor
return f"📐 **{value} {from_unit}** = **{result:,.4f} {to_unit}**"
except Exception:
pass
return None
UNIT_KEYWORDS = [
"đổi", "convert", "bằng bao nhiêu", "ra bao nhiêu",
"sang bao nhiêu", "km sang", "kg sang", "°c sang", "°f sang",
"m sang", "l sang", "đổi đơn vị",
]
def detect_unit_request(text: str) -> bool:
t = text.lower()
return any(kw in t for kw in UNIT_KEYWORDS)
# ================================================================
# MODULE 8: TRA TỪ ĐIỂN — Free Dictionary API
# ================================================================
def lookup_word(word: str) -> str:
"""Tra nghĩa từ tiếng Anh qua Free Dictionary API."""
word = word.strip().lower()
try:
resp = requests.get(
f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}",
timeout=6
)
if resp.status_code != 200:
return f"⚠️ Không tìm thấy từ '{word}' trong từ điển."
data = resp.json()[0]
phonetic = data.get("phonetic", "")
meanings = data.get("meanings", [])
lines = [f"📖 **{word}** {phonetic}\n"]
for m in meanings[:2]:
part = m.get("partOfSpeech", "")
lines.append(f"*{part}*:")
for defn in m.get("definitions", [])[:2]:
d = defn.get("definition", "")
ex = defn.get("example", "")
lines.append(f" • {d}")
if ex:
lines.append(f" → _{ex}_")
synonyms = m.get("synonyms", [])[:3]
if synonyms:
lines.append(f" Từ đồng nghĩa: {', '.join(synonyms)}")
lines.append("")
return "\n".join(lines)
except Exception as e:
return f"⚠️ Lỗi tra từ: {e}"
DICT_KEYWORDS = [
"tra từ", "nghĩa của", "from nghĩa là gì",
"mean", "definition of", "define",
"từ điển", " là gì trong tiếng anh",
"tiếng anh là gì",
]
def detect_dict_request(text: str) -> Optional[str]:
"""Trả về từ cần tra, hoặc None."""
t = text.lower()
if not any(kw in t for kw in DICT_KEYWORDS):
return None
patterns = [
r'(?:nghĩa của|tra từ|define|definition of)\s+"?([a-zA-Z\s]+)"?',
r'"?([a-zA-Z\s]+)"?\s+(?:nghĩa là gì|mean|là gì)',
r'(?:từ|word)\s+"?([a-zA-Z]+)"?',
]
for pat in patterns:
m = re.search(pat, t)
if m:
word = m.group(1).strip()
if 1 < len(word) < 30 and word.replace(" ", "").isalpha():
return word
return None
# ================================================================
# MODULE 9: QUIZ / TRÒ CHƠI — nâng cấp quiz_game cũ
# ================================================================
QUIZ_BANK = {
"kiến thức": [
{"q": "Thủ đô của Việt Nam là gì?", "a": "Hà Nội", "opts": ["Hà Nội", "TP.HCM", "Đà Nẵng", "Huế"]},
{"q": "Núi cao nhất Việt Nam?", "a": "Fansipan", "opts": ["Fansipan", "Bạch Mã", "Ngọc Linh", "Pu Si Lung"]},
{"q": "Sông dài nhất Việt Nam?", "a": "Sông Mekong (Cửu Long)", "opts": ["Sông Hồng", "Sông Mekong (Cửu Long)", "Sông Đà", "Sông Mã"]},
{"q": "Việt Nam có bao nhiêu tỉnh thành?", "a": "63", "opts": ["61", "63", "64", "65"]},
{"q": "ChatGPT được phát triển bởi công ty nào?", "a": "OpenAI", "opts": ["Google", "OpenAI", "Meta", "Microsoft"]},
{"q": "Đồng tiền của Nhật Bản là gì?", "a": "Yên (JPY)", "opts": ["Won", "Yên (JPY)", "Nhân dân tệ", "Đô la HK"]},
{"q": "1 km bằng bao nhiêu mét?", "a": "1000 m", "opts": ["100 m", "1000 m", "10000 m", "500 m"]},
{"q": "Hành tinh lớn nhất Hệ Mặt Trời?", "a": "Sao Mộc (Jupiter)", "opts": ["Sao Thổ", "Sao Mộc (Jupiter)", "Sao Hải Vương", "Mặt Trời"]},
],
"công nghệ": [
{"q": "CPU viết tắt của từ gì?", "a": "Central Processing Unit", "opts": ["Computer Power Unit", "Central Processing Unit", "Core Processing Utility", "Central Program Unit"]},
{"q": "HTTP viết tắt của gì?", "a": "HyperText Transfer Protocol", "opts": ["HyperText Transfer Protocol", "High Tech Transfer Protocol", "Hyper Transfer Text Protocol", "Host Text Transfer Protocol"]},
{"q": "RAM là viết tắt của?", "a": "Random Access Memory", "opts": ["Random Access Memory", "Read Access Memory", "Runtime Access Memory", "Rapid Access Module"]},
{"q": "Python được tạo ra bởi ai?", "a": "Guido van Rossum", "opts": ["James Gosling", "Guido van Rossum", "Linus Torvalds", "Dennis Ritchie"]},
],
"toán vui": [
{"q": "1 + 1 × 2 = ?", "a": "3", "opts": ["2", "3", "4", "1"]},
{"q": "Căn bậc hai của 144 = ?", "a": "12", "opts": ["11", "12", "13", "14"]},
{"q": "2^10 = ?", "a": "1024", "opts": ["512", "1024", "2048", "256"]},
{"q": "Số π (pi) xấp xỉ bằng?", "a": "3.14159", "opts": ["3.14159", "3.14169", "3.14150", "3.15149"]},
],
}
_active_quiz = {} # {username: {"q": ..., "a": ..., "category": ...}}
def start_quiz(username: str, category: str = "") -> str:
"""Bắt đầu quiz, trả về câu hỏi."""
# Chọn danh mục
if not category:
category = random.choice(list(QUIZ_BANK.keys()))
else:
for cat in QUIZ_BANK:
if cat in category.lower():
category = cat
break
else:
category = random.choice(list(QUIZ_BANK.keys()))
questions = QUIZ_BANK.get(category, QUIZ_BANK["kiến thức"])
q = random.choice(questions)
_active_quiz[username] = {
"q": q["q"], "a": q["a"],
"opts": q.get("opts", []), "category": category
}
opts_str = ""
if q.get("opts"):
opts_str = "\n\n" + "\n".join(
[f" {chr(65+i)}. {opt}" for i, opt in enumerate(q["opts"])]
)
return (
f"🎯 **Quiz — {category.title()}**\n\n"
f"**{q['q']}**{opts_str}\n\n"
f"*Trả lời để Yui chấm điểm nhé!*"
)
def check_quiz_answer(username: str, answer: str) -> Optional[str]:
"""Kiểm tra đáp án quiz đang chờ."""
if username not in _active_quiz:
return None
quiz = _active_quiz[username]
correct = quiz["a"]
ans_lower = answer.lower().strip()
correct_lower = correct.lower()
# Khớp chữ cái lựa chọn (A/B/C/D)
letter_match = re.match(r'^([abcd])[\.\)]?\s*$', ans_lower)
if letter_match and quiz.get("opts"):
idx = ord(letter_match.group(1)) - ord('a')
if 0 <= idx < len(quiz["opts"]):
ans_lower = quiz["opts"][idx].lower()
is_correct = (correct_lower in ans_lower or ans_lower in correct_lower)
del _active_quiz[username]
if is_correct:
return (
f"✅ **Chính xác!** 🎉\n"
f"Đáp án đúng: **{correct}**\n\n"
f"Muốn tiếp tục? Nói *'quiz tiếp'* nha!"
)
else:
return (
f"❌ **Sai rồi!**\n"
f"Đáp án đúng là: **{correct}**\n\n"
f"Đừng nản, thử câu khác nhé! Nói *'quiz tiếp'* nha!"
)
QUIZ_KEYWORDS = [
"quiz", "đố", "câu hỏi", "trắc nghiệm",
"chơi đố vui", "đố vui", "thi kiến thức",
"quiz tiếp", "câu tiếp", "câu khác",
]
def detect_quiz_request(text: str) -> bool:
return any(kw in text.lower() for kw in QUIZ_KEYWORDS)
# ================================================================
# HOOK VÀO urals_brain — xử lý tất cả module trước khi gọi AI
# ================================================================
def _patch_urals_brain_mega():
"""Hook vào urals_brain để xử lý tất cả module mới."""
target = None
for mod in sys.modules.values():
if hasattr(mod, 'urals_brain') and hasattr(mod, 'ask_logic_brain'):
target = mod
break
if not target:
return False
_orig_brain = target.urals_brain
def _mega_brain(message, history, request=None):
# Lấy text
if isinstance(message, dict):
user_text = message.get("text", "")
has_file = bool(message.get("files"))
else:
user_text = str(message)
has_file = False
if has_file or not user_text.strip():
return _orig_brain(message, history, request)
username = request.username if request else "guest"
t = user_text.lower()
# ── MODULE 1: TIN TỨC ────────────────────────────────────
news_triggers = ["tin tức", "tin mới", "tin hot", "news", "báo hôm nay",
"tin thế giới", "tin công nghệ", "tin thể thao",
"tin kinh doanh", "tin giải trí"]
if any(kw in t for kw in news_triggers):
# Tìm danh mục
cat = ""
for kw in ["thế giới", "quốc tế", "công nghệ", "tech",
"kinh doanh", "kinh tế", "giải trí", "thể thao", "bóng đá"]:
if kw in t:
cat = kw
break
return get_news(cat, num=5)
# ── MODULE 2: CRYPTO ─────────────────────────────────────
coins = detect_crypto_request(user_text)
if coins is not None:
return get_crypto_price(coins)
# ── MODULE 3: GIÁ VÀNG / TỶ GIÁ ────────────────────────
if detect_gold_request(user_text):
return get_gold_price()
# ── MODULE 4: THỜI TIẾT ──────────────────────────────────
location = detect_weather_request(user_text)
if location is not None:
return get_weather_detail(location)
# ── MODULE 5: QR CODE ────────────────────────────────────
qr_content = detect_qr_request(user_text)
if qr_content is not None:
return generate_qr(qr_content)
# ── MODULE 6: NHẬT KÝ ────────────────────────────────────
if any(kw in t for kw in JOURNAL_KEYWORDS_READ):
return get_mood_history(username, days=7)
# ── MODULE 7: ĐỔI ĐƠN VỊ ────────────────────────────────
if detect_unit_request(user_text):
result = convert_unit(user_text)
if result:
return result
# ── MODULE 8: TỪ ĐIỂN ────────────────────────────────────
word = detect_dict_request(user_text)
if word is not None:
return lookup_word(word)
# ── MODULE 9: QUIZ — ĐÃ TẮT (quá phiền) ─────────────────
# if detect_quiz_request(user_text): ... # Disabled by admin
# Không match → chạy bình thường
return _orig_brain(message, history, request)
target.urals_brain = _mega_brain
print("[Mega Module v1.0] Hook vào urals_brain thành công!")
return True
class _MegaHook:
def __init__(self):
self._done = False
def install(self):
if _patch_urals_brain_mega():
self._done = True
return
import builtins
_orig_import = builtins.__import__
outer = self
def _hooked(name, *a, **kw):
mod = _orig_import(name, *a, **kw)
level = a[4] if len(a) >= 5 else kw.get("level", 0)
if not outer._done and ("." not in str(name)) and (level == 0):
try:
if hasattr(mod, 'urals_brain') and hasattr(mod, 'ask_logic_brain'):
if _patch_urals_brain_mega():
outer._done = True
builtins.__import__ = _orig_import
except Exception:
pass
return mod
builtins.__import__ = _hooked
print("[Mega Module v1.0] Đang chờ app.py load...")
_hook = _MegaHook()
_hook.install()
print("""
+======================================================+
| URAL MEGA MODULE v1.0 |
| |
| 9 module — 100% free, không key, không phí ẩn: |
| [1] Tin tức realtime — RSS VnExpress/TuoiTre |
| [2] Giá Crypto — CoinGecko API free |
| [3] Giá Vàng/USD — ExchangeRate API free |
| [4] Thời tiết — wttr.in (chi tiết hơn) |
| [5] QR Code — qrserver.com API free |
| [6] Nhật ký cảm xúc — SQLite đã có sẵn |
| [7] Đổi đơn vị — Logic thuần, offline |
| [8] Từ điển EN — Free Dictionary API |
| [9] Quiz / Trò chơi — 4 danh mục câu hỏi |
| |
| HF Free Tier: RAM +~2MB, CPU gần như không đổi |
+======================================================+
""")
# ================================================================
# TEST
# ================================================================
if __name__ == "__main__":
import asyncio
print("=" * 58)
print("TEST MEGA MODULE v1.0")
print("=" * 58)
passed = failed = 0
def check(name, result, expect_contains):
global passed, failed
ok = expect_contains.lower() in result.lower()
print(f"{'✅' if ok else '❌'} [{name}]")
if not ok:
print(f" Got: {result[:80]}")
failed += 1
else:
passed += 1
# Module 2: Crypto detection
print("\n--- Module 2: Crypto ---")
check("detect btc", str(detect_crypto_request("giá bitcoin hôm nay")), "bitcoin")
check("detect eth", str(detect_crypto_request("giá eth bao nhiêu")), "ethereum")
check("no crypto", str(detect_crypto_request("hôm nay ăn gì")), "None")
# Module 3: Gold detection
print("\n--- Module 3: Gold ---")
check("detect gold", str(detect_gold_request("giá vàng hôm nay")), "True")
check("detect usd", str(detect_gold_request("tỷ giá usd hôm nay")), "True")
check("no gold", str(detect_gold_request("tôi đói quá")), "False")
# Module 4: Weather detection
print("\n--- Module 4: Weather ---")
check("detect weather HCM", str(detect_weather_request("thời tiết hồ chí minh")), "Ho Chi Minh")
check("detect weather HN", str(detect_weather_request("trời hà nội hôm nay thế nào")), "Hanoi")
check("no weather", str(detect_weather_request("ăn gì ngon không")), "None")
# Module 5: QR detection
print("\n--- Module 5: QR Code ---")
check("detect qr url", str(detect_qr_request("tạo qr cho link https://google.com")), "google.com")
check("detect qr text", str(detect_qr_request("tạo mã qr cho xin chào")), "xin chào")
check("no qr", str(detect_qr_request("hôm nay thứ mấy")), "None")
# Module 7: Unit conversion
print("\n--- Module 7: Đổi đơn vị ---")
check("km to m", convert_unit("100 km sang m"), "100,000")
check("kg to lb", convert_unit("70 kg đổi ra lb"), "154")
check("C to F", convert_unit("37°C sang °F"), "98")
# Module 8: Dictionary detection
print("\n--- Module 8: Từ điển ---")
check("detect word", str(detect_dict_request("nghĩa của beautiful là gì")), "beautiful")
check("detect define",str(detect_dict_request("define algorithm")), "algorithm")
check("no dict", str(detect_dict_request("con mèo cute quá")), "None")
# Module 9: Quiz
print("\n--- Module 9: Quiz ---")
q = start_quiz("test_user", "kiến thức")
check("quiz start", q, "Quiz")
ans = check_quiz_answer("test_user", "sai hết rồi xxx yyy zzz 999")
check("quiz wrong", ans or "", "Sai")
q2 = start_quiz("test_user2", "toán")
check("quiz math", q2, "Quiz")
correct_ans = _active_quiz.get("test_user2", {}).get("a", "")
if correct_ans:
ans2 = check_quiz_answer("test_user2", correct_ans)
check("quiz correct", ans2 or "", "Chính xác")
# WAV wrapper (từ module 6 dùng SQLite — chỉ check không crash)
print("\n--- Module 6: Nhật ký ---")
result = save_mood_entry("test_user", 3, "Hôm nay vui!")
check("save mood", result, "✅")
hist = get_mood_history("test_user", days=7)
check("read mood", hist, "Hôm nay vui!")
print(f"\n{'='*58}")
print(f"KẾT QUẢ: {passed}/{passed+failed} ({passed/(passed+failed)*100:.0f}%)")
if failed == 0:
print("✅ TẤT CẢ TEST PASS!")
else:
print(f"⚠️ {failed} test chưa pass")