129 lines
4.7 KiB
Python
129 lines
4.7 KiB
Python
"""
|
|
Shared helper untuk varian instruction tool "private per character".
|
|
|
|
Base directory yang dipakai:
|
|
agent/characters/<AGENT_CHARACTER>/instruction/
|
|
|
|
Dipakai oleh:
|
|
tools/smart.py -> instruction_char_use
|
|
tools/smart_rp.py -> rp_instruction_char_use
|
|
"""
|
|
|
|
import os
|
|
|
|
import config
|
|
|
|
|
|
def char_from_config():
|
|
"""Nama karakter aktif (lowercase) — dibaca saat dipanggil supaya
|
|
override `--char` / config.load_character() ikut terpakai."""
|
|
return (getattr(config, "AGENT_CHARACTER", "") or "").strip().lower()
|
|
|
|
|
|
def char_instruction_base(char=None):
|
|
"""Return (char, base_dir) untuk instruction privat karakter.
|
|
|
|
base_dir bernilai None bila tidak ada karakter aktif atau CHARACTERS_DIR
|
|
tidak tersedia di config."""
|
|
char = (char or char_from_config())
|
|
base_dir = None
|
|
if char:
|
|
chars_dir = getattr(config, "CHARACTERS_DIR", None)
|
|
if chars_dir is not None:
|
|
base_dir = str(chars_dir / char / "instruction")
|
|
return char, base_dir
|
|
|
|
|
|
def sanitize_path(directory, filename):
|
|
"""Sanitasi input supaya tetap di dalam base_dir (anti path traversal).
|
|
Segment kosong, '.' dan '..' dibuang, slash absolut dinetralkan."""
|
|
parts = [p for p in str(directory).replace('\\', '/').split('/')
|
|
if p and p not in ('.', '..')]
|
|
safe_directory = os.path.join(*parts) if parts else ""
|
|
safe_filename = os.path.basename(str(filename).replace('\\', '/'))
|
|
return safe_directory, safe_filename
|
|
|
|
|
|
def inside_base(base_dir, file_path):
|
|
"""Guard tambahan: file harus benar-benar berada di dalam base_dir
|
|
(menutup celah symlink / penulisan path aneh)."""
|
|
try:
|
|
real_base = os.path.realpath(base_dir)
|
|
real_file = os.path.realpath(file_path)
|
|
except Exception:
|
|
return False
|
|
return real_file == real_base or real_file.startswith(real_base + os.sep)
|
|
|
|
|
|
def list_available(base_dir, limit=50):
|
|
"""Daftar file (relatif terhadap base_dir) untuk pesan error yang membantu.
|
|
Return None bila directory privat karakter belum ada sama sekali."""
|
|
if not base_dir or not os.path.isdir(base_dir):
|
|
return None
|
|
found = []
|
|
for root, _dirs, files in os.walk(base_dir):
|
|
for fname in sorted(files):
|
|
rel = os.path.relpath(os.path.join(root, fname), base_dir)
|
|
found.append(rel.replace(os.sep, "/"))
|
|
return sorted(found)[:limit]
|
|
|
|
|
|
def missing_hint(char, base_dir):
|
|
"""Pesan penutup untuk error 'file not found'."""
|
|
available = list_available(base_dir)
|
|
if available is None:
|
|
return (f"The character '{char}' has no private instruction directory yet "
|
|
f"(expected at {base_dir}).")
|
|
if available:
|
|
return f"Available files in '{base_dir}': {', '.join(available)}"
|
|
return f"The private instruction directory '{base_dir}' exists but is empty."
|
|
|
|
|
|
def read_instruction_file(tool_name, logger, directory, filename, label="instruction",
|
|
caller="instruction_char_use"):
|
|
"""Resolver + reader bersama untuk varian char (private per character).
|
|
|
|
Args:
|
|
tool_name: nama tool untuk log (mis. 'tools.smart_rp').
|
|
logger : loggorilla module, atau None untuk tanpa logging.
|
|
directory: sub-directory di dalam instruction privat karakter ('' = root).
|
|
filename : nama file instruksi.
|
|
label : penanda jenis instruction di pesan error ('instruction' | 'roleplay instruction').
|
|
|
|
Returns:
|
|
Isi file (str) atau string pesan error.
|
|
"""
|
|
|
|
def _log(msg):
|
|
if logger is not None:
|
|
logger.fyinf(tool_name, msg)
|
|
|
|
_log(f"Calling {caller} with directory='{directory}', filename='{filename}'")
|
|
|
|
char, base_dir = char_instruction_base()
|
|
|
|
if not char or not base_dir:
|
|
res = (f"Error: No active character is configured, so there is no private {label} "
|
|
"directory. Set the character in config.yaml or pass --char <character>.")
|
|
_log(res)
|
|
return res
|
|
|
|
safe_directory, safe_filename = sanitize_path(directory, filename)
|
|
file_path = os.path.join(base_dir, safe_directory, safe_filename)
|
|
|
|
if (not os.path.isfile(file_path)) or (not inside_base(base_dir, file_path)):
|
|
res = (f"Error: Character {label} file not found at {file_path}. "
|
|
f"{missing_hint(char, base_dir)}")
|
|
_log(f"File not found: {res}")
|
|
return res
|
|
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
_log(f"Successfully read char {label} file for '{char}'. Content snippet: {content[:100]}...")
|
|
return content
|
|
except Exception as e:
|
|
res = f"Error reading character {label} file: {str(e)}"
|
|
_log(f"Exception occurred: {res}")
|
|
return res
|