From 1e6dbacc017ae6405d566e6083bf095a39dd899a Mon Sep 17 00:00:00 2001 From: "fian@DESKTOP-JAO9LN0." Date: Mon, 5 Oct 2026 12:03:01 +0700 Subject: [PATCH] fixing function calling. --- ai_chat.py | 23 +++++-- docs/ai.md | 7 +++ tt_client.py | 166 +++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 155 insertions(+), 41 deletions(-) diff --git a/ai_chat.py b/ai_chat.py index 12eb331..670c914 100644 --- a/ai_chat.py +++ b/ai_chat.py @@ -63,6 +63,8 @@ Handling Commands & Actions: 1. When a user asks you to perform an action or command: - If there is a matching tool in your tools list, ALWAYS invoke that tool. + - Perhatikan Informasi Server Terkini: Jika pengguna meminta tindakan untuk dirinya sendiri (seperti: "jadikan aku operator channel", "move aku ke channel X"), gunakan nama penanya dan channel tempat penanya berada. + - Jika pengguna meminta memindahkan pengguna atau join ke channel (seperti: "move kak fian ke channel ekskul"), cari dan gunakan nama channel yang paling cocok dari daftar channel server terkini. - If the user asks you to perform an action or command that is NOT available in your tools list (contoh: memutar musik, memesan makanan, restart server atau komputer, atau perintah apapun yang tidak ada di daftar fungsi), JANGAN mengarang atau berpura-pura melaksanakannya. Katakan secara ramah dan santai khas Gen Z bahwa fungsinya belum ada (contoh: "Wah kayaknya belum ada deh function buat itu di bot ini", atau "Fitur itu belum tersedia nih"). 2. If a sensitive function returns PERMISSION_DENIED because the requester is not an admin, firmly and casually refuse in your Gen Z vibe (for example: "Woi kamu bukan admin bro, gak boleh aneh-aneh ya"). 3. If an action succeeds, confirm it casually in plain text without Markdown or emojis.""" @@ -373,7 +375,7 @@ def clean_answer(text: str) -> str: "properties": { "nickname": { "type": "string", - "description": "The nickname of the user.", + "description": "The nickname of the user (or 'aku' to set requester).", }, "channel": { "type": "string", @@ -381,10 +383,10 @@ def clean_answer(text: str) -> str: }, "is_operator": { "type": "boolean", - "description": "True to grant operator status, False to revoke operator status.", + "description": "True to grant operator status (default True), False to revoke operator status.", }, }, - "required": ["nickname", "is_operator"], + "required": ["nickname"], }, }, { @@ -594,7 +596,20 @@ def _extract_tool_calls(self, result: Dict[str, Any]) -> Optional[List[Dict[str, def answer(self, question: str, history: List[Tuple[str, str]], context: Any = None) -> str: """Ask the model and return its cleaned answer; handles function calls.""" - messages: List[Dict[str, Any]] = [{"role": "system", "content": SYSTEM_PROMPT}] + system_content = SYSTEM_PROMPT + if isinstance(context, dict): + extra = [] + req_nick = context.get("nickname") or "Pengguna" + chan_name = context.get("channel_name") or f"channel {context.get('channel')}" + extra.append(f"Penanya: {req_nick} (berada di channel: {chan_name}).") + if context.get("online_users"): + extra.append(f"Pengguna online: {', '.join(context['online_users'][:30])}.") + if context.get("channels"): + extra.append(f"Daftar channel server: {', '.join(context['channels'][:30])}.") + if extra: + system_content += "\n\nInformasi Server Terkini:\n" + "\n".join(extra) + + messages: List[Dict[str, Any]] = [{"role": "system", "content": system_content}] for earlier_question, earlier_answer in history: messages.append({"role": "user", "content": earlier_question}) messages.append({"role": "assistant", "content": earlier_answer}) diff --git a/docs/ai.md b/docs/ai.md index 0e1c40b..389f3c5 100644 --- a/docs/ai.md +++ b/docs/ai.md @@ -150,6 +150,12 @@ The AI automatically invokes tools executed safely on the bot's main thread: **Unrecognized or unsupported commands**: If a user asks the AI to perform a task or action that does not have a corresponding function in the bot (for example: playing songs, ordering food, restarting servers), the AI does not hallucinate or guess; it politely informs the user in its Gen Z persona that the function is not yet available (for example: *"Wah kayaknya belum ada deh function buat itu di bot ini"*). +**Live Server Context & Fuzzy Lookups**: +- **Server snapshot injection**: When answering `@ai`, the bot injects real-time context into the AI's prompt: requester nickname, requester's current channel, online users and their channels, and the server channel list. +- **Self-referential requests**: Requests mentioning "aku", "saya", or omitted nicknames (e.g. `@ai jadiin aku operator channel dong` or `@ai move aku ke channel ekskul dong`) automatically resolve to the requester and their current channel. +- **Fuzzy user lookups**: The bot strips common Indonesian honorifics (`kak`, `bang`, `mas`, `pak`, `bu`, `@`, `bro`) and matches by exact name, stem, tokens (e.g. `kak fian` matches `Kak Fian (Ketua)` or `Fian`), or username. +- **Fuzzy channel lookups**: Strips conversational prepositions and channel terms (e.g. `ke channel ekskul` or `ruang ekskul` cleanly matches `Ekskul Robotik` or `/Root/Ekskul Robotik`). Channel terms like `sini` or `channel ini` resolve to the requester's current channel. + Setup: the same Cloudflare keys as above, then `/abt aichat on` (off by default). Other settings: `AI_CHAT_PREFIX` (default `@ai`), `AI_CHAT_MODEL` (empty: same as `AI_MODEL`), `AI_CHAT_MAX_TOKENS` (default 300), @@ -158,3 +164,4 @@ default). Other settings: `AI_CHAT_PREFIX` (default `@ai`), `AI_CHAT_MODEL` answer; empty to leave it out). Users can read `/help ai`. + diff --git a/tt_client.py b/tt_client.py index 24d3d82..3cf6b90 100644 --- a/tt_client.py +++ b/tt_client.py @@ -2627,12 +2627,31 @@ def _handle_ai_question(self, userid: int, channel_id: int, question: str): ] user = safe_call(self.getUser, userid) nickname = from_tt_char(user.szNickname).strip() if user is not None else "" + chan_path = self.getChannelPath(channel_id) or str(channel_id) + + # Snapshot of online users and channels for AI context + online_users = [] + for u in safe_call(self.getServerUsers, default=[]) or []: + u_nick = from_tt_char(u.szNickname).strip() + if u_nick: + u_chan = self.getChannelPath(u.nChannelID) or "Root" + online_users.append(f"{u_nick} (di channel {u_chan})") + + channels_list = [] + for ch in safe_call(self.getServerChannels, default=[]) or []: + c_name = self.getChannelPath(ch.nChannelID) or from_tt_char(ch.szName).strip() + if c_name: + channels_list.append(c_name) + context = { "userid": userid, "channel": channel_id, + "channel_name": chan_path, "nickname": nickname, "question": question, "is_admin": self._is_admin(userid), + "online_users": online_users[:40], + "channels": channels_list[:40], } if not self._ai_chat.ask(question, history, context): safe_call(self.send_pm, userid, "AI sedang sibuk, coba lagi sebentar lagi.") @@ -2685,38 +2704,74 @@ def _send_channel_text(self, channel_id: int, text: str): except Exception: logger.exception("Failed to send a message to channel %s", channel_id) - def _find_users_by_nickname_fuzzy(self, name: str) -> List[User]: + def _find_users_by_nickname_fuzzy(self, name: str, context: Any = None) -> List[User]: clean_name = str(name or "").strip().lower() + # If user refers to self: "aku", "saya", "me", etc. + if clean_name in ("aku", "saya", "gue", "gw", "me", "diriku", "aku sendiri", "saya sendiri"): + if isinstance(context, dict) and context.get("userid"): + try: + u = self.getUser(int(context["userid"])) + if u is not None: + return [u] + except Exception: + pass if not clean_name: return [] + users = self.getServerUsers() - # 1. Exact match - exact = [u for u in users if from_tt_char(u.szNickname).strip().lower() == clean_name] + # 1. Exact match on nickname or username + exact = [ + u for u in users + if from_tt_char(u.szNickname).strip().lower() == clean_name + or from_tt_char(u.szUsername).strip().lower() == clean_name + ] if exact: return exact - # 2. Match without honorifics/prefixes like "kak ", "bang ", "mas ", "pak ", "bu " - without_prefix = re.sub(r"^(kak|bang|mas|pak|bu|om|tante)\s+", "", clean_name).strip() - if without_prefix and without_prefix != clean_name: - prefix_match = [ - u for u in users if from_tt_char(u.szNickname).strip().lower() == without_prefix + + # 2. Match without honorifics/prefixes like "kak ", "bang ", "mas ", "pak ", "bu ", "@" + prefix_pattern = r"^@\s*|^(kak|bang|mas|pak|bu|om|tante|bro|sis|ustadz)\s+" + without_prefix = re.sub(prefix_pattern, "", clean_name).strip() + stem = without_prefix or clean_name + if stem: + stem_match = [ + u for u in users + if re.sub(prefix_pattern, "", from_tt_char(u.szNickname).strip().lower()).strip() == stem + or from_tt_char(u.szUsername).strip().lower() == stem ] - if prefix_match: - return prefix_match - # 3. Substring match + if stem_match: + return stem_match + + # 3. Token match (e.g. "fian" matches "Kak Fian" or "Fian Pratama") + if stem: + token_match = [] + for u in users: + nick = from_tt_char(u.szNickname).strip().lower() + tokens = set(re.findall(r"\w+", nick)) + if stem in tokens or clean_name in tokens: + token_match.append(u) + if token_match: + return token_match + + # 4. Substring match partial = [ u for u in users - if clean_name in from_tt_char(u.szNickname).strip().lower() - or from_tt_char(u.szNickname).strip().lower() in clean_name - or (without_prefix and without_prefix in from_tt_char(u.szNickname).strip().lower()) + if (stem and stem in from_tt_char(u.szNickname).strip().lower()) + or (stem and from_tt_char(u.szNickname).strip().lower() in stem) + or (stem and stem in from_tt_char(u.szUsername).strip().lower()) ] if partial: return partial return [] - def _find_channel_id(self, target: str) -> Optional[int]: + def _find_channel_id(self, target: str, context: Any = None) -> Optional[int]: target_str = str(target or "").strip() - if not target_str: - return None + target_lower = target_str.lower().strip("/") + + # If user says "sini", "channel ini", "room ini", "di sini", or empty + if not target_str or target_lower in ("sini", "di sini", "channel ini", "room ini", "current", "saat ini", "ini"): + if isinstance(context, dict) and context.get("channel"): + return int(context["channel"]) + # Try direct path or normalized path for p in (target_str, self._normalize_channel_target_to_path(target_str)): try: @@ -2725,17 +2780,48 @@ def _find_channel_id(self, target: str) -> Optional[int]: return cid except Exception: pass - # Search all channels by name or path - try: - target_lower = target_str.lower().strip("/") - clean_chan = re.sub(r"^channel\s+", "", target_lower).strip() - for ch in self.getServerChannels(): + + chans = self.getServerChannels() + + # 1. Exact match with original query (e.g. "Ruang Ekskul" or "Lobi") + for ch in chans: + name = from_tt_char(ch.szName).lower().strip() + path = (self.getChannelPath(ch.nChannelID) or "").lower().strip("/") + if target_lower in (name, path) or path.endswith(f"/{target_lower}"): + return ch.nChannelID + + # Clean target: strip leading prepositions and channel terms + # e.g. "ke channel ekskul" -> "ekskul", "ke room musik" -> "musik" + clean_chan = re.sub(r"^(ke|di|pada|menuju)?\s*(channel|room|ruang|saluran)?\s*", "", target_lower).strip() + clean_chan = clean_chan.strip("/") + + # 2. Exact match with clean name or path + if clean_chan and clean_chan != target_lower: + for ch in chans: name = from_tt_char(ch.szName).lower().strip() path = (self.getChannelPath(ch.nChannelID) or "").lower().strip("/") if clean_chan in (name, path) or path.endswith(f"/{clean_chan}"): return ch.nChannelID - except Exception: - pass + + # 3. Substring match (e.g. "ekskul" matches "Ruang Ekskul" or "Ekskul Musik") + search_term = clean_chan or target_lower + if search_term: + for ch in chans: + name = from_tt_char(ch.szName).lower().strip() + path = (self.getChannelPath(ch.nChannelID) or "").lower().strip("/") + if search_term in name or search_term in path: + return ch.nChannelID + + # 4. Token match + if search_term: + tokens_query = set(re.findall(r"\w+", search_term)) + if tokens_query: + for ch in chans: + name = from_tt_char(ch.szName).lower().strip() + tokens_name = set(re.findall(r"\w+", name)) + if tokens_query.issubset(tokens_name): + return ch.nChannelID + return None def _execute_ai_tool(self, tool_name: str, tool_args: dict, context: Any) -> Dict[str, Any]: @@ -2751,7 +2837,7 @@ def _execute_ai_tool(self, tool_name: str, tool_args: dict, context: Any) -> Dic } nickname = str(tool_args.get("nickname") or "").strip() reason = str(tool_args.get("reason") or "").strip() - targets = self._find_users_by_nickname_fuzzy(nickname) + targets = self._find_users_by_nickname_fuzzy(nickname, context) if not targets: return { "status": "error", @@ -2792,7 +2878,7 @@ def _execute_ai_tool(self, tool_name: str, tool_args: dict, context: Any) -> Dic } nickname = str(tool_args.get("nickname") or "").strip() reason = str(tool_args.get("reason") or "").strip() - targets = self._find_users_by_nickname_fuzzy(nickname) + targets = self._find_users_by_nickname_fuzzy(nickname, context) if not targets: return { "status": "error", @@ -2838,14 +2924,14 @@ def _execute_ai_tool(self, tool_name: str, tool_args: dict, context: Any) -> Dic } nickname = str(tool_args.get("nickname") or "").strip() channel_target = str(tool_args.get("channel") or "").strip() - targets = self._find_users_by_nickname_fuzzy(nickname) + targets = self._find_users_by_nickname_fuzzy(nickname, context) if not targets: return { "status": "error", "error": "USER_NOT_FOUND", "message": f"User '{nickname}' tidak ditemukan di server.", } - cid = self._find_channel_id(channel_target) + cid = self._find_channel_id(channel_target, context) if not cid: return { "status": "error", @@ -2922,7 +3008,7 @@ def _execute_ai_tool(self, tool_name: str, tool_args: dict, context: Any) -> Dic except Exception: minutes = 5 reason = str(tool_args.get("reason") or "").strip() - targets = self._find_users_by_nickname_fuzzy(nickname) + targets = self._find_users_by_nickname_fuzzy(nickname, context) if not targets: return { "status": "error", @@ -3107,7 +3193,7 @@ def _execute_ai_tool(self, tool_name: str, tool_args: dict, context: Any) -> Dic items = self._find_abuse_items(target) if not items: - users = self._find_users_by_nickname_fuzzy(target) + users = self._find_users_by_nickname_fuzzy(target, context) for u in users: items.extend(self._find_abuse_items(from_tt_char(u.szNickname))) items.extend(self._find_abuse_items(from_tt_char(u.szUsername))) @@ -3241,7 +3327,7 @@ def _execute_ai_tool(self, tool_name: str, tool_args: dict, context: Any) -> Dic nickname = str(tool_args.get("nickname") or "").strip() if not nickname: return {"status": "error", "message": "Nama pengguna harus diisi."} - targets = self._find_users_by_nickname_fuzzy(nickname) + targets = self._find_users_by_nickname_fuzzy(nickname, context) if not targets: return { "status": "error", @@ -3263,7 +3349,7 @@ def _execute_ai_tool(self, tool_name: str, tool_args: dict, context: Any) -> Dic elif tool_name == "check_channel_owner": channel_target = str(tool_args.get("channel") or "").strip() - cid = self._find_channel_id(channel_target) + cid = self._find_channel_id(channel_target, context) if not cid: return { "status": "error", @@ -3307,7 +3393,7 @@ def _execute_ai_tool(self, tool_name: str, tool_args: dict, context: Any) -> Dic } channel_target = str(tool_args.get("channel") or "").strip() password = str(tool_args.get("password") or "").strip() - cid = self._find_channel_id(channel_target) + cid = self._find_channel_id(channel_target, context) if not cid: return { "status": "error", @@ -3345,7 +3431,9 @@ def _execute_ai_tool(self, tool_name: str, tool_args: dict, context: Any) -> Dic nickname = str(tool_args.get("nickname") or "").strip() channel_target = str(tool_args.get("channel") or "").strip() is_op = bool(tool_args.get("is_operator", True)) - targets = self._find_users_by_nickname_fuzzy(nickname) + if not nickname: + nickname = "aku" + targets = self._find_users_by_nickname_fuzzy(nickname, context) if not targets: return { "status": "error", @@ -3353,7 +3441,9 @@ def _execute_ai_tool(self, tool_name: str, tool_args: dict, context: Any) -> Dic "message": f"User '{nickname}' tidak ditemukan di server.", } u = targets[0] - cid = self._find_channel_id(channel_target) if channel_target else u.nChannelID + cid = self._find_channel_id(channel_target, context) if channel_target else None + if not cid and isinstance(context, dict) and context.get("channel"): + cid = int(context["channel"]) if not cid: cid = u.nChannelID self.doChannelOpEx(u.nUserID, cid, "", is_op) @@ -3367,7 +3457,7 @@ def _execute_ai_tool(self, tool_name: str, tool_args: dict, context: Any) -> Dic elif tool_name == "get_channel_info": channel_target = str(tool_args.get("channel") or "").strip() - cid = self._find_channel_id(channel_target) + cid = self._find_channel_id(channel_target, context) if not cid: return { "status": "error", @@ -3395,7 +3485,9 @@ def _execute_ai_tool(self, tool_name: str, tool_args: dict, context: Any) -> Dic elif tool_name == "get_user_info": nickname = str(tool_args.get("nickname") or "").strip() - targets = self._find_users_by_nickname_fuzzy(nickname) + if not nickname: + nickname = "aku" + targets = self._find_users_by_nickname_fuzzy(nickname, context) if not targets: return { "status": "error",