From 07dd99ded44312b44b3c4a06c761693712542b6f Mon Sep 17 00:00:00 2001 From: Dita Aji Pratama Date: Tue, 16 Jun 2026 22:49:00 +0700 Subject: [PATCH] pakai agent loop yang modular --- services/xmpp_client.py | 203 ++++++++++++++++++++-------------------- 1 file changed, 100 insertions(+), 103 deletions(-) diff --git a/services/xmpp_client.py b/services/xmpp_client.py index f58cb7a..5e0222c 100644 --- a/services/xmpp_client.py +++ b/services/xmpp_client.py @@ -1,5 +1,4 @@ import asyncio -import json import random import signal import threading @@ -7,6 +6,7 @@ from datetime import datetime from slixmpp import ClientXMPP from services.session_manager import SessionManager +from services.agent_loop import run_agent_loop import config from tools.roleplayer import should_respond @@ -306,7 +306,54 @@ class XMPPClient(ClientXMPP): if self._loop and not self._loop.is_closed(): asyncio.run_coroutine_threadsafe(_read_delay(), self._loop) - self._agent_loop(session, jid, body, 'chat', sender_nickname=jid) + my_name = PERSONALITY.name + quote = body + + def on_tool_calls(tnames): + if not is_roleplay: + self._schedule_send(jid, f'> {quote}\nUsing: {", ".join(tnames)}', 'chat') + + final_content = run_agent_loop( + session, self._llm, self._TOOLS, self._TOOL_HANDLERS, + self._max_iterations, on_tool_calls=on_tool_calls + ) + + if final_content is not None: + if is_roleplay: + if config.XMPP_SELECTIVE_RESPONSE: + recent_msgs = [] + for msg in session.messages[-6:]: + if msg.get('role') == 'user': + recent_msgs.append(f"User: {msg.get('content', '')}") + elif msg.get('role') == 'assistant' and msg.get('content'): + recent_msgs.append(f"{my_name}: {msg.get('content', '')}") + recent_history = "\n".join(recent_msgs) + + if should_respond( + message=quote, + sender_nickname=jid, + recent_history=recent_history, + my_name=my_name, + ): + print(f'[{_ts()}] need_response=True → sending response') + self._schedule_send(jid, final_content, 'chat') + else: + print(f'[{_ts()}] need_response=False → staying silent') + else: + from tools.roleplayer import _name_mentioned + if _name_mentioned(my_name, quote): + print(f'[{_ts()}] Name mentioned → sending response') + self._schedule_send(jid, final_content, 'chat') + else: + print(f'[{_ts()}] Name not mentioned → staying silent') + else: + self._schedule_send(jid, f'> {quote}\n{final_content}', 'chat') + else: + msg = 'Max iterations reached without final answer.' + if is_roleplay: + self._schedule_send(jid, msg, 'chat') + else: + self._schedule_send(jid, f'> {quote}\n{msg}', 'chat') # DM: timeout 24 jam (efektif tidak auto-close), MUC tetap 5 menit session.start_timer(86400, self._timeout_session, jid, 'chat') @@ -337,110 +384,60 @@ class XMPPClient(ClientXMPP): if self._loop and not self._loop.is_closed(): asyncio.run_coroutine_threadsafe(_read_delay(), self._loop) - self._agent_loop(session, room, f'[{nick}] {body}', 'groupchat', sender_nickname=nick) + my_name = PERSONALITY.name + quote = f'[{nick}] {body}' + + def on_tool_calls(tnames): + if not is_roleplay: + self._schedule_send(room, f'> {quote}\nUsing: {", ".join(tnames)}', 'groupchat') + + final_content = run_agent_loop( + session, self._llm, self._TOOLS, self._TOOL_HANDLERS, + self._max_iterations, on_tool_calls=on_tool_calls + ) + + if final_content is not None: + if is_roleplay: + if config.XMPP_SELECTIVE_RESPONSE: + recent_msgs = [] + for msg in session.messages[-6:]: + if msg.get('role') == 'user': + recent_msgs.append(f"User: {msg.get('content', '')}") + elif msg.get('role') == 'assistant' and msg.get('content'): + recent_msgs.append(f"{my_name}: {msg.get('content', '')}") + recent_history = "\n".join(recent_msgs) + + if should_respond( + message=quote, + sender_nickname=nick, + recent_history=recent_history, + my_name=my_name, + ): + print(f'[{_ts()}] need_response=True → sending response') + self._schedule_send(room, final_content, 'groupchat') + else: + print(f'[{_ts()}] need_response=False → staying silent') + else: + from tools.roleplayer import _name_mentioned + if _name_mentioned(my_name, quote): + print(f'[{_ts()}] Name mentioned → sending response') + self._schedule_send(room, final_content, 'groupchat') + else: + print(f'[{_ts()}] Name not mentioned → staying silent') + else: + self._schedule_send(room, f'> {quote}\n{final_content}', 'groupchat') + else: + msg = 'Max iterations reached without final answer.' + if is_roleplay: + self._schedule_send(room, msg, 'groupchat') + else: + self._schedule_send(room, f'> {quote}\n{msg}', 'groupchat') session.start_timer(300, self._timeout_session, room, 'groupchat') - def _agent_loop(self, session, to, quote, mtype, sender_nickname=""): - is_roleplayer = self._skill == 'roleplayer' - my_name = PERSONALITY.name - - for step in range(self._max_iterations): - print(f'[{_ts()}] Step {step + 1} — calling LLM...') - response = self._llm.chat(session.messages, tools=self._TOOLS) - - if response.tool_calls: - amsg = { - 'role': 'assistant', - 'content': response.content, - 'tool_calls': response.tool_calls, - } - session.messages.append(amsg) - - tnames = [tc['function']['name'] for tc in response.tool_calls] - print(f'[{_ts()}] Using tools: {", ".join(tnames)}') - - # Roleplayer tidak perlu kirim status "Using: ..." - if not is_roleplayer: - self._schedule_send(to, f'> {quote}\nUsing: {", ".join(tnames)}', mtype) - - for tc in response.tool_calls: - result = self._execute_tool(tc) - session.messages.append({ - 'role': 'tool', - 'tool_call_id': tc['id'], - 'content': str(result), - }) - else: - if response.content: - print(f'[{_ts()}] Response generated ({len(response.content)} chars)') - session.messages.append({'role': 'assistant', 'content': response.content}) - - # ── Roleplayer: cek need_response sebelum kirim ── - if is_roleplayer: - if config.XMPP_SELECTIVE_RESPONSE: - # Build recent history dari session messages (tanpa system prompt) - recent_msgs = [] - for msg in session.messages[-6:]: - if msg.get('role') == 'user': - recent_msgs.append(f"User: {msg.get('content', '')}") - elif msg.get('role') == 'assistant' and msg.get('content'): - recent_msgs.append(f"{my_name}: {msg.get('content', '')}") - recent_history = "\n".join(recent_msgs) - - original_message = quote - if should_respond( - message=original_message, - sender_nickname=sender_nickname, - recent_history=recent_history, - my_name=my_name, - ): - print(f'[{_ts()}] need_response=True → sending response') - self._schedule_send(to, response.content, mtype) - else: - print(f'[{_ts()}] need_response=False → staying silent') - else: - # Selective response OFF: cuma respon kalau nama AI disebut di pesan - from tools.roleplayer import _name_mentioned - if _name_mentioned(my_name, quote): - print(f'[{_ts()}] Name mentioned → sending response') - self._schedule_send(to, response.content, mtype) - else: - print(f'[{_ts()}] Name not mentioned → staying silent') - else: - self._schedule_send(to, f'> {quote}\n{response.content}', mtype) - return - - print(f'[{_ts()}] Max iterations ({self._max_iterations}) reached') - session.messages.append({ - 'role': 'assistant', - 'content': 'Max iterations reached without final answer.', - }) - - if is_roleplayer: - self._schedule_send(to, 'Max iterations reached without final answer.', mtype) - else: - self._schedule_send(to, f'> {quote}\nMax iterations reached without final answer.', mtype) - def _execute_tool(self, tool_call): - tname = tool_call['function']['name'] - targs = json.loads(tool_call['function']['arguments']) - handler = self._TOOL_HANDLERS.get(tname) - if not handler: - return f'Tool {tname} not found' - try: - if tname == 'search_code': - return handler( - pattern=targs['pattern'], - search_type=targs['search_type'], - path=targs.get('path', '.'), - ) - elif tname == 'git_operation': - return handler(args=targs['args']) - else: - return handler(**targs) - except Exception as e: - return f'Error executing tool: {str(e)}' + from services.agent_loop import execute_tool + return execute_tool(tool_call, self._TOOL_HANDLERS) def _schedule_send(self, to, body, mtype='chat'): if self._loop and not self._loop.is_closed(): @@ -480,7 +477,7 @@ class XMPPClient(ClientXMPP): # dan kita tidak mau proses mati karena itu. try: self._loop.add_signal_handler(signal.SIGTERM, self._stopped.set) - except NotImplementedError: + except (NotImplementedError, RuntimeError): pass await self.connect()