Huge refactor
This commit is contained in:
parent
bf59ed5766
commit
540e166e89
@ -1,4 +1,4 @@
|
||||
mode: programmer
|
||||
skill: programmer
|
||||
name: Hendrik
|
||||
age: 35
|
||||
gender: male
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
31
config.yaml
31
config.yaml
@ -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)
|
||||
|
||||
@ -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 = [
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 = ""
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user