This commit is contained in:
Dita Aji Pratama 2026-06-10 15:12:50 +07:00
parent dc5fb67ac1
commit 8a0363b985
7 changed files with 355 additions and 100 deletions

View File

@ -14,3 +14,16 @@ XMPP_USERNAME=
XMPP_PASSWORD= XMPP_PASSWORD=
# XMPP_MUC_ROOMS=room1@conference.server,room2@conference.server # XMPP_MUC_ROOMS=room1@conference.server,room2@conference.server
# ─── Persona / Mode ──────────────────────────────────────────────────────────
# Mode AI: "programmer" (default, koding) | "roleplayer" (ngobrol)
PERSONA_MODE=programmer
# Personality
PERSONA_NAME=OWL
PERSONA_TONE=casual # casual | formal | playful | warm
PERSONA_VERBOSITY=balanced # concise | balanced | detailed
PERSONA_HUMOR=light # none | light | witty
PERSONA_LANGUAGE=id # id | en | (kosong = auto)
PERSONA_MOOD=cheerful # cheerful | calm | energetic | sarcastic
# PERSONA_CATCHPHRASES=Siap bro!, Haha~, Wkwkwk

View File

@ -3,27 +3,54 @@ from dotenv import load_dotenv
load_dotenv() load_dotenv()
# LLM Configuration
llm_baseurl = os.getenv("LLM_BASE_URL", default="http://localhost:11434/v1" ) llm_baseurl = os.getenv("LLM_BASE_URL", default="http://localhost:11434/v1" )
llm_model = os.getenv("LLM_MODEL", default="granite4.1:8b" ) llm_model = os.getenv("LLM_MODEL", default="granite4.1:8b" )
llm_api_key = os.getenv("LLM_API_KEY", default="ollama" ) llm_api_key = os.getenv("LLM_API_KEY", default="ollama" )
llm_timeout = int( os.getenv("LLM_TIMEOUT", default="600" ) ) llm_timeout = int( os.getenv("LLM_TIMEOUT", default="600" ) )
# Agent Configuration
AGENT_MAX_ITERATIONS = int( os.getenv("AGENT_MAX_ITERATIONS", default="10" ) ) AGENT_MAX_ITERATIONS = int( os.getenv("AGENT_MAX_ITERATIONS", default="30" ) )
# Tool Configuration (for future use)
MAX_TOOL_OUTPUT = int( os.getenv("MAX_TOOL_OUTPUT", default="4000" ) ) MAX_TOOL_OUTPUT = int( os.getenv("MAX_TOOL_OUTPUT", default="40000" ) )
# RAG Configuration
RAG_PERSIST_DIR = os.getenv("RAG_PERSIST_DIR", default="chroma_db" ) RAG_PERSIST_DIR = os.getenv("RAG_PERSIST_DIR", default="chroma_db" ) # Embedding: ChromaDB ONNX default (all-MiniLM-L6-v2, lokal, tidak perlu API call)
# Embedding: ChromaDB ONNX default (all-MiniLM-L6-v2, lokal, tidak perlu API call)
# XMPP Configuration
XMPP_ENABLED = os.getenv("XMPP_ENABLED", default="False" ).strip().lower() in ("true", "1", "yes") XMPP_ENABLED = os.getenv("XMPP_ENABLED", default="False" ).strip().lower() in ("true", "1", "yes")
XMPP_USERNAME = os.getenv("XMPP_USERNAME", default="" ) XMPP_USERNAME = os.getenv("XMPP_USERNAME", default="" )
XMPP_PASSWORD = os.getenv("XMPP_PASSWORD", default="" ) XMPP_PASSWORD = os.getenv("XMPP_PASSWORD", default="" )
XMPP_MUC_ROOMS = os.getenv("XMPP_MUC_ROOMS", default="" ) XMPP_MUC_ROOMS = os.getenv("XMPP_MUC_ROOMS", default="" )
XMPP_NICKNAME = os.getenv("XMPP_NICKNAME", default="" ) # custom nick MUC (kosong = pakai username) XMPP_NICKNAME = os.getenv("XMPP_NICKNAME", default="" ) # custom nick MUC (empty = use username)
# ─── Persona / Mode Configuration ────────────────────────────────────────────
# Pilihan mode AI:
# "programmer" → AI Agent untuk koding (default), tool-focused
# "roleplayer" → Teman ngobrol / chat companion, conversational
PERSONA_MODE = os.getenv("PERSONA_MODE", default="programmer").strip().lower()
# Personality — nama panggilan AI (default: "OWL")
PERSONA_NAME = os.getenv("PERSONA_NAME", default="OWL").strip() or "OWL"
# Gaya bicara: "casual" | "formal" | "playful" | "warm"
PERSONA_TONE = os.getenv("PERSONA_TONE", default="casual").strip().lower() or "casual"
# Panjang jawaban: "concise" | "balanced" | "detailed"
PERSONA_VERBOSITY = os.getenv("PERSONA_VERBOSITY", default="balanced").strip().lower() or "balanced"
# Humor: "none" | "light" | "witty"
PERSONA_HUMOR = os.getenv("PERSONA_HUMOR", default="light").strip().lower() or "light"
# Bahasa: "id" | "en" | "" (auto)
PERSONA_LANGUAGE = os.getenv("PERSONA_LANGUAGE", default="id").strip().lower() or "id"
# Mood: "cheerful" | "calm" | "energetic" | "sarcastic"
PERSONA_MOOD = os.getenv("PERSONA_MOOD", default="cheerful").strip().lower() or "cheerful"
# Catchphrases khas AI (comma-separated)
# Contoh: "Siap bro!, Haha~, Wkwkwk"
PERSONA_CATCHPHRASES = os.getenv("PERSONA_CATCHPHRASES", default="").strip()
# Humanize Delay Configuration (anti-bot detection) # Humanize Delay Configuration (anti-bot detection)
READ_DELAY_MIN = float( os.getenv("READ_DELAY_MIN", default="1.0" ) ) # min reading delay (detik) READ_DELAY_MIN = float( os.getenv("READ_DELAY_MIN", default="1.0" ) ) # min reading delay (second)
READ_DELAY_MAX = float( os.getenv("READ_DELAY_MAX", default="2.0" ) ) # max reading delay (detik) READ_DELAY_MAX = float( os.getenv("READ_DELAY_MAX", default="2.0" ) ) # max reading delay (second)
TYPING_SPEED = float( os.getenv("TYPING_SPEED", default="15.0" ) ) # karakter per detik (kecepatan mengetik) TYPING_SPEED = float( os.getenv("TYPING_SPEED", default="15.0" ) ) # characters per second
TYPING_MAX = float( os.getenv("TYPING_MAX", default="10.0" ) ) # batas max typing delay (detik) TYPING_MAX = float( os.getenv("TYPING_MAX", default="10.0" ) ) # max typing delay limit (second)

View File

@ -1,39 +1,42 @@
import os, sys import os, sys
import config import config
from services.xmpp_client import XMPPClient
from scripts.llm_client import LLMClient from scripts.llm_client import LLMClient
from tools import coder, rag, carrack from tools import coder, rag, carrack
from scripts import gadget from scripts import gadget
from scripts.persona import build_system_prompt, PERSONALITY, MODE
# Daftar tools yang tersedia
tools_definition = [ tools_definition = [
gadget.tools_mapping( schema = coder.schema_read_file, handler = coder.read_file ),
gadget.tools_mapping( schema = coder.schema_write_file, handler = coder.write_file ), gadget.tools_mapping( schema = coder.schema_read_file, handler = coder.read_file ),
gadget.tools_mapping( schema = coder.schema_edit_file, handler = coder.edit_file ), gadget.tools_mapping( schema = coder.schema_write_file, handler = coder.write_file ),
gadget.tools_mapping( schema = coder.schema_run_bash, handler = coder.run_bash ), gadget.tools_mapping( schema = coder.schema_edit_file, handler = coder.edit_file ),
gadget.tools_mapping( schema = coder.schema_search_code, handler = coder.search_code ), gadget.tools_mapping( schema = coder.schema_run_bash, handler = coder.run_bash ),
gadget.tools_mapping( schema = coder.schema_git_operation, handler = coder.git_operation ), gadget.tools_mapping( schema = coder.schema_search_code, handler = coder.search_code ),
gadget.tools_mapping( schema = rag.schema_ingest_files, handler = rag.ingest_files ), gadget.tools_mapping( schema = coder.schema_git_operation, handler = coder.git_operation ),
gadget.tools_mapping( schema = rag.schema_store_knowledge, handler = rag.store_knowledge ),
gadget.tools_mapping( schema = rag.schema_search_knowledge, handler = rag.search_knowledge ), gadget.tools_mapping( schema = rag.schema_ingest_files, handler = rag.ingest_files ),
gadget.tools_mapping( schema = rag.schema_store_knowledge, handler = rag.store_knowledge ),
gadget.tools_mapping( schema = rag.schema_search_knowledge, handler = rag.search_knowledge ),
gadget.tools_mapping( schema = rag.schema_create_collection, handler = rag.create_collection ), gadget.tools_mapping( schema = rag.schema_create_collection, handler = rag.create_collection ),
gadget.tools_mapping( schema = rag.schema_delete_collection, handler = rag.delete_collection ), gadget.tools_mapping( schema = rag.schema_delete_collection, handler = rag.delete_collection ),
gadget.tools_mapping( schema = rag.schema_list_collections, handler = rag.list_collections ), gadget.tools_mapping( schema = rag.schema_list_collections, handler = rag.list_collections ),
gadget.tools_mapping( schema = rag.schema_inspect_collection, handler = rag.inspect_collection ), gadget.tools_mapping( schema = rag.schema_inspect_collection, handler = rag.inspect_collection ),
gadget.tools_mapping( schema = carrack.schema_sendhttprequest, handler = carrack.sendhttprequest ), gadget.tools_mapping( schema = carrack.schema_sendhttprequest, handler = carrack.sendhttprequest ),
] ]
# Ekstrak dari tools_definition ke dua format berbeda
TOOLS = gadget.tool_schemas (tools_definition) TOOLS = gadget.tool_schemas (tools_definition)
TOOL_HANDLERS = gadget.tool_handlers (tools_definition) TOOL_HANDLERS = gadget.tool_handlers (tools_definition)
def main(): def main():
llm_client = LLMClient(config.llm_baseurl, config.llm_model, config.llm_api_key, config.llm_timeout) llm_client = LLMClient(config.llm_baseurl, config.llm_model, config.llm_api_key, config.llm_timeout)
# Parsing arguments `-w <dir>` atau `--workspace <dir>` workspace = None
workspace = None i = 1
i = 1
while i < len(sys.argv): while i < len(sys.argv):
if sys.argv[i] in ('-w', '--workspace') and i + 1 < len(sys.argv): if sys.argv[i] in ('-w', '--workspace') and i + 1 < len(sys.argv):
workspace = sys.argv[i + 1] workspace = sys.argv[i + 1]
@ -41,7 +44,6 @@ def main():
else: else:
i += 1 i += 1
# Apply workspace jika ada
if workspace: if workspace:
resolved = os.path.abspath(workspace) resolved = os.path.abspath(workspace)
if not os.path.isdir(resolved): if not os.path.isdir(resolved):
@ -50,24 +52,21 @@ def main():
os.chdir(resolved) os.chdir(resolved)
if config.XMPP_ENABLED: if config.XMPP_ENABLED:
from services.xmpp_client import XMPPClient
muc_rooms = [] muc_rooms = []
if config.XMPP_MUC_ROOMS.strip(): if config.XMPP_MUC_ROOMS.strip():
muc_rooms = [r.strip() for r in config.XMPP_MUC_ROOMS.split(',') if r.strip()] muc_rooms = [r.strip() for r in config.XMPP_MUC_ROOMS.split(',') if r.strip()]
client = XMPPClient( client = XMPPClient(
jid = config.XMPP_USERNAME, jid = config.XMPP_USERNAME,
password = config.XMPP_PASSWORD, password = config.XMPP_PASSWORD,
llm_client = llm_client, llm_client = llm_client,
tools_definition = tools_definition, tools_definition = tools_definition,
TOOLS = TOOLS, TOOLS = TOOLS,
TOOL_HANDLERS = TOOL_HANDLERS, TOOL_HANDLERS = TOOL_HANDLERS,
build_system_prompt = gadget.build_system_prompt, build_system_prompt = build_system_prompt,
agent_max_iterations= config.AGENT_MAX_ITERATIONS, agent_max_iterations = config.AGENT_MAX_ITERATIONS,
muc_rooms = muc_rooms, muc_rooms = muc_rooms,
) )
client.start() # blocking, headless client.start()
else: else:
from tui import HendrikTUI from tui import HendrikTUI
HendrikTUI( HendrikTUI(
@ -75,10 +74,10 @@ def main():
tools_definition = tools_definition, tools_definition = tools_definition,
TOOLS = TOOLS, TOOLS = TOOLS,
TOOL_HANDLERS = TOOL_HANDLERS, TOOL_HANDLERS = TOOL_HANDLERS,
build_system_prompt = gadget.build_system_prompt, build_system_prompt = build_system_prompt,
agent_max_iterations = config.AGENT_MAX_ITERATIONS, agent_max_iterations = config.AGENT_MAX_ITERATIONS,
).run() ).run()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -1,57 +1,12 @@
import os from .persona import build_system_prompt
def tools_mapping(schema, handler, name=None): def tools_mapping(schema, handler, name=None):
tool_name = name or schema["function"]["name"] tool_name = name or schema["function"]["name"]
return {"name": tool_name, "schema": schema, "handler": handler} return {"name": tool_name, "schema": schema, "handler": handler}
def tool_schemas(tools_definition): def tool_schemas(tools_definition):
return [t["schema"] for t in tools_definition] return [t["schema"] for t in tools_definition]
def tool_handlers(tools_definition): def tool_handlers(tools_definition):
return {t["name"]: t["handler"] for t in tools_definition} return {t["name"]: t["handler"] for t in tools_definition}
def build_system_prompt(tools_definition):
lines = [
"You are a coding agent that assists with software engineering tasks. "
"You have access to the following tools:",
""
]
for i, tool in enumerate(tools_definition, 1):
name = tool["name"]
desc = tool["schema"]["function"]["description"]
lines.append(f"{i}. {name}: {desc}")
lines.extend([
"",
"Use tools by returning tool calls when needed. After receiving tool "
"results, continue your reasoning. When you have the final answer, "
"return it as plain text without tool calls.",
"",
f"Your workspace directory is: {os.getcwd()}. "
"All file operations are relative to this directory.",
"",
"⚠️ GIT POLICY — IMPORTANT:",
"- NEVER run 'git add' or 'git commit' automatically after making changes.",
"- After editing/creating files, always ASK the user first before committing.",
"- Only run git commands when the user explicitly asks you to commit.",
"- You may run 'git status', 'git diff', 'git log' freely to inspect state.",
"- When user asks to commit: show them the changes first, then wait for confirmation.",
"",
"RAG capabilities (knowledge retrieval):",
"- list_collections → see available collections & doc counts.",
"- create_collection → create a new collection for a new topic.",
"- delete_collection → permanently remove a collection and its data.",
"- inspect_collection → learn metadata fields before searching.",
"- search_knowledge → semantic search + optional metadata filter.",
"- store_knowledge → save docs with rich metadata for later use.",
"- ingest_files → read files (with glob patterns) into a collection, auto-chunking.",
"",
"You can create collections yourself! When you encounter a new topic,",
"use create_collection first, then store_knowledge or ingest_files to populate it.",
"Always inspect_collection to discover metadata keys before filtering."
])
return "\n".join(lines)

244
scripts/persona.py Normal file
View File

@ -0,0 +1,244 @@
import os
from dataclasses import dataclass, field
# ─── Mode / Skill ────────────────────────────────────────────────────────────
# Pilihan mode AI:
# - "programmer" : AI Agent untuk koding (default), tool-focused, task-oriented
# - "roleplayer" : Teman ngobrol / chat companion, conversational, expressive
MODE = os.getenv("PERSONA_MODE", default="programmer").strip().lower()
# ─── Personality Configuration ───────────────────────────────────────────────
# Semua parameter personality bisa di-set via .env.
# Lihat komentar di setiap field untuk pilihan yang tersedia.
@dataclass
class PersonalityConfig:
"""Konfigurasi personality AI yang berlaku lintas mode."""
# Nama panggilan AI (default: "OWL")
name: str = "OWL"
# Gaya bicara:
# "casual" → santai, gaul
# "formal" → sopan, profesional
# "playful" → ceria, suka bercanda
# "warm" → hangat, ramah
tone: str = "casual"
# Panjang jawaban:
# "concise" → singkat, to the point
# "balanced" → sedang (default)
# "detailed" → panjang, detail
verbosity: str = "balanced"
# Seberapa sering bercanda:
# "none" → serius, tidak bercanda
# "light" → sesekali (default)
# "witty" → sering, jenaka
humor_level: str = "light"
# Bahasa utama:
# "id" → Indonesia
# "en" → English
# "" → auto (sesuai bahasa user)
language: str = "id"
# Suasana hati umum:
# "cheerful" → ceria, positif
# "calm" → tenang, menenangkan
# "energetic" → bersemangat, aktif
# "sarcastic" → sarkastik, sinis
mood: str = "cheerful"
# Ekspresi khas AI (comma-separated di .env, jadi list di sini)
# Contoh: "Siap bro!, Haha~, Wkwkwk"
catchphrases: list = field(default_factory=list)
def _load_personality_from_env() -> PersonalityConfig:
"""Baca personality config dari environment variables."""
raw_catchphrases = os.getenv("PERSONA_CATCHPHRASES", default="").strip()
catchphrases = [c.strip() for c in raw_catchphrases.split(",") if c.strip()] if raw_catchphrases else []
return PersonalityConfig(
name=os.getenv("PERSONA_NAME", default="OWL").strip() or "OWL",
tone=os.getenv("PERSONA_TONE", default="casual").strip().lower() or "casual",
verbosity=os.getenv("PERSONA_VERBOSITY", default="balanced").strip().lower() or "balanced",
humor_level=os.getenv("PERSONA_HUMOR", default="light").strip().lower() or "light",
language=os.getenv("PERSONA_LANGUAGE", default="id").strip().lower() or "id",
mood=os.getenv("PERSONA_MOOD", default="cheerful").strip().lower() or "cheerful",
catchphrases=catchphrases,
)
# Instance global — di-load sekali saat import
PERSONALITY = _load_personality_from_env()
# ─── Prompt Builders ─────────────────────────────────────────────────────────
def _build_personality_block(cfg: PersonalityConfig) -> str:
"""Generate deskripsi personality dari config."""
parts = [f"You are {cfg.name}."]
# Tone
tone_map = {
"casual": "You speak in a casual, relaxed manner — like chatting with a friend.",
"formal": "You speak formally and professionally, using polite language.",
"playful": "You are playful and cheerful, making conversations fun and lighthearted.",
"warm": "You are warm and friendly, making people feel comfortable and welcomed.",
}
parts.append(tone_map.get(cfg.tone, tone_map["casual"]))
# Verbosity
verbosity_map = {
"concise": "Keep your answers short and to the point.",
"balanced": "Provide balanced answers — not too brief, not too long.",
"detailed": "Give thorough, detailed answers with explanations.",
}
parts.append(verbosity_map.get(cfg.verbosity, verbosity_map["balanced"]))
# Humor
humor_map = {
"none": "Stay serious and avoid jokes.",
"light": "Occasionally sprinkle in light humor when appropriate.",
"witty": "Be witty and humorous — jokes, puns, and playful banter are welcome.",
}
parts.append(humor_map.get(cfg.humor_level, humor_map["light"]))
# Language
if cfg.language == "id":
parts.append("Always respond in Indonesian (Bahasa Indonesia).")
elif cfg.language == "en":
parts.append("Always respond in English.")
else:
parts.append("Respond in the same language the user uses.")
# Mood
mood_map = {
"cheerful": "Your overall mood is cheerful and positive.",
"calm": "Your overall mood is calm and soothing.",
"energetic": "Your overall mood is energetic and enthusiastic.",
"sarcastic": "Your overall mood is sarcastic and dry-humored.",
}
parts.append(mood_map.get(cfg.mood, mood_map["cheerful"]))
# Catchphrases
if cfg.catchphrases:
phrases = ", ".join(f'"{p}"' for p in cfg.catchphrases)
parts.append(f"You sometimes use these catchphrases: {phrases}.")
return "\n".join(parts)
def _build_tools_block(tools_definition: list[dict]) -> str:
"""Generate daftar tools dari tools_definition."""
lines = [
"You have access to the following tools:",
"",
]
for i, tool in enumerate(tools_definition, 1):
name = tool["name"]
desc = tool["schema"]["function"]["description"]
lines.append(f"{i}. {name}: {desc}")
lines.append("")
lines.append(
"Use tools by returning tool calls when needed. After receiving tool "
"results, continue your reasoning. When you have the final answer, "
"return it as plain text without tool calls."
)
return "\n".join(lines)
def _build_programmer_prompt(cfg: PersonalityConfig, tools_definition: list[dict]) -> str:
"""Build system prompt untuk mode Programmer."""
lines = [
_build_personality_block(cfg),
"",
"You are a coding agent that assists with software engineering tasks.",
"",
_build_tools_block(tools_definition),
"",
f"Your workspace directory is: {os.getcwd()}. "
"All file operations are relative to this directory.",
"",
"⚠️ GIT POLICY — IMPORTANT:",
"- NEVER run 'git add' or 'git commit' automatically after making changes.",
"- After editing/creating files, always ASK the user first before committing.",
"- Only run git commands when the user explicitly asks you to commit.",
"- You may run 'git status', 'git diff', 'git log' freely to inspect state.",
"- When user asks to commit: show them the changes first, then wait for confirmation.",
"",
"RAG capabilities (knowledge retrieval):",
"- list_collections → see available collections & doc counts.",
"- create_collection → create a new collection for a new topic.",
"- delete_collection → permanently remove a collection and its data.",
"- inspect_collection → learn metadata fields before searching.",
"- search_knowledge → semantic search + optional metadata filter.",
"- store_knowledge → save docs with rich metadata for later use.",
"- ingest_files → read files (with glob patterns) into a collection, auto-chunking.",
"",
"You can create collections yourself! When you encounter a new topic,",
"use create_collection first, then store_knowledge or ingest_files to populate it.",
"Always inspect_collection to discover metadata keys before filtering.",
]
return "\n".join(lines)
def _build_roleplayer_prompt(cfg: PersonalityConfig) -> str:
"""Build system prompt untuk mode Roleplayer."""
lines = [
_build_personality_block(cfg),
"",
f"You are {cfg.name}, a conversational companion and roleplayer. "
"Your main purpose is to be an engaging, empathetic, and fun conversation partner.",
"",
"Guidelines:",
"- Stay in character at all times. Be consistent with your personality.",
"- Be responsive and empathetic — acknowledge the user's feelings and thoughts.",
"- Ask follow-up questions to keep the conversation flowing naturally.",
"- Use natural, conversational language — not robotic or overly formal.",
"- If the user wants to roleplay a scenario, dive into it enthusiastically.",
"- Adapt your tone and energy to match the mood of the conversation.",
"- Keep the conversation comfortable and enjoyable.",
"",
"Note: You currently do not have access to external tools. "
"Focus on being a great conversationalist!",
]
return "\n".join(lines)
# ─── Public API ──────────────────────────────────────────────────────────────
def build_system_prompt(
tools_definition: list[dict] | None = None,
mode: str | None = None,
personality: PersonalityConfig | None = None,
) -> str:
"""
Build system prompt berdasarkan mode dan personality config.
Args:
tools_definition: Daftar tools (required untuk mode programmer).
mode: "programmer" atau "roleplayer". Default: dari env PERSONA_MODE.
personality: PersonalityConfig instance. Default: dari env (global PERSONALITY).
Returns:
String system prompt lengkap.
"""
selected_mode = (mode or MODE).strip().lower()
cfg = personality or PERSONALITY
if selected_mode == "programmer":
if tools_definition is None:
raise ValueError("tools_definition is required for 'programmer' mode")
return _build_programmer_prompt(cfg, tools_definition)
elif selected_mode == "roleplayer":
return _build_roleplayer_prompt(cfg)
else:
raise ValueError(
f"Unknown mode: '{selected_mode}'. "
f"Available modes: 'programmer', 'roleplayer'"
)

View File

@ -9,6 +9,7 @@ from slixmpp import ClientXMPP
from services.session_manager import SessionManager from services.session_manager import SessionManager
import config import config
from scripts.persona import MODE as PERSONA_MODE
# Anti-ban: delay constants for MUC rejoin behavior # Anti-ban: delay constants for MUC rejoin behavior
MUC_REJOIN_INITIAL_DELAY = 5.0 # detik, delay awal sebelum rejoin MUC_REJOIN_INITIAL_DELAY = 5.0 # detik, delay awal sebelum rejoin
@ -47,6 +48,7 @@ class XMPPClient(ClientXMPP):
self._TOOL_HANDLERS = TOOL_HANDLERS self._TOOL_HANDLERS = TOOL_HANDLERS
self._build_system_prompt = build_system_prompt self._build_system_prompt = build_system_prompt
self._max_iterations = agent_max_iterations self._max_iterations = agent_max_iterations
self._mode = PERSONA_MODE
self._muc_rooms = muc_rooms or [] self._muc_rooms = muc_rooms or []
# Custom nick dari config, fallback ke username JID # Custom nick dari config, fallback ke username JID
self._muc_nick = config.XMPP_NICKNAME.strip() or jid.split('@')[0] self._muc_nick = config.XMPP_NICKNAME.strip() or jid.split('@')[0]
@ -277,7 +279,7 @@ class XMPPClient(ClientXMPP):
def _process_dm(self, jid, body): def _process_dm(self, jid, body):
session = self._session_mgr.get_or_create( session = self._session_mgr.get_or_create(
jid, self._build_system_prompt(self._tools_def) jid, self._build_system_prompt(tools_definition=self._tools_def)
) )
session.cancel_timer() session.cancel_timer()
@ -291,7 +293,8 @@ class XMPPClient(ClientXMPP):
session.add_message('user', body) session.add_message('user', body)
self._schedule_send(jid, f'> {body}\nThinking...') if self._mode != 'roleplayer':
self._schedule_send(jid, f'> {body}\nThinking...')
# Delay 1: simulasi membaca pesan user # Delay 1: simulasi membaca pesan user
if self._loop and not self._loop.is_closed(): if self._loop and not self._loop.is_closed():
@ -303,7 +306,7 @@ class XMPPClient(ClientXMPP):
def _process_muc(self, room, nick, body): def _process_muc(self, room, nick, body):
session = self._session_mgr.get_or_create( session = self._session_mgr.get_or_create(
room, self._build_system_prompt(self._tools_def) room, self._build_system_prompt(tools_definition=self._tools_def)
) )
session.cancel_timer() session.cancel_timer()
@ -316,7 +319,8 @@ class XMPPClient(ClientXMPP):
prefixed = f'[{nick}] {body}' prefixed = f'[{nick}] {body}'
session.add_message('user', prefixed) session.add_message('user', prefixed)
self._schedule_send(room, f'> [{nick}] {body}\nThinking...', mtype='groupchat') if self._mode != 'roleplayer':
self._schedule_send(room, f'> [{nick}] {body}\nThinking...', mtype='groupchat')
# Delay 1: simulasi membaca pesan user # Delay 1: simulasi membaca pesan user
if self._loop and not self._loop.is_closed(): if self._loop and not self._loop.is_closed():
@ -327,6 +331,8 @@ class XMPPClient(ClientXMPP):
session.start_timer(300, self._timeout_session, room, 'groupchat') session.start_timer(300, self._timeout_session, room, 'groupchat')
def _agent_loop(self, session, to, quote, mtype): def _agent_loop(self, session, to, quote, mtype):
is_roleplayer = self._mode == 'roleplayer'
for step in range(self._max_iterations): for step in range(self._max_iterations):
print(f'[{_ts()}] Step {step + 1} — calling LLM...') print(f'[{_ts()}] Step {step + 1} — calling LLM...')
response = self._llm.chat(session.messages, tools=self._TOOLS) response = self._llm.chat(session.messages, tools=self._TOOLS)
@ -341,7 +347,10 @@ class XMPPClient(ClientXMPP):
tnames = [tc['function']['name'] for tc in response.tool_calls] tnames = [tc['function']['name'] for tc in response.tool_calls]
print(f'[{_ts()}] Using tools: {", ".join(tnames)}') print(f'[{_ts()}] Using tools: {", ".join(tnames)}')
self._schedule_send(to, f'> {quote}\nUsing: {", ".join(tnames)}', mtype)
# 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: for tc in response.tool_calls:
result = self._execute_tool(tc) result = self._execute_tool(tc)
@ -354,7 +363,12 @@ class XMPPClient(ClientXMPP):
if response.content: if response.content:
print(f'[{_ts()}] Response sent ({len(response.content)} chars)') print(f'[{_ts()}] Response sent ({len(response.content)} chars)')
session.messages.append({'role': 'assistant', 'content': response.content}) session.messages.append({'role': 'assistant', 'content': response.content})
self._schedule_send(to, f'> {quote}\n{response.content}', mtype)
# Roleplayer: kirim langsung isinya, tanpa prefix
if is_roleplayer:
self._schedule_send(to, response.content, mtype)
else:
self._schedule_send(to, f'> {quote}\n{response.content}', mtype)
return return
print(f'[{_ts()}] Max iterations ({self._max_iterations}) reached') print(f'[{_ts()}] Max iterations ({self._max_iterations}) reached')
@ -362,7 +376,11 @@ class XMPPClient(ClientXMPP):
'role': 'assistant', 'role': 'assistant',
'content': 'Max iterations reached without final answer.', 'content': 'Max iterations reached without final answer.',
}) })
self._schedule_send(to, f'> {quote}\nMax iterations reached without final answer.', mtype)
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): def _execute_tool(self, tool_call):
tname = tool_call['function']['name'] tname = tool_call['function']['name']

View File

@ -40,8 +40,7 @@ class HendrikTUI:
stdscr.keypad(True) stdscr.keypad(True)
stdscr.refresh() stdscr.refresh()
self.messages = [{"role": "system", self.messages = [{"role": "system", "content": self.build_system_prompt(tools_definition=self.tools_def)}]
"content": self.build_system_prompt(self.tools_def)}]
log(self, "welcome", WELCOME_ART) log(self, "welcome", WELCOME_ART)
while self.running: while self.running: