Compare commits

..

4 Commits

6 changed files with 111 additions and 114 deletions

View File

@ -8,20 +8,6 @@ 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` berbeda dengan fungsi RAG general dari base system prompt; gunakan `ragroleplay` secara spesifik untuk kebutuhan storytelling, lore karakter, dan riwayat interaksi roleplay. - Tool `ragroleplay` digunakan 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.

175
config.py
View File

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

View File

@ -45,12 +45,15 @@ 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,13 +1,6 @@
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
@ -73,6 +66,9 @@ 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,13 +10,14 @@ 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 ────────────────────────────────────────────────────────────────────
@ -28,7 +29,7 @@ SKILLS_DIR = BASE_DIR / "skills"
# ─── Mode / Skill ────────────────────────────────────────────────────────────── # ─── Mode / Skill ──────────────────────────────────────────────────────────────
SKILL = os.getenv("AGENT_SKILL", default="programmer").strip().lower() SKILL = config.AGENT_SKILL
# ─── Personality Configuration ──────────────────────────────────────────────── # ─── Personality Configuration ────────────────────────────────────────────────
@ -49,19 +50,19 @@ class PersonalityConfig:
def _load_personality_from_env() -> PersonalityConfig: def _load_personality_from_env() -> PersonalityConfig:
"""Baca personality config dari environment variables.""" """Baca personality config dari config module globals."""
raw_catchphrases = os.getenv("PERSONALITY_CATCHPHRASES", default="").strip() raw_catchphrases = config.PERSONALITY_CATCHPHRASES.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=os.getenv("PERSONALITY_NAME", default="OWL").strip() or "OWL", name=config.PERSONALITY_NAME.strip() or "OWL",
age=os.getenv("PERSONALITY_AGE", default="").strip(), age=config.PERSONALITY_AGE.strip(),
gender=os.getenv("PERSONALITY_GENDER", default="").strip(), gender=config.PERSONALITY_GENDER.strip(),
tone=os.getenv("PERSONALITY_TONE", default="casual").strip().lower() or "casual", tone=config.PERSONALITY_TONE.strip().lower() or "casual",
verbosity=os.getenv("PERSONALITY_VERBOSITY", default="balanced").strip().lower() or "balanced", verbosity=config.PERSONALITY_VERBOSITY.strip().lower() or "balanced",
humor_level=os.getenv("PERSONALITY_HUMOR", default="light").strip().lower() or "light", humor_level=config.PERSONALITY_HUMOR.strip().lower() or "light",
language=os.getenv("PERSONALITY_LANGUAGE", default="id").strip().lower() or "id", language=config.PERSONALITY_LANGUAGE.strip().lower() or "id",
mood=os.getenv("PERSONALITY_MOOD", default="cheerful").strip().lower() or "cheerful", mood=config.PERSONALITY_MOOD.strip().lower() or "cheerful",
catchphrases=catchphrases, catchphrases=catchphrases,
) )
@ -273,7 +274,7 @@ def build_system_prompt(
cfg = personality or PERSONALITY cfg = personality or PERSONALITY
# Resolve character name # Resolve character name
character_name = (character or os.getenv("AGENT_CHARACTER", default="")).strip().lower() character_name = (character or "").strip().lower()
# ── Load personality.yaml dari character directory ───────────────────────────────── # ── Load personality.yaml dari character directory ─────────────────────────────────
if character_name: if character_name:
@ -317,7 +318,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 = os.getenv("AGENT_SKILLS", default="").strip() skills_env = config.AGENT_SKILLS.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: