Instruction per character
This commit is contained in:
parent
d3d16e85c7
commit
98080ca83c
@ -29,8 +29,10 @@ tools_definition = [
|
|||||||
# Image Generation Tools
|
# Image Generation Tools
|
||||||
gadget.tools_mapping( schema = imagegen.schema_generate_image, handler = imagegen.generate_image ),
|
gadget.tools_mapping( schema = imagegen.schema_generate_image, handler = imagegen.generate_image ),
|
||||||
# Instruction Tools
|
# Instruction Tools
|
||||||
gadget.tools_mapping( schema = smart.schema_instruction_use, handler = smart.instruction_use ), # snippet
|
gadget.tools_mapping( schema = smart.schema_instruction_use, handler = smart.instruction_use ), # snippet (global)
|
||||||
gadget.tools_mapping( schema = smart_rp.schema_rp_instruction_use, handler = smart_rp.rp_instruction_use ), # roleplay
|
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
|
# Rag Roleplay Tools
|
||||||
gadget.tools_mapping( schema = ragroleplay_tools.schema_users_store, handler = ragroleplay_tools.users_store ),
|
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
|
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
|
import os
|
||||||
|
|
||||||
# NOTE: loggorilla dimatikan di tool ini. print()-nya bisa bikin error saat
|
# 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.
|
# proses keluar (SCI), jadi semua pemanggilan di bawah hanya di-comment, jangan dihapus.
|
||||||
# from scripts import loggorilla
|
# from scripts import loggorilla
|
||||||
|
from tools import instrbase
|
||||||
|
|
||||||
schema_instruction_use = {
|
schema_instruction_use = {
|
||||||
"type": "function",
|
"type": "function",
|
||||||
@ -48,3 +50,43 @@ def instruction_use(directory, filename):
|
|||||||
res = f"Error reading file: {str(e)}"
|
res = f"Error reading file: {str(e)}"
|
||||||
# loggorilla.fyinf("tools.smart", f"Exception occurred: {res}")
|
# loggorilla.fyinf("tools.smart", f"Exception occurred: {res}")
|
||||||
return 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
|
import os
|
||||||
|
|
||||||
from scripts import loggorilla
|
from scripts import loggorilla
|
||||||
|
from tools import instrbase
|
||||||
|
|
||||||
schema_rp_instruction_use = {
|
schema_rp_instruction_use = {
|
||||||
"type": "function",
|
"type": "function",
|
||||||
@ -46,3 +48,46 @@ def rp_instruction_use(directory, filename):
|
|||||||
res = f"Error reading RP file: {str(e)}"
|
res = f"Error reading RP file: {str(e)}"
|
||||||
loggorilla.fyinf("tools.smart_rp", f"Exception occurred: {res}")
|
loggorilla.fyinf("tools.smart_rp", f"Exception occurred: {res}")
|
||||||
return 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