Huge refactor

This commit is contained in:
Dita Aji Pratama 2026-06-14 10:56:55 +07:00
parent bf59ed5766
commit 540e166e89
8 changed files with 158 additions and 43 deletions

View File

@ -1,4 +1,4 @@
mode: programmer
skill: programmer
name: Hendrik
age: 35
gender: male

View File

@ -3,7 +3,35 @@
## Role
Kamu adalah conversational companion dan roleplayer.
Tujuan utama-mu adalah menjadi partner ngobrol yang engaging, empatik, dan menyenangkan.
## Thinking / Reasoning
- **JANGAN** pernah output thinking/reasoning sebagai respons.
- Jangan pernah output XML tag `<think>`, JSON thinking field, atau apapun yang memperlihatkan proses reasoning.
- Langsung jawab dalam karakter — no preamble, no meta-commentary, no "thinking step-by-step".
## Format Roleplay
- Dialog TIDAK perlu diapit quote (`"..."`). Cukup tulis langsung.
- Aksi/narasi ditulis dengan format *contoh aksi*.
- Contoh format:
> *Aku masuk ke ruang kerja*
> Pagi, kamu lagi ngapain?
## Penulisan XMPP
- Response roleplay dikirim sebagai pesan chat biasa (plain text langsung dalam karakter).
- Jangan bungkus dengan markdown thinking blocks atau reasoning explanation.
- Langsung output dialog atau aksi, nothing else.
## DM vs Group Chat (MUC)
### Direct Message (DM)
- Di DM, kamu BERBICARA LANGSUNG dengan user. Tidak perlu sisipkan atau mengutip (quote) pesan sebelumnya di response-mu.
- Langsung respon dalam karakter tanpa format `> {quote}`.
### Group Chat (MUC)
- Kamu TIDAK perlu merespon setiap pesan. Gunakan selective response.
## Guidelines
@ -26,7 +54,7 @@ Gunakan rules berikut untuk memutuskan apakah harus reply:
### 2. BRIEF REPLY — Respon singkat ketika:
- Seseorang bicara TENTANG-mu (mention nama di third person).
- Kamu bisa menambahkan sesuatu yang relevan atau lucu ke topik yang sedang berjalan.
- Kamu bisa menambahkan sesuatu yang relevan atau lucu ke topik yang yang sedang berjalan.
### 3. CONTEXTUAL REPLY — Respon ketika:
- Pesan berhubungan dengan topik yang sebelumnya sedang dibahas.

View File

@ -44,8 +44,8 @@ AGENT_MAX_TOOL_OUTPUT = int(os.getenv("AGENT_MAX_TOOL_OUTPUT", default=_yaml_get
# ─── Persona / Mode (YAML, bisa di-override dari .env) ──────────────────────────
PERSONA_MODE = os.getenv("PERSONA_MODE", default=_yaml_get("persona", "mode", default="programmer")).strip().lower()
PERSONA_NAME = os.getenv("PERSONA_NAME", default=_yaml_get("persona", "name", default="OWL")).strip() or "OWL"
AGENT_SKILL = os.getenv("AGENT_SKILL", default=_yaml_get("persona", "skill", default="programmer")).strip().lower()
PERSONA_NAME = os.getenv("PERSONA_NAME", default=_yaml_get("persona", "name", default="Hendrik")).strip() or "Hendrik"
PERSONA_AGE = os.getenv("PERSONA_AGE", default=_yaml_get("persona", "age", default="")).strip()
PERSONA_GENDER = os.getenv("PERSONA_GENDER", default=_yaml_get("persona", "gender", default="")).strip()
PERSONA_TONE = os.getenv("PERSONA_TONE", default=_yaml_get("persona", "tone", default="casual")).strip().lower() or "casual"
@ -102,7 +102,7 @@ if ENV_CHARACTER_CONFIG_PATH and ENV_CHARACTER_CONFIG_PATH.is_file():
_character_env[key.strip()] = value.strip()
_character_overrides = {
"PERSONA_MODE": PERSONA_MODE,
"AGENT_SKILL": AGENT_SKILL,
"PERSONA_NAME": PERSONA_NAME,
"PERSONA_AGE": PERSONA_AGE,
"PERSONA_GENDER": PERSONA_GENDER,
@ -115,7 +115,7 @@ if ENV_CHARACTER_CONFIG_PATH and ENV_CHARACTER_CONFIG_PATH.is_file():
}
for key, fallback in _character_overrides.items():
value = _character_env.get(key, fallback).strip()
if key in {"PERSONA_MODE", "PERSONA_TONE", "PERSONA_VERBOSITY", "PERSONA_HUMOR", "PERSONA_LANGUAGE", "PERSONA_MOOD"}:
if key in {"AGENT_SKILL", "PERSONA_TONE", "PERSONA_VERBOSITY", "PERSONA_HUMOR", "PERSONA_LANGUAGE", "PERSONA_MOOD"}:
value = value.lower() or fallback
if key == "PERSONA_NAME" and not value:
value = fallback

View File

@ -1,27 +1,26 @@
# Agent behavior
agent:
max_iterations: 30
max_iterations: 40
max_tool_output: 40000
# Character & Skills
character:
preset: hendrik # nama directory di agent/characters/<preset>/
skills: "" # comma-separated, e.g. "programmer,analyst"
persona:
skill: programmer # Default skill: programmer, roleplayer, analyst
character:
preset: hendrik # Directory name in agent/characters/<preset>/
skills: programmer # comma-separated, e.g. "programmer,analyst"
# XMPP
xmpp:
enabled: false
muc_rooms: "" # comma-separated, e.g. "room1@conference.server,room2@conference.server"
nickname: "" # custom nick MUC (kosong = pakai username)
selective_response: true # true = hanya respon kalau ada mention/relevansi
muc_rooms: "" # comma-separated, e.g. "room1@conference.server,room2@conference.server"
nickname: "" # custom MUC nickname (empty = use username)
selective_response: true # true = only response if mentioned/relevant
# Humanize Delay (anti-bot detection)
delay:
read_min: 1.0 # min reading delay (detik)
read_max: 2.0 # max reading delay (detik)
typing_speed: 15.0 # characters per second
typing_max: 10.0 # max typing delay limit (detik)
read_min: 1.0 # per second
read_max: 2.0 # per second
typing_speed: 15.0 # characters per second
typing_max: 10.0 # max typing delay limit per second
# RAG
rag:
persist_dir: chroma_db # ChromaDB ONNX default (all-MiniLM-L6-v2, lokal)
persist_dir: chroma_db # ChromaDB ONNX default (all-MiniLM-L6-v2, local)

View File

@ -6,7 +6,7 @@ from services.xmpp_client import XMPPClient
from scripts.llm_client import LLMClient
from tools import coder, rag, carrack
from scripts import gadget
from scripts.persona import build_system_prompt, PERSONALITY, MODE
from scripts.persona import build_system_prompt
tools_definition = [

View File

@ -1,11 +1,49 @@
import json
import re
import urllib.request
import urllib.error
def _strip_thinking(text: str) -> str:
"""
Hapus semua bentuk thinking/reasoning dari response text.
Handles:
- <think>...</think> blocks (any case)
- <reasoning>...</reasoning> blocks
- "Thinking:" / "Reasoning:" inline prefixes
"""
if not text:
return text
# Strip XML-style thinking blocks (case-insensitive, DOTALL for multiline)
text = re.sub(r'<think[^>]*>.*?</think>', '', text, flags=re.DOTALL | re.IGNORECASE)
text = re.sub(r'<reasoning[^>]*>.*?</reasoning>', '', text, flags=re.DOTALL | re.IGNORECASE)
# Strip lines starting with Thinking: / Reasoning: / Let me think...
lines = text.splitlines()
cleaned = []
skip_block = False
for line in lines:
stripped = line.strip().lower()
if stripped.startswith(('thinking:', 'reasoning:', 'let me thought', 'let me think')):
skip_block = True
continue
if skip_block and not stripped:
skip_block = False
continue
if not skip_block:
cleaned.append(line)
result = '\n'.join(cleaned).strip()
return result
class LLMClient:
class Message:
def __init__(self, msg):
self.content = msg.get('content', '')
raw_content = msg.get('content', '')
# Auto-strip thinking dari content
self.content = _strip_thinking(raw_content) if isinstance(raw_content, str) else raw_content
self.tool_calls = msg.get('tool_calls', None)
self.warning = None
@ -25,6 +63,10 @@ class LLMClient:
payload["tools"] = tools
payload["tool_choice"] = "auto"
# Disable reasoning/thinking di level API bila didukung
# OpenRouter & beberapa provider support ini
payload["reasoning"] = {"enabled": False}
data = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(url, data=data, method='POST')
req.add_header('Content-Type', 'application/json')
@ -89,4 +131,12 @@ class LLMClient:
})
message = response['choices'][0]['message']
# Handle reasoning_content field dari OpenRouter/models yang support thinking
# Pindahkan ke content jangan sampai keluar
reasoning_content = message.pop('reasoning_content', None)
reasoning_field = message.pop('reasoning', None)
# Jangan inject reasoning ke content — buang saja
# (kita sudah strip via _strip_thinking di Message.__init__)
return self.Message(message)

View File

@ -15,6 +15,8 @@ import re
from dataclasses import dataclass, field
from pathlib import Path
import yaml
# ─── Paths ────────────────────────────────────────────────────────────────────
@ -26,7 +28,7 @@ SKILLS_DIR = BASE_DIR / "skills"
# ─── Mode / Skill ──────────────────────────────────────────────────────────────
MODE = os.getenv("PERSONA_MODE", default="programmer").strip().lower()
SKILL = os.getenv("AGENT_SKILL", default="programmer").strip().lower()
# ─── Personality Configuration ────────────────────────────────────────────────
@ -228,46 +230,82 @@ def _load_skills(skill_names: list[str]) -> str:
def build_system_prompt(
tools_definition: list[dict] | None = None,
mode: str | None = None,
skill: str | None = None,
personality: PersonalityConfig | None = None,
character: str | None = None,
skills: list[str] | None = None,
) -> str:
"""
Build system prompt berdasarkan mode, character, dan skills.
Build system prompt berdasarkan skill, character, dan skills.
Load order:
1. Base prompt
2. Personality block (dari config)
3. Policies (dari env-characters/<name>/policies.md)
2. Personality block (dari config / persona.yaml character)
3. Policies (dari agent/characters/<name>/policies.md)
4. Skill instructions (dari skills/<name>/instructions.md)
Args:
tools_definition: Daftar tools (required untuk skill programmer).
mode: "programmer" atau "roleplayer". Default: dari env PERSONA_MODE.
skill: "programmer" atau "roleplayer". Default: dari env AGENT_SKILL.
personality: PersonalityConfig instance. Default: global PERSONALITY.
character: Nama env character. Default: dari env AGENT_CHARACTER.
skills: List nama skill aktif. Default: derives from mode atau env AGENT_SKILLS.
skills: List nama skill aktif. Default: derives dari env AGENT_SKILLS.
Returns:
String system prompt lengkap.
"""
selected_mode = (mode or MODE).strip().lower()
selected_skill = (skill or SKILL).strip().lower()
cfg = personality or PERSONALITY
# Resolve character name
character_name = (character or os.getenv("AGENT_CHARACTER", default="")).strip().lower()
# ── Load persona.yaml dari character directory (override personality) ─────────
if character_name:
char_dir = ENV_CHARACTERS_DIR / character_name
persona_yaml_path = char_dir / "persona.yaml"
if persona_yaml_path.is_file():
try:
with open(persona_yaml_path, "r", encoding="utf-8") as f:
_persona_data = yaml.safe_load(f) or {}
if isinstance(_persona_data, dict):
# Override personality dari persona.yaml
if _persona_data.get("name"):
cfg.name = _persona_data["name"]
if _persona_data.get("age"):
cfg.age = str(_persona_data["age"])
if _persona_data.get("gender"):
cfg.gender = _persona_data["gender"]
if _persona_data.get("tone"):
cfg.tone = _persona_data["tone"]
if _persona_data.get("verbosity"):
cfg.verbosity = _persona_data["verbosity"]
if _persona_data.get("humor"):
cfg.humor_level = _persona_data["humor"]
if _persona_data.get("language"):
cfg.language = _persona_data["language"]
if _persona_data.get("mood"):
cfg.mood = _persona_data["mood"]
# key "skill" (baru) atau "mode" (legacy) menentukan active skill
_skill_from_yaml = _persona_data.get("skill") or _persona_data.get("mode")
if _skill_from_yaml:
selected_skill = _skill_from_yaml.strip().lower()
except Exception as e:
print(f"[persona] Warning: gagal load persona.yaml untuk '{character_name}': {e}")
# Resolve skills list
if skills is None:
# Priority: explicit skills param > persona.yaml skill > AGENT_SKILL env > AGENT_SKILLS env > selected_skill
if skills is not None:
skills_list = skills
elif selected_skill != SKILL:
# persona.yaml meng-override skill → pakai skill dari persona.yaml
skills_list = [selected_skill] if selected_skill in ("programmer", "roleplayer", "analyst") else []
else:
skills_env = os.getenv("AGENT_SKILLS", default="").strip()
if skills_env:
skills_list = [s.strip() for s in skills_env.split(",") if s.strip()]
else:
# Derive from mode untuk backward compatibility
skills_list = [selected_mode] if selected_mode in ("programmer", "roleplayer") else []
else:
skills_list = skills
skills_list = [selected_skill] if selected_skill in ("programmer", "roleplayer", "analyst") else []
# ── 1. Base prompt ──────────────────────────────────────────────────────────
base_prompt = ""

View File

@ -9,7 +9,6 @@ from slixmpp import ClientXMPP
from services.session_manager import SessionManager
import config
from scripts.persona import MODE as PERSONA_MODE
from tools.roleplayer import should_respond
from scripts.persona import PERSONALITY
@ -50,7 +49,7 @@ class XMPPClient(ClientXMPP):
self._TOOL_HANDLERS = TOOL_HANDLERS
self._build_system_prompt = build_system_prompt
self._max_iterations = agent_max_iterations
self._mode = PERSONA_MODE
self._skill = config.AGENT_SKILL
self._muc_rooms = muc_rooms or []
# Custom nick dari config, fallback ke username JID
self._muc_nick = config.XMPP_NICKNAME.strip() or jid.split('@')[0]
@ -299,7 +298,8 @@ class XMPPClient(ClientXMPP):
session.add_message('user', body)
if self._mode != 'roleplayer':
is_roleplay = self._skill == 'roleplayer'
if not is_roleplay:
self._schedule_send(jid, f'> {body}\nThinking...')
# Delay 1: simulasi membaca pesan user
@ -329,7 +329,7 @@ class XMPPClient(ClientXMPP):
prefixed = f'[{nick}] {body}'
session.add_message('user', prefixed)
if self._mode != 'roleplayer':
if self._skill != 'roleplayer':
self._schedule_send(room, f'> [{nick}] {body}\nThinking...', mtype='groupchat')
# Delay 1: simulasi membaca pesan user
@ -341,7 +341,7 @@ class XMPPClient(ClientXMPP):
session.start_timer(300, self._timeout_session, room, 'groupchat')
def _agent_loop(self, session, to, quote, mtype, sender_nickname=""):
is_roleplayer = self._mode == 'roleplayer'
is_roleplayer = self._skill == 'roleplayer'
my_name = PERSONALITY.name
for step in range(self._max_iterations):