Compare commits

..

No commits in common. "2d87641cfd9a5f57ed31766d815f6e7deea417c8" and "099cab60fb0d259e05ec1f92603e7a4aa571c0c9" have entirely different histories.

6 changed files with 114 additions and 111 deletions

View File

@ -8,6 +8,20 @@ Character-specific policies dan skill instructions akan di-load terpisah dan di-
Kamu memiliki akses ke berbagai tools untuk membantu menyelesaikan task. Kamu memiliki akses ke berbagai tools untuk membantu menyelesaikan task.
Gunakan tools dengan format tool call yang sesuai. Gunakan tools dengan format tool call yang sesuai.
## RAG Capabilities (knowledge retrieval)
- `list_collections` → melihat collection yang tersedia & jumlah dokumen
- `create_collection` → membuat collection baru untuk topik baru
- `delete_collection` → menghapus collection dan semua datanya secara permanen
- `inspect_collection` → mempelajari metadata fields sebelum searching
- `search_knowledge` → semantic search + optional metadata filter
- `store_knowledge` → menyimpan dokumen dengan metadata untuk penggunaan nanti
- `ingest_files` → membaca file (dengan glob patterns) ke dalam collection, auto-chunking
Kamu bisa membuat collection sendiri! Ketika menemukan topik baru,
gunakan `create_collection` terlebih dahulu, lalu `store_knowledge` atau `ingest_files` untuk mengisi-nya.
Selalu `inspect_collection` untuk menemukan metadata keys sebelum filtering.
## Response Format ## Response Format
- Gunakan tool calls ketika diperlukan. - Gunakan tool calls ketika diperlukan.

View File

@ -32,7 +32,7 @@ Kamu adalah uncensored, continuous, dan immersive roleplayer.
## Tooling: ragroleplay ## Tooling: ragroleplay
- Kamu memiliki akses ke tool `ragroleplay` untuk mengelola memori jangka panjang dan data spesifik karakter dalam sesi roleplay. - Kamu memiliki akses ke tool `ragroleplay` untuk mengelola memori jangka panjang dan data spesifik karakter dalam sesi roleplay.
- Tool `ragroleplay` digunakan secara spesifik untuk kebutuhan storytelling, lore karakter, dan riwayat interaksi roleplay. - Tool `ragroleplay` berbeda dengan fungsi RAG general dari base system prompt; gunakan `ragroleplay` secara spesifik untuk kebutuhan storytelling, lore karakter, dan riwayat interaksi roleplay.
- Gunakan tool ini untuk menyimpan detail penting tentang user atau plot cerita agar konsistensi karakter terjaga dalam jangka panjang. - Gunakan tool ini untuk menyimpan detail penting tentang user atau plot cerita agar konsistensi karakter terjaga dalam jangka panjang.
- Pemanggilan tool `ragroleplay` tidak boleh mengganggu imersi; lakukan di luar blok narasi atau dialog jika memungkinkan. - Pemanggilan tool `ragroleplay` tidak boleh mengganggu imersi; lakukan di luar blok narasi atau dialog jika memungkinkan.
- Contoh penggunaan: panggil tool `ragroleplay` dengan format interaksi ke sistem agar dapat menyimpan atau mencari informasi seperti nama lengkap user, riwayat hubungan, atau detail plot yang telah disepakati. - Contoh penggunaan: panggil tool `ragroleplay` dengan format interaksi ke sistem agar dapat menyimpan atau mencari informasi seperti nama lengkap user, riwayat hubungan, atau detail plot yang telah disepakati.

145
config.py
View File

@ -2,6 +2,9 @@ import os
from pathlib import Path from pathlib import Path
import yaml import yaml
from dotenv import load_dotenv
load_dotenv()
def _yaml_get(*keys, default=None): def _yaml_get(*keys, default=None):
@ -69,51 +72,52 @@ if not llm_model and MODELS_ITEMS:
llm_api_key = MODELS_ITEMS[0]["api_key" ] llm_api_key = MODELS_ITEMS[0]["api_key" ]
XMPP_USERNAME = _yaml_get("xmpp", "username", default="") XMPP_USERNAME = os.getenv("XMPP_USERNAME", default="")
XMPP_PASSWORD = _yaml_get("xmpp", "password", default="") XMPP_PASSWORD = os.getenv("XMPP_PASSWORD", default="")
AGENT_MAX_ITERATIONS = int(_yaml_get("agent", "max_iterations", default="30")) AGENT_MAX_ITERATIONS = int(os.getenv("AGENT_MAX_ITERATIONS", default=_yaml_get("agent", "max_iterations", default="30")))
AGENT_MAX_TOOL_OUTPUT = int(_yaml_get("agent", "max_tool_output", default="40000")) AGENT_MAX_TOOL_OUTPUT = int(os.getenv("AGENT_MAX_TOOL_OUTPUT", default=_yaml_get("agent", "max_tool_output", default="40000")))
AGENT_SKILL = "programmer" AGENT_SKILL = os.getenv("AGENT_SKILL", default="programmer").strip().lower()
PERSONALITY_NAME = "Hendrik" PERSONALITY_NAME = os.getenv("PERSONALITY_NAME", default=_yaml_get("personality", "name", default="Hendrik")).strip() or "Hendrik"
PERSONALITY_AGE = "" PERSONALITY_AGE = os.getenv("PERSONALITY_AGE", default=_yaml_get("personality", "age", default="")).strip()
PERSONALITY_GENDER = "" PERSONALITY_GENDER = os.getenv("PERSONALITY_GENDER", default=_yaml_get("personality", "gender", default="")).strip()
PERSONALITY_TONE = "casual" PERSONALITY_TONE = os.getenv("PERSONALITY_TONE", default=_yaml_get("personality", "tone", default="casual")).strip().lower() or "casual"
PERSONALITY_VERBOSITY = "balanced" PERSONALITY_VERBOSITY = os.getenv("PERSONALITY_VERBOSITY", default=_yaml_get("personality", "verbosity", default="balanced")).strip().lower() or "balanced"
PERSONALITY_HUMOR = "light" PERSONALITY_HUMOR = os.getenv("PERSONALITY_HUMOR", default=_yaml_get("personality", "humor", default="light")).strip().lower() or "light"
PERSONALITY_LANGUAGE = "id" PERSONALITY_LANGUAGE = os.getenv("PERSONALITY_LANGUAGE", default=_yaml_get("personality", "language", default="id")).strip().lower() or "id"
PERSONALITY_MOOD = "cheerful" PERSONALITY_MOOD = os.getenv("PERSONALITY_MOOD", default=_yaml_get("personality", "mood", default="cheerful")).strip().lower() or "cheerful"
PERSONALITY_CATCHPHRASES = "" PERSONALITY_CATCHPHRASES = os.getenv("PERSONALITY_CATCHPHRASES", default=_yaml_get("personality", "catchphrases", default="")).strip()
# ─── Character & Skills ──────────────────────────────────────────────────────── # ─── Character & Skills (YAML, bisa di-override dari .env) ─────────────────────
AGENT_CHARACTER = _yaml_get("agent", "character", default="").strip().lower() AGENT_CHARACTER = os.getenv("AGENT_CHARACTER", default=_yaml_get("agent", "character", default="")).strip().lower()
AGENT_SKILLS = _yaml_get("agent", "skills", default="").strip().lower() AGENT_SKILLS = os.getenv("AGENT_SKILLS", default="").strip().lower()
# ─── XMPP ────────────────────────────────────────────────────────────────────── # ─── XMPP (non-credential dari YAML, credential dari .env) ─────────────────────
XMPP_ENABLED = str(_yaml_get("xmpp", "enabled", default="false")).strip().lower() in ("true", "1", "yes") XMPP_ENABLED = os.getenv("XMPP_ENABLED", default=str(_yaml_get("xmpp", "enabled", default="false"))).strip().lower() in ("true", "1", "yes")
XMPP_MUC_ROOMS = _yaml_get("xmpp", "muc_rooms", default="").strip() XMPP_MUC_ROOMS = os.getenv("XMPP_MUC_ROOMS", default=_yaml_get("xmpp", "muc_rooms", default="")).strip()
XMPP_NICKNAME = _yaml_get("xmpp", "nickname", default="").strip() XMPP_NICKNAME = os.getenv("XMPP_NICKNAME", default=_yaml_get("xmpp", "nickname", default="")).strip()
XMPP_SELECTIVE_RESPONSE = str(_yaml_get("xmpp", "selective_response", default="true")).strip().lower() in ("true", "1", "yes")
XMPP_SELECTIVE_RESPONSE = os.getenv("XMPP_SELECTIVE_RESPONSE", default=str(_yaml_get("xmpp", "selective_response", default="true"))).strip().lower() in ("true", "1", "yes")
# ─── Telegram ────────────────────────────────────────────────────────────────── # ─── Telegram (non-credential dari YAML, credential dari .env) ─────────────────
TELEGRAM_ENABLED = str(_yaml_get("telegram", "enabled", default="false")).strip().lower() in ("true", "1", "yes") TELEGRAM_ENABLED = os.getenv("TELEGRAM_ENABLED", default=str(_yaml_get("telegram", "enabled", default="false"))).strip().lower() in ("true", "1", "yes")
TELEGRAM_TOKEN = _yaml_get("telegram", "token", default="") TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", default="")
TELEGRAM_ALLOWED_GROUP_IDS = _yaml_get("telegram", "allowed_group_ids", default="").strip() TELEGRAM_ALLOWED_GROUP_IDS = os.getenv("TELEGRAM_ALLOWED_GROUP_IDS", default=_yaml_get("telegram", "allowed_group_ids", default="")).strip()
TELEGRAM_SELECTIVE_RESPONSE = str(_yaml_get("telegram", "selective_response", default="true")).strip().lower() in ("true", "1", "yes") TELEGRAM_SELECTIVE_RESPONSE = os.getenv("TELEGRAM_SELECTIVE_RESPONSE", default=str(_yaml_get("telegram", "selective_response", default="true"))).strip().lower() in ("true", "1", "yes")
# ─── Session (TinyDB) ──────────────────────────────────────────────────────────── # ─── Session (TinyDB) ────────────────────────────────────────────────────────────
SESSION_DB_PATH = os.path.expanduser( SESSION_DB_PATH = os.path.expanduser(
_yaml_get("session", "db_path", default="~/.config/hendrik/sessions.json") os.getenv("SESSION_DB_PATH", default=_yaml_get("session", "db_path", default="~/.config/hendrik/sessions.json"))
) )
# Humanize Delay (YAML) # Humanize Delay (YAML)
@ -123,65 +127,41 @@ TYPING_SPEED = float( _yaml_get("delay", "typing_speed", default="15.0" ) )
TYPING_MAX = float( _yaml_get("delay", "typing_max", default="10.0" ) ) TYPING_MAX = float( _yaml_get("delay", "typing_max", default="10.0" ) )
# ─── Character Loader ────────────────────────────────────────────────────────── # ─── Character Preset Override ──────────────────────────────────────────────────
# Jika AGENT_CHARACTER di-set, baca character.md dari agent/characters/<preset>/
# dan override nilai personality yang relevan.
CHARACTERS_DIR = Path(__file__).resolve().parent / "agent" / "characters" ENV_CHARACTERS_DIR = Path(__file__).resolve().parent / "agent" / "characters"
ENV_CHARACTER_CONFIG_PATH = ENV_CHARACTERS_DIR / AGENT_CHARACTER / "character.md" if AGENT_CHARACTER else None
_yaml_to_config_key = { # Coba personality.yaml dulu (prioritas utama), kalau tidak ada fallback ke character.md
"AGENT_SKILL": "skill", _personality_yaml_path = ENV_CHARACTERS_DIR / AGENT_CHARACTER / "personality.yaml" if AGENT_CHARACTER else None
"PERSONALITY_NAME": "name",
"PERSONALITY_AGE": "age",
"PERSONALITY_GENDER": "gender",
"PERSONALITY_TONE": "tone",
"PERSONALITY_VERBOSITY": "verbosity",
"PERSONALITY_HUMOR": "humor",
"PERSONALITY_LANGUAGE": "language",
"PERSONALITY_MOOD": "mood",
"PERSONALITY_CATCHPHRASES": "catchphrases",
}
_lowercase_keys = {"AGENT_SKILL", "PERSONALITY_TONE", "PERSONALITY_VERBOSITY", "PERSONALITY_HUMOR", "PERSONALITY_LANGUAGE", "PERSONALITY_MOOD"} if _personality_yaml_path and _personality_yaml_path.is_file():
# Prioritas utama: baca personality.yaml dari character directory
def load_character(char_override=None):
"""Load personality dari character directory. Bisa di-call ulang untuk override."""
global AGENT_CHARACTER, AGENT_SKILL, PERSONALITY_NAME, PERSONALITY_AGE, PERSONALITY_GENDER
global PERSONALITY_TONE, PERSONALITY_VERBOSITY, PERSONALITY_HUMOR, PERSONALITY_LANGUAGE
global PERSONALITY_MOOD, PERSONALITY_CATCHPHRASES
if char_override:
AGENT_CHARACTER = char_override.strip().lower()
if not AGENT_CHARACTER:
return
personality_yaml_path = CHARACTERS_DIR / AGENT_CHARACTER / "personality.yaml"
character_config_path = CHARACTERS_DIR / AGENT_CHARACTER / "character.md"
if personality_yaml_path.is_file():
try: try:
_character_env = yaml.safe_load(personality_yaml_path.read_text(encoding="utf-8")) or {} _character_env = yaml.safe_load(_personality_yaml_path.read_text(encoding="utf-8")) or {}
if not isinstance(_character_env, dict): if not isinstance(_character_env, dict):
_character_env = {} _character_env = {}
except Exception as _e: except Exception as _e:
print(f"[config] Warning: gagal load personality.yaml untuk '{AGENT_CHARACTER}': {_e}", flush=True) print(f"[config] Warning: gagal load personality.yaml untuk '{AGENT_CHARACTER}': {_e}", flush=True)
_character_env = {} _character_env = {}
elif character_config_path.is_file(): elif ENV_CHARACTER_CONFIG_PATH and ENV_CHARACTER_CONFIG_PATH.is_file():
# Fallback: baca character.md (format lama)
_character_env = {} _character_env = {}
for line in character_config_path.read_text(encoding="utf-8").splitlines(): for line in ENV_CHARACTER_CONFIG_PATH.read_text(encoding="utf-8").splitlines():
line = line.strip() line = line.strip()
if not line or line.startswith("#"): if not line or line.startswith("#"):
continue continue
if "=" in line: if "=" in line:
key, value = line.split("=", 1) key, value = line.split("=", 1)
_character_env[key.strip()] = value.strip() _character_env[key.strip()] = value.strip()
else: else:
return _character_env = None
if not _character_env: if _character_env:
return
_defaults = { _character_overrides = {
"AGENT_SKILL": AGENT_SKILL, "AGENT_SKILL": AGENT_SKILL,
"PERSONALITY_NAME": PERSONALITY_NAME, "PERSONALITY_NAME": PERSONALITY_NAME,
"PERSONALITY_AGE": PERSONALITY_AGE, "PERSONALITY_AGE": PERSONALITY_AGE,
@ -193,20 +173,29 @@ def load_character(char_override=None):
"PERSONALITY_MOOD": PERSONALITY_MOOD, "PERSONALITY_MOOD": PERSONALITY_MOOD,
"PERSONALITY_CATCHPHRASES": PERSONALITY_CATCHPHRASES, "PERSONALITY_CATCHPHRASES": PERSONALITY_CATCHPHRASES,
} }
# Mapping dari key personality.yaml ke key config
for key, fallback in _defaults.items(): _yaml_to_config_key = {
"AGENT_SKILL": "skill",
"PERSONALITY_NAME": "name",
"PERSONALITY_AGE": "age",
"PERSONALITY_GENDER": "gender",
"PERSONALITY_TONE": "tone",
"PERSONALITY_VERBOSITY": "verbosity",
"PERSONALITY_HUMOR": "humor",
"PERSONALITY_LANGUAGE": "language",
"PERSONALITY_MOOD": "mood",
"PERSONALITY_CATCHPHRASES": "catchphrases",
}
for key, fallback in _character_overrides.items():
yaml_key = _yaml_to_config_key.get(key, key.lower()) yaml_key = _yaml_to_config_key.get(key, key.lower())
raw = _character_env.get(yaml_key, _character_env.get(key, fallback)) raw = _character_env.get(yaml_key, _character_env.get(key, fallback))
value = str(raw).strip() if raw is not None else "" value = str(raw).strip() if raw is not None else ""
if key in _lowercase_keys: if key in {"AGENT_SKILL", "PERSONALITY_TONE", "PERSONALITY_VERBOSITY", "PERSONALITY_HUMOR", "PERSONALITY_LANGUAGE", "PERSONALITY_MOOD"}:
value = value.lower() or fallback value = value.lower() or fallback
if key == "PERSONALITY_NAME" and not value: if key == "PERSONALITY_NAME" and not value:
value = fallback value = fallback
_defaults[key] = value _character_overrides[key] = value
for key, value in _defaults.items(): for key, value in _character_overrides.items():
globals()[key] = value globals()[key] = value
os.environ[key] = value
# Load character dari config.yaml saat import time
load_character()

View File

@ -45,15 +45,12 @@ session:
xmpp: xmpp:
enabled: false enabled: false
username: ""
password: ""
muc_rooms: "" # comma-separated, e.g. "room1@conference.server,room2@conference.server" muc_rooms: "" # comma-separated, e.g. "room1@conference.server,room2@conference.server"
nickname: "" # custom MUC nickname (empty = use username) nickname: "" # custom MUC nickname (empty = use username)
selective_response: true # true = only response if mentioned/relevant selective_response: true # true = only response if mentioned/relevant
telegram: telegram:
enabled: false enabled: false
token: ""
allowed_group_ids: "" # comma-separated, empty = all group allowed_group_ids: "" # comma-separated, empty = all group
selective_response: true # true = only response if mentioned/relevant selective_response: true # true = only response if mentioned/relevant

View File

@ -1,6 +1,13 @@
import os, sys import os, sys
import argparse import argparse
# Pre-parse --char BEFORE config import (config reads os.environ at import time)
_pre = argparse.ArgumentParser(add_help=False)
_pre.add_argument('--char', type=str, default=None)
_pre_args, _ = _pre.parse_known_args()
if _pre_args.char:
os.environ['AGENT_CHARACTER'] = _pre_args.char
import threading, time, signal import threading, time, signal
import config import config
@ -66,9 +73,6 @@ def main():
parser.add_argument('-w', '--workspace', type=str, default=None, help='Working directory') parser.add_argument('-w', '--workspace', type=str, default=None, help='Working directory')
args = parser.parse_args() args = parser.parse_args()
if args.char:
config.load_character(char_override=args.char)
if args.workspace: if args.workspace:
resolved = os.path.abspath(args.workspace) resolved = os.path.abspath(args.workspace)
if not os.path.isdir(resolved): if not os.path.isdir(resolved):

View File

@ -10,14 +10,13 @@ Arsitektur:
Load order: Base Character Policies Skills Load order: Base Character Policies Skills
""" """
import os
import re import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
import yaml import yaml
import config
# ─── Paths ──────────────────────────────────────────────────────────────────── # ─── Paths ────────────────────────────────────────────────────────────────────
@ -29,7 +28,7 @@ SKILLS_DIR = BASE_DIR / "skills"
# ─── Mode / Skill ────────────────────────────────────────────────────────────── # ─── Mode / Skill ──────────────────────────────────────────────────────────────
SKILL = config.AGENT_SKILL SKILL = os.getenv("AGENT_SKILL", default="programmer").strip().lower()
# ─── Personality Configuration ──────────────────────────────────────────────── # ─── Personality Configuration ────────────────────────────────────────────────
@ -50,19 +49,19 @@ class PersonalityConfig:
def _load_personality_from_env() -> PersonalityConfig: def _load_personality_from_env() -> PersonalityConfig:
"""Baca personality config dari config module globals.""" """Baca personality config dari environment variables."""
raw_catchphrases = config.PERSONALITY_CATCHPHRASES.strip() raw_catchphrases = os.getenv("PERSONALITY_CATCHPHRASES", default="").strip()
catchphrases = [c.strip() for c in raw_catchphrases.split(",") if c.strip()] if raw_catchphrases else [] catchphrases = [c.strip() for c in raw_catchphrases.split(",") if c.strip()] if raw_catchphrases else []
return PersonalityConfig( return PersonalityConfig(
name=config.PERSONALITY_NAME.strip() or "OWL", name=os.getenv("PERSONALITY_NAME", default="OWL").strip() or "OWL",
age=config.PERSONALITY_AGE.strip(), age=os.getenv("PERSONALITY_AGE", default="").strip(),
gender=config.PERSONALITY_GENDER.strip(), gender=os.getenv("PERSONALITY_GENDER", default="").strip(),
tone=config.PERSONALITY_TONE.strip().lower() or "casual", tone=os.getenv("PERSONALITY_TONE", default="casual").strip().lower() or "casual",
verbosity=config.PERSONALITY_VERBOSITY.strip().lower() or "balanced", verbosity=os.getenv("PERSONALITY_VERBOSITY", default="balanced").strip().lower() or "balanced",
humor_level=config.PERSONALITY_HUMOR.strip().lower() or "light", humor_level=os.getenv("PERSONALITY_HUMOR", default="light").strip().lower() or "light",
language=config.PERSONALITY_LANGUAGE.strip().lower() or "id", language=os.getenv("PERSONALITY_LANGUAGE", default="id").strip().lower() or "id",
mood=config.PERSONALITY_MOOD.strip().lower() or "cheerful", mood=os.getenv("PERSONALITY_MOOD", default="cheerful").strip().lower() or "cheerful",
catchphrases=catchphrases, catchphrases=catchphrases,
) )
@ -274,7 +273,7 @@ def build_system_prompt(
cfg = personality or PERSONALITY cfg = personality or PERSONALITY
# Resolve character name # Resolve character name
character_name = (character or "").strip().lower() character_name = (character or os.getenv("AGENT_CHARACTER", default="")).strip().lower()
# ── Load personality.yaml dari character directory ───────────────────────────────── # ── Load personality.yaml dari character directory ─────────────────────────────────
if character_name: if character_name:
@ -318,7 +317,7 @@ def build_system_prompt(
# personality.yaml meng-override skill → pakai skill dari personality.yaml # personality.yaml meng-override skill → pakai skill dari personality.yaml
skills_list = [selected_skill] if selected_skill in ("programmer", "roleplayer", "analyst", "strategist") else [] skills_list = [selected_skill] if selected_skill in ("programmer", "roleplayer", "analyst", "strategist") else []
else: else:
skills_env = config.AGENT_SKILLS.strip() skills_env = os.getenv("AGENT_SKILLS", default="").strip()
if skills_env: if skills_env:
skills_list = [s.strip() for s in skills_env.split(",") if s.strip()] skills_list = [s.strip() for s in skills_env.split(",") if s.strip()]
else: else: