diff --git a/config.py b/config.py index b34c889..a6cf119 100644 --- a/config.py +++ b/config.py @@ -150,7 +150,7 @@ if _persona_yaml_path and _persona_yaml_path.is_file(): if not isinstance(_character_env, dict): _character_env = {} except Exception as _e: - print(f"[config] Warning: gagal load persona.yaml untuk '{AGENT_CHARACTER}': {_e}") + print(f"[config] Warning: gagal load persona.yaml untuk '{AGENT_CHARACTER}': {_e}", flush=True) _character_env = {} elif ENV_CHARACTER_CONFIG_PATH and ENV_CHARACTER_CONFIG_PATH.is_file(): # Fallback: baca character.md (format lama) diff --git a/hendrik.py b/hendrik.py index 594cfb5..6a384f0 100644 --- a/hendrik.py +++ b/hendrik.py @@ -50,7 +50,7 @@ def main(): if workspace: resolved = os.path.abspath(workspace) if not os.path.isdir(resolved): - print(f"Error: '{resolved}' is not a valid directory") + print(f"Error: '{resolved}' is not a valid directory", flush=True) sys.exit(1) os.chdir(resolved) @@ -103,7 +103,7 @@ def main(): def _handle_sig(signum, frame): nonlocal _shutdown - print("\nShutting down...") + print("\nShutting down...", flush=True) for svc in services: svc.stop() _shutdown = True @@ -116,7 +116,7 @@ def main(): time.sleep(1) except KeyboardInterrupt: _shutdown = True - print("Exiting.") + print("Exiting.", flush=True) else: from tui import HendrikTUI HendrikTUI( diff --git a/scripts/persona.py b/scripts/persona.py index 21931fe..9559b47 100644 --- a/scripts/persona.py +++ b/scripts/persona.py @@ -293,7 +293,7 @@ def build_system_prompt( else: selected_skill = "" except Exception as e: - print(f"[persona] Warning: gagal load persona.yaml untuk '{character_name}': {e}") + print(f"[persona] Warning: gagal load persona.yaml untuk '{character_name}': {e}", flush=True) # Resolve skills list # Priority: explicit skills param > persona.yaml skill > AGENT_SKILL env > AGENT_SKILLS env > selected_skill diff --git a/services/agent_loop.py b/services/agent_loop.py index 87801ef..5af5b19 100644 --- a/services/agent_loop.py +++ b/services/agent_loop.py @@ -29,7 +29,7 @@ def execute_tool(tool_call, TOOL_HANDLERS): def run_agent_loop(session, llm_client, TOOLS, TOOL_HANDLERS, max_iterations, on_tool_calls=None): for step in range(max_iterations): - print(f'[{_ts()}] Step {step + 1} — calling LLM...') + print(f'[{_ts()}] Step {step + 1} — calling LLM...', flush=True) response = llm_client.chat(session.messages, tools=TOOLS) if response.tool_calls: @@ -41,7 +41,7 @@ def run_agent_loop(session, llm_client, TOOLS, TOOL_HANDLERS, max_iterations, on session.messages.append(amsg) tnames = [tc['function']['name'] for tc in response.tool_calls] - print(f'[{_ts()}] Using tools: {", ".join(tnames)}') + print(f'[{_ts()}] Using tools: {", ".join(tnames)}', flush=True) if on_tool_calls: on_tool_calls(tnames) @@ -55,12 +55,12 @@ def run_agent_loop(session, llm_client, TOOLS, TOOL_HANDLERS, max_iterations, on }) else: if response.content: - print(f'[{_ts()}] Response generated ({len(response.content)} chars)') + print(f'[{_ts()}] Response generated ({len(response.content)} chars)', flush=True) session.messages.append({'role': 'assistant', 'content': response.content}) return response.content return None - print(f'[{_ts()}] Max iterations ({max_iterations}) reached') + print(f'[{_ts()}] Max iterations ({max_iterations}) reached', flush=True) session.messages.append({ 'role': 'assistant', 'content': 'Max iterations reached without final answer.', diff --git a/services/telegram_client.py b/services/telegram_client.py index 14d76a6..a2cca00 100644 --- a/services/telegram_client.py +++ b/services/telegram_client.py @@ -46,7 +46,7 @@ class TelegramClient: self._app = Application.builder().token(token).build() def start(self): - print(f'[{_ts()}] Starting Telegram service...') + print(f'[{_ts()}] Starting Telegram service...', flush=True) asyncio.run(self._async_run()) def stop(self): @@ -62,14 +62,14 @@ class TelegramClient: bot_user = await self._app.bot.get_me() self._bot_username = bot_user.username or "" - print(f'[{_ts()}] Telegram bot: @{self._bot_username}') + print(f'[{_ts()}] Telegram bot: @{self._bot_username}', flush=True) self._register_handlers() await self._app.initialize() await self._app.start() await self._app.updater.start_polling() - print(f'[{_ts()}] Telegram bot is polling') + print(f'[{_ts()}] Telegram bot is polling', flush=True) try: await self._stopped.wait() @@ -79,7 +79,7 @@ class TelegramClient: await self._app.updater.stop() await self._app.stop() await self._app.shutdown() - print(f'[{_ts()}] Telegram service stopped') + print(f'[{_ts()}] Telegram service stopped', flush=True) def _register_handlers(self): from telegram.ext import MessageHandler, filters, CommandHandler @@ -115,7 +115,7 @@ class TelegramClient: if not text: return msg_id = update.message.message_id - print(f'[{_ts()}] Telegram DM from {chat_id}: {text[:60]}') + print(f'[{_ts()}] Telegram DM from {chat_id}: {text[:60]}', flush=True) threading.Thread( target=self._process_message, args=(chat_id, text, 'private', '', msg_id), @@ -156,7 +156,7 @@ class TelegramClient: name_mentioned = _name_mentioned(PERSONALITY.name, text) if not replied_to_bot and not has_mention and not name_mentioned: - print(f'[{_ts()}] Telegram Group [{chat_id}] NO-REPLY: {text[:60]}') + print(f'[{_ts()}] Telegram Group [{chat_id}] NO-REPLY: {text[:60]}', flush=True) return if has_mention and self._bot_username: @@ -167,7 +167,7 @@ class TelegramClient: msg_id = update.message.message_id sender = update.effective_user sender_name = sender.full_name or sender.username or str(sender.id) if sender else str(chat_id) - print(f'[{_ts()}] Telegram Group [{chat_id}] <{sender_name}>: {text[:60]}') + print(f'[{_ts()}] Telegram Group [{chat_id}] <{sender_name}>: {text[:60]}', flush=True) threading.Thread( target=self._process_message, args=(chat_id, text, 'group', sender_name, msg_id), @@ -186,7 +186,7 @@ class TelegramClient: if body in (':new', '/new'): self._session_mgr.reset(str(chat_id)) - print(f'[{_ts()}] Session reset for {chat_id}') + print(f'[{_ts()}] Session reset for {chat_id}', flush=True) self._schedule_send(chat_id, 'Memulai sesi baru. Ada yang bisa dibantu?', reply_to_msg_id) return @@ -203,7 +203,7 @@ class TelegramClient: def on_tool_calls(tnames): info = f'Using: {", ".join(tnames)}' - print(f'[{_ts()}] {info}') + print(f'[{_ts()}] {info}', flush=True) if not is_roleplay: self._schedule_send(chat_id, info, reply_to_msg_id) @@ -231,17 +231,17 @@ class TelegramClient: recent_history=recent_history, my_name=my_name, ): - print(f'[{_ts()}] need_response=True → sending response') + print(f'[{_ts()}] need_response=True → sending response', flush=True) self._schedule_send(chat_id, final_content, reply_to_msg_id) else: - print(f'[{_ts()}] need_response=False → staying silent') + print(f'[{_ts()}] need_response=False → staying silent', flush=True) else: from tools.roleplayer import _name_mentioned if _name_mentioned(my_name, body): - print(f'[{_ts()}] Name mentioned → sending response') + print(f'[{_ts()}] Name mentioned → sending response', flush=True) self._schedule_send(chat_id, final_content, reply_to_msg_id) else: - print(f'[{_ts()}] Name not mentioned → staying silent') + print(f'[{_ts()}] Name not mentioned → staying silent', flush=True) else: self._schedule_send(chat_id, final_content, reply_to_msg_id) else: @@ -255,7 +255,7 @@ class TelegramClient: if self._loop and not self._loop.is_closed(): char_count = len(text) if text else 0 sleep_delay = max(1.0, min(char_count / config.TYPING_SPEED, config.TYPING_MAX)) - print(f'[{_ts()}] Typing delay: {sleep_delay:.1f}s ({char_count} chars)') + print(f'[{_ts()}] Typing delay: {sleep_delay:.1f}s ({char_count} chars)', flush=True) time.sleep(sleep_delay) asyncio.run_coroutine_threadsafe( @@ -267,9 +267,9 @@ class TelegramClient: self._loop ) else: - print(f'[{_ts()}] WARNING: cannot send to {chat_id} — loop unavailable') + print(f'[{_ts()}] WARNING: cannot send to {chat_id} — loop unavailable', flush=True) def _timeout_session(self, chat_id): - print(f'[{_ts()}] Session timeout: {chat_id}') + print(f'[{_ts()}] Session timeout: {chat_id}', flush=True) self._schedule_send(chat_id, 'Sesi ditutup. Sampai jumpa') self._session_mgr.reset(str(chat_id)) diff --git a/services/xmpp_client.py b/services/xmpp_client.py index 5e0222c..6313a2d 100644 --- a/services/xmpp_client.py +++ b/services/xmpp_client.py @@ -99,7 +99,7 @@ class XMPPClient(ClientXMPP): pending = self._muc_rejoin_tasks.get(room) if pending and not pending.done(): pending.cancel() - print(f'[{_ts()}] MUC [{room}] Cancelled pending rejoin (new trigger)') + print(f'[{_ts()}] MUC [{room}] Cancelled pending rejoin (new trigger)', flush=True) # Check cooldown: jangan rejoin terlalu cepat berturut-turut now = datetime.now() @@ -109,7 +109,7 @@ class XMPPClient(ClientXMPP): if elapsed < MUC_REJOIN_COOLDOWN: # Anti-ban: too soon, schedule delayed rejoin instead of immediate cooldown_left = MUC_REJOIN_COOLDOWN - elapsed - print(f'[{_ts()}] MUC [{room}] Cooldown active ({cooldown_left:.0f}s left), delaying rejoin') + print(f'[{_ts()}] MUC [{room}] Cooldown active ({cooldown_left:.0f}s left), delaying rejoin', flush=True) delay = cooldown_left + self._calc_rejoin_delay(room) else: delay = self._calc_rejoin_delay(room) @@ -120,7 +120,7 @@ class XMPPClient(ClientXMPP): attempts = self._muc_rejoin_attempts.get(room, 0) + 1 self._muc_rejoin_attempts[room] = attempts - print(f'[{_ts()}] MUC [{room}] Rejoin scheduled in {delay:.0f}s (attempt #{attempts})') + print(f'[{_ts()}] MUC [{room}] Rejoin scheduled in {delay:.0f}s (attempt #{attempts})', flush=True) if self._loop and not self._loop.is_closed(): task = asyncio.run_coroutine_threadsafe( @@ -134,20 +134,20 @@ class XMPPClient(ClientXMPP): await asyncio.sleep(delay) # Double-check: jangan rejoin kalau sudah di _muc_ready if room in self._muc_ready: - print(f'[{_ts()}] MUC [{room}] Already ready, skip rejoin') + print(f'[{_ts()}] MUC [{room}] Already ready, skip rejoin', flush=True) return nick = self._get_muc_nick(room) - print(f'[{_ts()}] MUC [{room}] Rejoining as {nick}...') + print(f'[{_ts()}] MUC [{room}] Rejoining as {nick}...', flush=True) await self.plugin['xep_0045'].join_muc_wait(room, nick, maxstanzas=0) self._muc_last_join[room] = datetime.now() # _muc_ready akan di-set oleh _on_muc_presence saat join berhasil self._muc_rejoin_attempts.pop(room, None) self._muc_rejoin_attempts.pop("_nick_" + room, None) - print(f'[{_ts()}] MUC [{room}] Rejoin successful as {nick}') + print(f'[{_ts()}] MUC [{room}] Rejoin successful as {nick}', flush=True) except asyncio.CancelledError: - print(f'[{_ts()}] MUC [{room}] Rejoin cancelled') + print(f'[{_ts()}] MUC [{room}] Rejoin cancelled', flush=True) except Exception as e: - print(f'[{_ts()}] MUC [{room}] Rejoin failed: {e}') + print(f'[{_ts()}] MUC [{room}] Rejoin failed: {e}', flush=True) # Anti-ban: handle 409 Conflict - nick sudah dipakai orang lain if '409' in str(e) or 'conflict' in str(e).lower(): nick_attempts = self._muc_rejoin_attempts.get("_nick_" + room, 0) @@ -155,33 +155,33 @@ class XMPPClient(ClientXMPP): # Anti-ban: coba nick alternatif (lily_, lily__) self._muc_rejoin_attempts["_nick_" + room] = nick_attempts + 1 new_nick = self._get_muc_nick(room) - print(f'[{_ts()}] MUC [{room}] Nick conflict, trying alternative: {new_nick}') + print(f'[{_ts()}] MUC [{room}] Nick conflict, trying alternative: {new_nick}', flush=True) # Retry segera dengan nick baru (tanpa backoff rejoin, tapi tetap ada delay biasa) self._schedule_muc_rejoin(room) else: # Anti-ban: semua nick alternativehabis, stop retry untuk avoid ban - print(f'[{_ts()}] MUC [{room}] All nick variations exhausted, skipping room') - print(f'[{_ts()}] MUC [{room}] Set XMPP_NICKNAME in .env to a unique nick') + print(f'[{_ts()}] MUC [{room}] All nick variations exhausted, skipping room', flush=True) + print(f'[{_ts()}] MUC [{room}] Set XMPP_NICKNAME in .env to a unique nick', flush=True) else: # Anti-ban: error biasa (network, dll), retry with backoff self._schedule_muc_rejoin(room) async def _on_connected(self, event): - print(f'[{_ts()}] XMPP connected') + print(f'[{_ts()}] XMPP connected', flush=True) async def _on_disconnected(self, event): - print(f'[{_ts()}] XMPP disconnected') + print(f'[{_ts()}] XMPP disconnected', flush=True) # Anti-ban: cancel all pending rejoin tasks on disconnect for room, task in list(self._muc_rejoin_tasks.items()): if not task.done(): task.cancel() - print(f'[{_ts()}] MUC [{room}] Cancelled pending rejoin (disconnected)') + print(f'[{_ts()}] MUC [{room}] Cancelled pending rejoin (disconnected)', flush=True) self._muc_rejoin_tasks.clear() async def _on_session_start(self, event): self.send_presence() self.get_roster() - print(f'[{_ts()}] XMPP online as {self.boundjid.full}') + print(f'[{_ts()}] XMPP online as {self.boundjid.full}', flush=True) for room in self._muc_rooms: # Anti-ban: retry join dengan incremental delay & nick fallback success = False @@ -189,35 +189,35 @@ class XMPPClient(ClientXMPP): nick = self._get_muc_nick(room) try: await self.plugin['xep_0045'].join_muc_wait(room, nick, maxstanzas=0) - print(f'[{_ts()}] Joined MUC room: {room} as {nick}') + print(f'[{_ts()}] Joined MUC room: {room} as {nick}', flush=True) self._muc_last_join[room] = datetime.now() self._muc_rejoin_attempts.pop(room, None) self._muc_rejoin_attempts.pop("_nick_" + room, None) success = True break except Exception as e: - print(f'[{_ts()}] MUC join attempt #{attempt} failed ({room}): {e}') + print(f'[{_ts()}] MUC join attempt #{attempt} failed ({room}): {e}', flush=True) # Anti-ban: handle 409 Conflict - coba nick alternatif if '409' in str(e) or 'conflict' in str(e).lower(): nick_attempts = self._muc_rejoin_attempts.get("_nick_" + room, 0) if nick_attempts < MUC_NICK_SUFFIX_MAX: nick_attempts += 1 self._muc_rejoin_attempts["_nick_" + room] = nick_attempts - print(f'[{_ts()}] MUC [{room}] Nick conflict, switching to: {self._get_muc_nick(room)}') + print(f'[{_ts()}] MUC [{room}] Nick conflict, switching to: {self._get_muc_nick(room)}', flush=True) # Retry segera dengan nick baru (jangan wait) continue else: # Anti-ban: semua nick alternatif habis - print(f'[{_ts()}] MUC [{room}] All nick variations exhausted') + print(f'[{_ts()}] MUC [{room}] All nick variations exhausted', flush=True) break elif attempt < 3: # Anti-ban: error biasa, wait before retry (2s, 4s) retry_delay = 2.0 * attempt - print(f'[{_ts()}] MUC [{room}] Retrying in {retry_delay:.0f}s...') + print(f'[{_ts()}] MUC [{room}] Retrying in {retry_delay:.0f}s...', flush=True) await asyncio.sleep(retry_delay) if not success: # Anti-ban: semua attempt gagal, schedule background rejoin - print(f'[{_ts()}] MUC [{room}] All join attempts failed, scheduling background rejoin') + print(f'[{_ts()}] MUC [{room}] All join attempts failed, scheduling background rejoin', flush=True) self._schedule_muc_rejoin(room) def _on_message(self, msg): @@ -227,7 +227,7 @@ class XMPPClient(ClientXMPP): body = msg['body'].strip() if not body: return - print(f'[{_ts()}] DM from {jid}: {body[:60]}') + print(f'[{_ts()}] DM from {jid}: {body[:60]}', flush=True) threading.Thread(target=self._process_dm, args=(jid, body), daemon=True).start() def _on_groupchat_message(self, msg): @@ -243,7 +243,7 @@ class XMPPClient(ClientXMPP): body = msg['body'].strip() if not body: return - print(f'[{_ts()}] MUC [{room}] <{nick}>: {body[:60]}') + print(f'[{_ts()}] MUC [{room}] <{nick}>: {body[:60]}', flush=True) threading.Thread(target=self._process_muc, args=(room, nick, body), daemon=True).start() def _is_my_nick(self, room: str, nick: str) -> bool: @@ -263,20 +263,20 @@ class XMPPClient(ClientXMPP): self._muc_rejoin_attempts.pop(room, None) self._muc_rejoin_attempts.pop("_nick_" + room, None) if ptype == 'unavailable': - print(f'[{_ts()}] MUC [{room}] <{nick}> left') + print(f'[{_ts()}] MUC [{room}] <{nick}> left', flush=True) # Anti-ban: remove from ready set on unavailable to keep state consistent self._muc_ready.discard(room) # Anti-ban: trigger auto-rejoin with exponential backoff if self._is_my_nick(room, nick): self._schedule_muc_rejoin(room) elif ptype == 'error': - print(f'[{_ts()}] MUC [{room}] error: {presence}') + print(f'[{_ts()}] MUC [{room}] error: {presence}', flush=True) # Anti-ban: also rejoin on error (e.g. temporary failure) if self._is_my_nick(room, nick): self._muc_ready.discard(room) self._schedule_muc_rejoin(room) else: - print(f'[{_ts()}] MUC [{room}] <{nick}> joined (type={ptype})') + print(f'[{_ts()}] MUC [{room}] <{nick}> joined (type={ptype})', flush=True) def _process_dm(self, jid, body): session = self._session_mgr.get_or_create( @@ -292,7 +292,7 @@ class XMPPClient(ClientXMPP): if body == ':new': self._session_mgr.reset(jid) - print(f'[{_ts()}] Session reset for {jid}') + print(f'[{_ts()}] Session reset for {jid}', flush=True) self._schedule_send(jid, 'Memulai sesi baru. Ada yang bisa di bantu?') return @@ -335,17 +335,17 @@ class XMPPClient(ClientXMPP): recent_history=recent_history, my_name=my_name, ): - print(f'[{_ts()}] need_response=True → sending response') + print(f'[{_ts()}] need_response=True → sending response', flush=True) self._schedule_send(jid, final_content, 'chat') else: - print(f'[{_ts()}] need_response=False → staying silent') + print(f'[{_ts()}] need_response=False → staying silent', flush=True) else: from tools.roleplayer import _name_mentioned if _name_mentioned(my_name, quote): - print(f'[{_ts()}] Name mentioned → sending response') + print(f'[{_ts()}] Name mentioned → sending response', flush=True) self._schedule_send(jid, final_content, 'chat') else: - print(f'[{_ts()}] Name not mentioned → staying silent') + print(f'[{_ts()}] Name not mentioned → staying silent', flush=True) else: self._schedule_send(jid, f'> {quote}\n{final_content}', 'chat') else: @@ -370,7 +370,7 @@ class XMPPClient(ClientXMPP): if body == ':new': self._session_mgr.reset(room) - print(f'[{_ts()}] Session reset for MUC room {room}') + print(f'[{_ts()}] Session reset for MUC room {room}', flush=True) self._schedule_send(room, 'Memulai sesi baru. Ada yang bisa di bantu?', mtype='groupchat') return @@ -413,17 +413,17 @@ class XMPPClient(ClientXMPP): recent_history=recent_history, my_name=my_name, ): - print(f'[{_ts()}] need_response=True → sending response') + print(f'[{_ts()}] need_response=True → sending response', flush=True) self._schedule_send(room, final_content, 'groupchat') else: - print(f'[{_ts()}] need_response=False → staying silent') + print(f'[{_ts()}] need_response=False → staying silent', flush=True) else: from tools.roleplayer import _name_mentioned if _name_mentioned(my_name, quote): - print(f'[{_ts()}] Name mentioned → sending response') + print(f'[{_ts()}] Name mentioned → sending response', flush=True) self._schedule_send(room, final_content, 'groupchat') else: - print(f'[{_ts()}] Name not mentioned → staying silent') + print(f'[{_ts()}] Name not mentioned → staying silent', flush=True) else: self._schedule_send(room, f'> {quote}\n{final_content}', 'groupchat') else: @@ -445,27 +445,27 @@ class XMPPClient(ClientXMPP): self._send_coro(to, body, mtype), self._loop ) else: - print(f'[{_ts()}] WARNING: cannot send to {to} — loop unavailable') + print(f'[{_ts()}] WARNING: cannot send to {to} — loop unavailable', flush=True) async def _send_coro(self, to, body, mtype): try: # Delay 2: simulasi mengetik (proporsional dengan panjang pesan) delay = _typing_delay(body) - print(f'[{_ts()}] Typing delay: {delay:.1f}s ({len(body)} chars)') + print(f'[{_ts()}] Typing delay: {delay:.1f}s ({len(body)} chars)', flush=True) await asyncio.sleep(delay) msg = self.make_message(mto=to, mbody=body, mtype=mtype) msg.send() except Exception as e: - print(f'[{_ts()}] SEND ERROR: {e}') + print(f'[{_ts()}] SEND ERROR: {e}', flush=True) def _timeout_session(self, session_id, mtype): - print(f'[{_ts()}] Session timeout: {session_id}') + print(f'[{_ts()}] Session timeout: {session_id}', flush=True) self._schedule_send(session_id, 'Sesi ditutup. Sampai jumpa', mtype) self._session_mgr.reset(session_id) def start(self): - print(f'[{_ts()}] Starting XMPP service...') + print(f'[{_ts()}] Starting XMPP service...', flush=True) asyncio.run(self._run()) async def _run(self): @@ -485,7 +485,7 @@ class XMPPClient(ClientXMPP): await self._stopped.wait() except (asyncio.CancelledError, KeyboardInterrupt): pass - print(f'[{_ts()}] Shutting down...') + print(f'[{_ts()}] Shutting down...', flush=True) await self.disconnect() def stop(self):