""" Shared helper untuk varian instruction tool "private per character". Base directory yang dipakai: agent/characters//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 .") _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