Spaces:
Sleeping
Sleeping
quachtiensinh27
feat: initialize core agent infrastructure including Redis client, tool directory, scheduling logic, and documentation.
85ff578 | 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 | |
| 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" | |
| } | |
| 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."} | |
| 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)} | |
| 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}"} | |
| 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ở."} | |
| 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} | |