092_agent_api / tools /scheduler.py
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
@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}