import logging import time import dateparser from typing import Optional from datetime import datetime from .base import register_tool, get_llm from ..redis_client import redis_client logger = logging.getLogger(__name__) # Note: DATA_FILE and local JSON methods are deprecated and removed. def _normalize_time_string(time_str: str) -> str: """Chuyển đổi định dạng thời gian kiểu Việt Nam (VD: 20h30 -> 20:30).""" import re if not time_str: return time_str # Thay thế 20h30 hoặc 20h thành 20:30 hoặc 20:00 time_str = re.sub(r'(\d+)h(\d*)', lambda m: f"{m.group(1)}:{m.group(2) or '00'}", time_str) return time_str def _parse_time_with_llm(time_str: str) -> Optional[datetime]: """Sử dụng LLM để hiểu thời gian tự nhiên tiếng Việt.""" try: llm = get_llm() now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") prompt = f"Hôm nay là {now}. Hãy chuyển cụm từ thời gian sau sang định dạng ISO 8601 (YYYY-MM-DDTHH:MM:SS): '{time_str}'. Chỉ trả về chuỗi ISO, không giải thích gì thêm." response = llm.invoke(prompt) iso_str = response.content.strip() # Clean up if AI returns code blocks iso_str = iso_str.replace('`', '').replace('json', '').strip() return datetime.fromisoformat(iso_str) except Exception as e: logger.error(f"Error parsing time with LLM: {e}") return None @register_tool( name="get_schedule", description="Tra cứu lịch họp và sự kiện của nhóm. Hỗ trợ lọc theo thời gian hoặc từ khóa. Đồng thời kiểm tra ngữ cảnh thảo luận gần đây.", parameters=[ { "name": "query", "type": "string", "description": "TỪ KHÓA DUY NHẤT (VD: 'họp'). Để trống nếu muốn xem toàn bộ lịch trình.", "required": False }, { "name": "date_str", "type": "string", "description": "Thời gian muốn tra cứu bằng từ ngữ tự nhiên. Ví dụ: 'sáng thứ 3', 'ngày mai', 'tuần sau'", "required": False }, { "name": "room_id", "type": "string", "description": "ID phòng chat để kiểm tra thêm ngữ cảnh thảo luận (tùy chọn).", "required": False } ] ) def tool_get_schedule(query: str = "", date_str: str = "", room_id: str = None) -> dict: results = [] # 1. Fetch Structured Events from Redis target_date = None start_ts, end_ts = 0, 4000000000000 # Default range if date_str: date_str = _normalize_time_string(date_str) target_date = dateparser.parse(date_str, languages=['vi', 'en'], settings={'PREFER_DATES_FROM': 'future'}) if not target_date: target_date = _parse_time_with_llm(date_str) if target_date: logger.info(f"Normalized date '{date_str}' to '{target_date.date()}'") # Create a 24-hour range for the sorted set query day_start = datetime.combine(target_date.date(), datetime.min.time()) day_end = datetime.combine(target_date.date(), datetime.max.time()) start_ts = int(day_start.timestamp() * 1000) end_ts = int(day_end.timestamp() * 1000) else: # Fallback for range-like strings (e.g., "next 2 weeks", "tuần tới") range_keywords = ["tuần", "tháng", "next", "week", "month", "khoảng", "tới"] if any(k in date_str.lower() for k in range_keywords): logger.info(f"Date string '{date_str}' looks like a range. Returning all future events (30 days).") start_ts = int(datetime.now().timestamp() * 1000) end_ts = start_ts + (30 * 24 * 60 * 60 * 1000) # 30 days else: return { "status": "error", "message": f"Không thể hiểu được khoảng thời gian: '{date_str}'." } # Retrieve from Redis events = redis_client.list_events(start_ts, end_ts) for event in events: match = True if query: name = event.get("name", "").lower() desc = event.get("description", "").lower() if query.lower() not in name and query.lower() not in desc: match = False if match: results.append(event) # Robustness Fallback: # If results are empty and there was a query but no date_str, # check if the query was meant to be a date (e.g., "tối nay"). # We only fallback if the query contains common time-related keywords to avoid false positives (e.g., "ăn"). time_keywords = ["nay", "mai", "mốt", "hôm", "tối", "sáng", "chiều", "trưa", "ngày", "lịch", "tuần", "tháng"] is_time_query = any(k in query.lower() for k in time_keywords) or any(char.isdigit() for char in query) if not results and query and not date_str and is_time_query: query_norm = _normalize_time_string(query) fallback_date = dateparser.parse(query_norm, languages=['vi', 'en'], settings={'PREFER_DATES_FROM': 'future'}) if fallback_date: # Check if parsing was actually meaningful (not just a random number or word parsed as current year) logger.info(f"Query '{query}' looks like a date. Retrying search with date filtering.") # Recursive call with query moved to date_str return tool_get_schedule(query="", date_str=query, room_id=room_id) # 2. Fetch Chat Context (Hybrid Memory) chat_context = [] if room_id: # Get last 50 messages to see if there are any discussed but unscheduled events chat_context = redis_client.get_room_messages(room_id, limit=50) return { "status": "success", "count": len(results), "scheduled_events": results, "recent_discussions_context": chat_context if room_id else "No room_id provided for context" } @register_tool( name="add_event", description="Thêm một sự kiện mới vào lịch của nhóm (Lưu trực tiếp vào Redis).", parameters=[ {"name": "name", "type": "string", "description": "Tên sự kiện. Ví dụ: 'Họp nhóm'", "required": True}, {"name": "time_str", "type": "string", "description": "Thời gian tổ chức sự kiện. Ví dụ: '9h sáng thứ 3 tuần sau'", "required": True}, {"name": "description", "type": "string", "description": "Mô tả chi tiết nội dung sự kiện", "required": False}, {"name": "location", "type": "string", "description": "Địa điểm tổ chức", "required": False} ] ) def tool_add_event(name: str, time_str: str, description: str = "", location: str = "TBD") -> dict: # Parse time time_str = _normalize_time_string(time_str) event_time = dateparser.parse(time_str, languages=['vi', 'en'], settings={'PREFER_DATES_FROM': 'future'}) if not event_time: event_time = _parse_time_with_llm(time_str) if not event_time: return {"status": "error", "message": f"Không thể hiểu được thời gian: '{time_str}'."} new_event = { "id": f"evt_{int(datetime.now().timestamp() * 1000)}", "name": name, "time": event_time.isoformat(), "location": location, "description": description, "owner": "Agent" } if redis_client.save_event(new_event): return { "status": "success", "message": f"Đã thêm sự kiện: {name} vào lúc {event_time}", "data": new_event } else: return {"status": "error", "message": "Lỗi lưu sự kiện vào Redis."} @register_tool( name="update_event", description="Cập nhật thông tin của một sự kiện đã tồn tại trong lịch.", parameters=[ {"name": "event_id", "type": "string", "description": "ID của sự kiện cần cập nhật. Ví dụ: 'evt_1712345678'", "required": True}, {"name": "name", "type": "string", "description": "Tên mới của sự kiện", "required": False}, {"name": "time_str", "type": "string", "description": "Thời gian mới (VD: '14h chiều mai')", "required": False}, {"name": "description", "type": "string", "description": "Mô tả mới", "required": False}, {"name": "location", "type": "string", "description": "Địa điểm mới", "required": False} ] ) def tool_update_event(event_id: str, **kwargs) -> dict: """Cập nhật sự kiện hiện có.""" try: # Lấy dữ liệu cũ (list_events trả về list, ta cần tìm đúng ID) all_events = redis_client.list_events() existing = next((e for e in all_events if e.get("id") == event_id), None) if not existing: return {"status": "error", "message": f"Không tìm thấy sự kiện với ID: {event_id}"} # Cập nhật các trường if "name" in kwargs: existing["name"] = kwargs["name"] if "description" in kwargs: existing["description"] = kwargs["description"] if "location" in kwargs: existing["location"] = kwargs["location"] if "time_str" in kwargs: ts_norm = _normalize_time_string(kwargs["time_str"]) new_time = dateparser.parse(ts_norm, languages=['vi', 'en'], settings={'PREFER_DATES_FROM': 'future'}) if new_time: existing["time"] = new_time.isoformat() else: return {"status": "error", "message": f"Không hiểu thời gian mới: {kwargs['time_str']}"} if redis_client.save_event(existing): return {"status": "success", "message": f"Đã cập nhật sự kiện {event_id}", "data": existing} return {"status": "error", "message": "Lỗi khi lưu cập nhật vào Redis."} except Exception as e: return {"status": "error", "message": str(e)} @register_tool( name="delete_event", description="Xóa một sự kiện khỏi lịch trình.", parameters=[ {"name": "event_id", "type": "string", "description": "ID của sự kiện cần xóa.", "required": True} ] ) def tool_delete_event(event_id: str) -> dict: """Xóa sự kiện.""" if redis_client.delete_event(event_id): return {"status": "success", "message": f"Đã xóa sự kiện {event_id}"} return {"status": "error", "message": f"Thất bại khi xóa sự kiện {event_id}"} @register_tool( name="add_reminder", description="Đặt một lời nhắc nhở nhanh cho bản thân.", parameters=[ {"name": "content", "type": "string", "description": "Nội dung cần nhắc nhở. Ví dụ: 'Uống thuốc'", "required": True}, {"name": "time_str", "type": "string", "description": "Thời gian nhắc (VD: 'trong 5 phút nữa', '8h tối nay')", "required": True} ] ) def tool_add_reminder(content: str, time_str: str) -> dict: """Thêm lời nhắc mới.""" time_str = _normalize_time_string(time_str) target_time = dateparser.parse(time_str, languages=['vi', 'en'], settings={'PREFER_DATES_FROM': 'future'}) if not target_time: target_time = _parse_time_with_llm(time_str) if not target_time: return {"status": "error", "message": f"Không hiểu thời gian: {time_str}"} rem_id = f"rem_{int(time.time() * 1000)}" reminder_data = { "id": rem_id, "content": content, "time": target_time.isoformat(), "timestamp": int(target_time.timestamp() * 1000), "status": "pending" } if redis_client.save_reminder(reminder_data): return {"status": "success", "message": f"Đã ghi nhớ lời nhắc: '{content}' vào lúc {target_time}", "data": reminder_data} return {"status": "error", "message": "Lỗi lưu nhắc nhở."} @register_tool( name="get_reminders", description="Lấy danh sách các lời nhắc nhở đã đặt.", parameters=[ {"name": "limit", "type": "integer", "description": "Số lượng lời nhắc cần lấy.", "required": False} ] ) def tool_get_reminders(limit: int = 50) -> dict: """Liệt kê nhắc nhở.""" rems = redis_client.list_reminders(limit=limit) return {"status": "success", "count": len(rems), "reminders": rems}