Instruction per character
This commit is contained in:
parent
d3d16e85c7
commit
98080ca83c
@ -29,8 +29,10 @@ tools_definition = [
|
||||
# Image Generation Tools
|
||||
gadget.tools_mapping( schema = imagegen.schema_generate_image, handler = imagegen.generate_image ),
|
||||
# Instruction Tools
|
||||
gadget.tools_mapping( schema = smart.schema_instruction_use, handler = smart.instruction_use ), # snippet
|
||||
gadget.tools_mapping( schema = smart_rp.schema_rp_instruction_use, handler = smart_rp.rp_instruction_use ), # roleplay
|
||||
gadget.tools_mapping( schema = smart.schema_instruction_use, handler = smart.instruction_use ), # snippet (global)
|
||||
gadget.tools_mapping( schema = smart.schema_instruction_char_use, handler = smart.instruction_char_use ), # snippet (private per character)
|
||||
gadget.tools_mapping( schema = smart_rp.schema_rp_instruction_use, handler = smart_rp.rp_instruction_use ), # roleplay (global)
|
||||
gadget.tools_mapping( schema = smart_rp.schema_rp_instruction_char_use, handler = smart_rp.rp_instruction_char_use ), # roleplay (private per character)
|
||||
# Rag Roleplay Tools
|
||||
gadget.tools_mapping( schema = ragroleplay_tools.schema_users_store, handler = ragroleplay_tools.users_store ),
|
||||
gadget.tools_mapping( schema = ragroleplay_tools.schema_user_load, handler = ragroleplay_tools.user_load ), # s_load
|
||||
|
||||
128
tools/instrbase.py
Normal file
128
tools/instrbase.py
Normal file
@ -0,0 +1,128 @@
|
||||
"""
|
||||
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
|
||||
@ -1,7 +1,9 @@
|
||||
import os
|
||||
|
||||
# NOTE: loggorilla dimatikan di tool ini. print()-nya bisa bikin error saat
|
||||
# proses keluar (SCI), jadi semua pemanggilan di bawah hanya di-comment, jangan dihapus.
|
||||
# from scripts import loggorilla
|
||||
from tools import instrbase
|
||||
|
||||
schema_instruction_use = {
|
||||
"type": "function",
|
||||
@ -48,3 +50,43 @@ def instruction_use(directory, filename):
|
||||
res = f"Error reading file: {str(e)}"
|
||||
# loggorilla.fyinf("tools.smart", f"Exception occurred: {res}")
|
||||
return res
|
||||
|
||||
|
||||
# ─── instruction_char_use (private / per-character variant) ────────────────────
|
||||
|
||||
schema_instruction_char_use = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "instruction_char_use",
|
||||
"description": (
|
||||
"Reads a specific instruction file from the PRIVATE 'instruction' directory of the "
|
||||
"active character (agent/characters/<character>/instruction) and returns its content. "
|
||||
"This is the character-scoped variant of instruction_use: use it to load snippets, "
|
||||
"guidelines or system prompt extensions that belong only to the current character, "
|
||||
"instead of the shared/global 'instruction' directory."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"The subdirectory within the character's own 'instruction' folder "
|
||||
"(e.g., 'snippets', 'scenes', 'world'). Pass an empty string to read "
|
||||
"directly from the root of that folder."
|
||||
)
|
||||
},
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"description": "The name of the instruction file to read (e.g., 'opening.md', 'rules.md')."
|
||||
}
|
||||
},
|
||||
"required": ["directory", "filename"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def instruction_char_use(directory, filename):
|
||||
# loggorilla dimatikan di tool ini (lihat catatan di atas), logger dikirim None.
|
||||
return instrbase.read_instruction_file("tools.smart", None, directory, filename)
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import os
|
||||
|
||||
from scripts import loggorilla
|
||||
from tools import instrbase
|
||||
|
||||
schema_rp_instruction_use = {
|
||||
"type": "function",
|
||||
@ -46,3 +48,46 @@ def rp_instruction_use(directory, filename):
|
||||
res = f"Error reading RP file: {str(e)}"
|
||||
loggorilla.fyinf("tools.smart_rp", f"Exception occurred: {res}")
|
||||
return res
|
||||
|
||||
|
||||
# ─── rp_instruction_char_use (private / per-character variant) ─────────────────
|
||||
|
||||
schema_rp_instruction_char_use = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "rp_instruction_char_use",
|
||||
"description": (
|
||||
"CRITICAL ROLEPLAY VALIDATION TOOL (character-scoped): Use this tool ONLY when the "
|
||||
"character is interacting with a specific object, outfit, or location defined in this "
|
||||
"character's PRIVATE roleplay instruction files, located at "
|
||||
"agent/characters/<character>/instruction. This is the character-scoped variant of "
|
||||
"rp_instruction_use — it must be called to fetch the authoritative description before "
|
||||
"describing the item, to ensure narrative consistency and prevent hallucination. "
|
||||
"DO NOT use if the character is not interacting with a specific listed item."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"The subdirectory within the character's own 'instruction' folder "
|
||||
"(e.g., 'rp/item', 'rp/armor', 'rp/world', 'gameplay'). Pass an empty "
|
||||
"string to read directly from the root of that folder."
|
||||
)
|
||||
},
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"description": "The name of the instruction file to read (e.g., 'rules.md', 'iron-sword.md')."
|
||||
}
|
||||
},
|
||||
"required": ["directory", "filename"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def rp_instruction_char_use(directory, filename):
|
||||
return instrbase.read_instruction_file(
|
||||
"tools.smart_rp", loggorilla, directory, filename,
|
||||
label="roleplay instruction", caller="rp_instruction_char_use"
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user