Compare commits
5 Commits
4995412bc1
...
099cab60fb
| Author | SHA1 | Date | |
|---|---|---|---|
| 099cab60fb | |||
| 2c65c51ec9 | |||
| 11d05a937e | |||
| 414f94811a | |||
| f72533dcc3 |
@ -67,9 +67,9 @@ Jika user memperkenalkan diri dalam format `!start ... !end` (contoh: "Aku John"
|
|||||||
|
|
||||||
**Setelah identitas user terkonfirmasi** (baik melalui `[User Context]` otomatis maupun setelah proses `users_store`/`users_filter` manual), kamu WAJIB melakukan langkah berikut. Ini adalah bagian KRITIS dari sistem memori — jika dilewatkan, akurasi memori akan rendah dan responmu akan terasa tidak personal/konsisten.
|
**Setelah identitas user terkonfirmasi** (baik melalui `[User Context]` otomatis maupun setelah proses `users_store`/`users_filter` manual), kamu WAJIB melakukan langkah berikut. Ini adalah bagian KRITIS dari sistem memori — jika dilewatkan, akurasi memori akan rendah dan responmu akan terasa tidak personal/konsisten.
|
||||||
|
|
||||||
1. **WAJIB Retrieve Recent History**: Panggil `memories_latest(character="<nama-karaktermu>", user="<nickname-user>")` dengan limit 3 untuk mengambil fragmen interaksi terbaru antara character dan user.
|
1. **WAJIB Retrieve Recent History**: Panggil `memories_latest(character="<nama-karaktermu>", user_id="<user-uuid>")` dengan limit 3 untuk mengambil fragmen interaksi terbaru antara character dan user.
|
||||||
- **Parameter Call**: Gunakan nama karaktermu sendiri (sesuai persona/identitasmu) sebagai `character` dan gunakan **Nickname** user yang tertera pada `[User Context]` sebagai `user`.
|
- **Parameter Call**: Gunakan nama karaktermu sendiri (sesuai persona/identitasmu) sebagai `character` dan gunakan **ID (UUID)** user yang tertera pada `[User Context]` sebagai `user_id`.
|
||||||
- **Contoh**: Jika namamu adalah "Lily" dan nickname user adalah "Budi", panggil: `memories_latest(character="Lily", user="Budi", limit=3)`
|
- **Contoh**: Jika namamu adalah "Lily" dan ID user adalah "550e8400-e29b-41d4-a716-446655440000", panggil: `memories_latest(character="Lily", user_id="550e8400-e29b-41d4-a716-446655440000", limit=3)`
|
||||||
2. **Execution Rule**:
|
2. **Execution Rule**:
|
||||||
- WAJIB lakukan di awal sesi setiap kali user mengirim pesan baru (setelah identitas terkonfirmasi).
|
- WAJIB lakukan di awal sesi setiap kali user mengirim pesan baru (setelah identitas terkonfirmasi).
|
||||||
- Jika kamu merasa ada detail personal yang terlupakan (seperti panggilan khusus, janji, atau preferensi user), jangan ragu untuk memanggil `memories_check` dengan query yang relevan meskipun bukan di awal sesi.
|
- Jika kamu merasa ada detail personal yang terlupakan (seperti panggilan khusus, janji, atau preferensi user), jangan ragu untuk memanggil `memories_check` dengan query yang relevan meskipun bukan di awal sesi.
|
||||||
|
|||||||
10
config.py
10
config.py
@ -120,16 +120,6 @@ SESSION_DB_PATH = os.path.expanduser(
|
|||||||
os.getenv("SESSION_DB_PATH", default=_yaml_get("session", "db_path", default="~/.config/hendrik/sessions.json"))
|
os.getenv("SESSION_DB_PATH", default=_yaml_get("session", "db_path", default="~/.config/hendrik/sessions.json"))
|
||||||
)
|
)
|
||||||
|
|
||||||
# ─── RAG (YAML) ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
RAG_PERSIST_DIR = os.path.expanduser(
|
|
||||||
os.getenv("RAG_PERSIST_DIR", default=_yaml_get("rag", "persist_dir", default="lancedb_data"))
|
|
||||||
)
|
|
||||||
RAG_MODEL_PATH = os.path.expanduser(
|
|
||||||
os.getenv("RAG_MODEL_PATH", default=_yaml_get("rag", "model_path", default=""))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Humanize Delay (YAML)
|
# Humanize Delay (YAML)
|
||||||
READ_DELAY_MIN = float( _yaml_get("delay", "read_min", default="1.0" ) )
|
READ_DELAY_MIN = float( _yaml_get("delay", "read_min", default="1.0" ) )
|
||||||
READ_DELAY_MAX = float( _yaml_get("delay", "read_max", default="2.0" ) )
|
READ_DELAY_MAX = float( _yaml_get("delay", "read_max", default="2.0" ) )
|
||||||
|
|||||||
@ -34,10 +34,6 @@ llm:
|
|||||||
- name : "nex-agi/nex-n2-pro:free"
|
- name : "nex-agi/nex-n2-pro:free"
|
||||||
- name : "z-ai/glm-5"
|
- name : "z-ai/glm-5"
|
||||||
|
|
||||||
rag:
|
|
||||||
persist_dir: "~/.config/hendrik/rag" # LanceDB Vector Store (all-MiniLM-L6-v2, local)
|
|
||||||
model_path: "~/.config/hendrik/models" # Custom path to store/load embedding model.
|
|
||||||
|
|
||||||
ragroleplay:
|
ragroleplay:
|
||||||
db_path : "./.ragroleplay"
|
db_path : "./.ragroleplay"
|
||||||
vector_size : 768 # used on table create only
|
vector_size : 768 # used on table create only
|
||||||
|
|||||||
41
hendrik.py
41
hendrik.py
@ -1,8 +1,18 @@
|
|||||||
import os, sys, threading, time, signal
|
import os, sys
|
||||||
|
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 config
|
import config
|
||||||
|
|
||||||
from tools import coder, rag, carrack, ragroleplay
|
from tools import coder, carrack, ragroleplay
|
||||||
|
|
||||||
from services.xmpp_client import XMPPClient
|
from services.xmpp_client import XMPPClient
|
||||||
from services.telegram_client import TelegramClient
|
from services.telegram_client import TelegramClient
|
||||||
@ -21,14 +31,6 @@ tools_definition = [
|
|||||||
gadget.tools_mapping( schema = coder.schema_search_code, handler = coder.search_code ),
|
gadget.tools_mapping( schema = coder.schema_search_code, handler = coder.search_code ),
|
||||||
gadget.tools_mapping( schema = coder.schema_git_operation, handler = coder.git_operation ),
|
gadget.tools_mapping( schema = coder.schema_git_operation, handler = coder.git_operation ),
|
||||||
|
|
||||||
gadget.tools_mapping( schema = rag.schema_ingest_files, handler = rag.ingest_files ),
|
|
||||||
gadget.tools_mapping( schema = rag.schema_store_knowledge, handler = rag.store_knowledge ),
|
|
||||||
gadget.tools_mapping( schema = rag.schema_search_knowledge, handler = rag.search_knowledge ),
|
|
||||||
gadget.tools_mapping( schema = rag.schema_create_collection, handler = rag.create_collection ),
|
|
||||||
gadget.tools_mapping( schema = rag.schema_delete_collection, handler = rag.delete_collection ),
|
|
||||||
gadget.tools_mapping( schema = rag.schema_list_collections, handler = rag.list_collections ),
|
|
||||||
gadget.tools_mapping( schema = rag.schema_inspect_collection, handler = rag.inspect_collection ),
|
|
||||||
|
|
||||||
gadget.tools_mapping( schema = carrack.schema_sendhttprequest, handler = carrack.sendhttprequest ),
|
gadget.tools_mapping( schema = carrack.schema_sendhttprequest, handler = carrack.sendhttprequest ),
|
||||||
|
|
||||||
# Rag Roleplay Tools
|
# Rag Roleplay Tools
|
||||||
@ -66,23 +68,20 @@ TOOLS = gadget.tool_schemas (tools_definition)
|
|||||||
TOOL_HANDLERS = gadget.tool_handlers (tools_definition)
|
TOOL_HANDLERS = gadget.tool_handlers (tools_definition)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
llm_client = LLMClient(config.llm_baseurl, config.llm_model, config.llm_api_key, config.llm_timeout)
|
parser = argparse.ArgumentParser(description="Hendrik AI Agent")
|
||||||
|
parser.add_argument('--char', type=str, default=None, help='Character name (overrides config.yaml)')
|
||||||
|
parser.add_argument('-w', '--workspace', type=str, default=None, help='Working directory')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
workspace = None
|
if args.workspace:
|
||||||
i = 1
|
resolved = os.path.abspath(args.workspace)
|
||||||
while i < len(sys.argv):
|
|
||||||
if sys.argv[i] in ('-w', '--workspace') and i + 1 < len(sys.argv):
|
|
||||||
workspace = sys.argv[i + 1]
|
|
||||||
i += 2
|
|
||||||
else:
|
|
||||||
i += 1
|
|
||||||
if workspace:
|
|
||||||
resolved = os.path.abspath(workspace)
|
|
||||||
if not os.path.isdir(resolved):
|
if not os.path.isdir(resolved):
|
||||||
print(f"Error: '{resolved}' is not a valid directory", flush=True)
|
print(f"Error: '{resolved}' is not a valid directory", flush=True)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
os.chdir(resolved)
|
os.chdir(resolved)
|
||||||
|
|
||||||
|
llm_client = LLMClient(config.llm_baseurl, config.llm_model, config.llm_api_key, config.llm_timeout)
|
||||||
|
|
||||||
services = []
|
services = []
|
||||||
|
|
||||||
if config.XMPP_ENABLED:
|
if config.XMPP_ENABLED:
|
||||||
|
|||||||
@ -1,28 +1,30 @@
|
|||||||
import requests, gc, lancedb, pyarrow, uuid
|
import requests, gc, lancedb, pyarrow, uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
def create_table(db_path, vector_size):
|
|
||||||
db = lancedb.connect(db_path)
|
|
||||||
|
|
||||||
schema_user = pyarrow.schema([
|
# ─── Table Schemas ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _schema_user(vector_size):
|
||||||
|
return pyarrow.schema([
|
||||||
pyarrow.field('id', pyarrow.uuid(), metadata={'description': 'Unique identifier (UUID)'}),
|
pyarrow.field('id', pyarrow.uuid(), metadata={'description': 'Unique identifier (UUID)'}),
|
||||||
pyarrow.field('timestamp', pyarrow.timestamp('ms'), metadata={'description': 'When record created'}),
|
pyarrow.field('timestamp', pyarrow.timestamp('ms'), metadata={'description': 'When record created'}),
|
||||||
pyarrow.field('fullname', pyarrow.string(), metadata={'description': 'Fullname'}),
|
pyarrow.field('fullname', pyarrow.string(), metadata={'description': 'Fullname'}),
|
||||||
pyarrow.field('nickname', pyarrow.string(), metadata={'description': 'Nickname'}),
|
pyarrow.field('nickname', pyarrow.string(), metadata={'description': 'Nickname'}),
|
||||||
pyarrow.field('alias', pyarrow.string(), nullable=True, metadata={'description': 'Another call name'}),
|
pyarrow.field('alias', pyarrow.string(), nullable=True, metadata={'description': 'Another call name'}),
|
||||||
|
pyarrow.field('salutation', pyarrow.string(), nullable=True, metadata={'description': 'Salutation/greeting for the user'}),
|
||||||
pyarrow.field('persona', pyarrow.string(), nullable=True, metadata={'description': 'Persona'}),
|
pyarrow.field('persona', pyarrow.string(), nullable=True, metadata={'description': 'Persona'}),
|
||||||
pyarrow.field('telegram_id', pyarrow.string(), nullable=True, metadata={'description': 'Telegram ID'}),
|
pyarrow.field('telegram_id', pyarrow.string(), nullable=True, metadata={'description': 'Telegram ID'}),
|
||||||
pyarrow.field('telegram_username', pyarrow.string(), nullable=True, metadata={'description': 'Telegram username'}),
|
pyarrow.field('telegram_username', pyarrow.string(), nullable=True, metadata={'description': 'Telegram username'}),
|
||||||
pyarrow.field('xampp_username', pyarrow.string(), nullable=True, metadata={'description': 'XAMPP username'}),
|
pyarrow.field('xampp_username', pyarrow.string(), nullable=True, metadata={'description': 'XAMPP username'}),
|
||||||
pyarrow.field('vector_fullname', pyarrow.list_(pyarrow.float32(), vector_size), metadata={'description': 'Vector data of fullname'}),
|
pyarrow.field('vector_fullname', pyarrow.list_(pyarrow.float32(), vector_size), metadata={'description': 'Vector data of fullname'}),
|
||||||
])
|
])
|
||||||
db.create_table("knowledge_user", schema=schema_user)
|
|
||||||
|
|
||||||
schema_memories = pyarrow.schema([
|
def _schema_memories(vector_size):
|
||||||
|
return pyarrow.schema([
|
||||||
pyarrow.field('id', pyarrow.uuid(), metadata={'description': 'Unique identifier (UUID)'}),
|
pyarrow.field('id', pyarrow.uuid(), metadata={'description': 'Unique identifier (UUID)'}),
|
||||||
pyarrow.field('timestamp', pyarrow.timestamp('ms'), metadata={'description': 'When record created'}),
|
pyarrow.field('timestamp', pyarrow.timestamp('ms'), metadata={'description': 'When record created'}),
|
||||||
pyarrow.field('character', pyarrow.string(), metadata={'description': 'Active Character'}),
|
pyarrow.field('character', pyarrow.string(), metadata={'description': 'Active Character'}),
|
||||||
pyarrow.field('user', pyarrow.string(), metadata={'description': 'Active User'}),
|
pyarrow.field('user', pyarrow.uuid(), metadata={'description': 'Active User UUID'}),
|
||||||
pyarrow.field('event', pyarrow.string(), metadata={'description': 'What event'}),
|
pyarrow.field('event', pyarrow.string(), metadata={'description': 'What event'}),
|
||||||
pyarrow.field('category', pyarrow.string(), metadata={'description': 'Event category'}),
|
pyarrow.field('category', pyarrow.string(), metadata={'description': 'Event category'}),
|
||||||
pyarrow.field('detail', pyarrow.string(), metadata={'description': 'Event detail'}),
|
pyarrow.field('detail', pyarrow.string(), metadata={'description': 'Event detail'}),
|
||||||
@ -30,19 +32,19 @@ def create_table(db_path, vector_size):
|
|||||||
pyarrow.field('emotional', pyarrow.string(), metadata={'description': 'Character emotional state'}),
|
pyarrow.field('emotional', pyarrow.string(), metadata={'description': 'Character emotional state'}),
|
||||||
pyarrow.field('vector_context', pyarrow.list_(pyarrow.float32(), vector_size), metadata={'description': 'Vector data of combined data'}),
|
pyarrow.field('vector_context', pyarrow.list_(pyarrow.float32(), vector_size), metadata={'description': 'Vector data of combined data'}),
|
||||||
])
|
])
|
||||||
db.create_table("knowledge_memories", schema=schema_memories)
|
|
||||||
|
|
||||||
schema_scenario = pyarrow.schema([
|
def _schema_scenario(vector_size):
|
||||||
|
return pyarrow.schema([
|
||||||
pyarrow.field('id', pyarrow.uuid(), metadata={'description': 'Unique identifier (UUID)'}),
|
pyarrow.field('id', pyarrow.uuid(), metadata={'description': 'Unique identifier (UUID)'}),
|
||||||
pyarrow.field('timestamp', pyarrow.timestamp('ms'), metadata={'description': 'When record created'}),
|
pyarrow.field('timestamp', pyarrow.timestamp('ms'), metadata={'description': 'When record created'}),
|
||||||
pyarrow.field('character', pyarrow.string(), metadata={'description': 'Active Character'}),
|
pyarrow.field('character', pyarrow.string(), metadata={'description': 'Active Character'}),
|
||||||
pyarrow.field('user', pyarrow.string(), metadata={'description': 'Active User'}),
|
pyarrow.field('user', pyarrow.uuid(), metadata={'description': 'Active User UUID'}),
|
||||||
pyarrow.field('scenario', pyarrow.string(), metadata={'description': 'Detailed scenario description'}),
|
pyarrow.field('scenario', pyarrow.string(), metadata={'description': 'Detailed scenario description'}),
|
||||||
pyarrow.field('vector_context', pyarrow.list_(pyarrow.float32(), vector_size), metadata={'description': 'Vector data of scenario'}),
|
pyarrow.field('vector_context', pyarrow.list_(pyarrow.float32(), vector_size), metadata={'description': 'Vector data of scenario'}),
|
||||||
])
|
])
|
||||||
db.create_table("knowledge_scenario", schema=schema_scenario)
|
|
||||||
|
|
||||||
schema_todo = pyarrow.schema([
|
def _schema_todo(vector_size):
|
||||||
|
return pyarrow.schema([
|
||||||
pyarrow.field('id', pyarrow.uuid(), metadata={'description': 'Unique identifier (UUID)'}),
|
pyarrow.field('id', pyarrow.uuid(), metadata={'description': 'Unique identifier (UUID)'}),
|
||||||
pyarrow.field('timestamp', pyarrow.timestamp('ms'), metadata={'description': 'When record created'}),
|
pyarrow.field('timestamp', pyarrow.timestamp('ms'), metadata={'description': 'When record created'}),
|
||||||
pyarrow.field('keyword', pyarrow.string(), metadata={'description': 'Keywords to trigger to-do'}),
|
pyarrow.field('keyword', pyarrow.string(), metadata={'description': 'Keywords to trigger to-do'}),
|
||||||
@ -50,9 +52,9 @@ def create_table(db_path, vector_size):
|
|||||||
pyarrow.field('do', pyarrow.string(), metadata={'description': 'Do'}),
|
pyarrow.field('do', pyarrow.string(), metadata={'description': 'Do'}),
|
||||||
pyarrow.field('vector_context', pyarrow.list_(pyarrow.float32(), vector_size), metadata={'description': 'Vector data of keyword+when'}),
|
pyarrow.field('vector_context', pyarrow.list_(pyarrow.float32(), vector_size), metadata={'description': 'Vector data of keyword+when'}),
|
||||||
])
|
])
|
||||||
db.create_table("knowledge_todo", schema=schema_todo)
|
|
||||||
|
|
||||||
schema_outfit_set = pyarrow.schema([
|
def _schema_outfit_set(vector_size):
|
||||||
|
return pyarrow.schema([
|
||||||
pyarrow.field('id', pyarrow.uuid(), metadata={'description': 'Unique identifier (UUID)'}),
|
pyarrow.field('id', pyarrow.uuid(), metadata={'description': 'Unique identifier (UUID)'}),
|
||||||
pyarrow.field('timestamp', pyarrow.timestamp('ms'), metadata={'description': 'When record created'}),
|
pyarrow.field('timestamp', pyarrow.timestamp('ms'), metadata={'description': 'When record created'}),
|
||||||
pyarrow.field('keyword', pyarrow.string(), metadata={'description': 'Keywords of outfit set'}),
|
pyarrow.field('keyword', pyarrow.string(), metadata={'description': 'Keywords of outfit set'}),
|
||||||
@ -60,9 +62,9 @@ def create_table(db_path, vector_size):
|
|||||||
pyarrow.field('outfit', pyarrow.string(), metadata={'description': 'Outfit set description'}),
|
pyarrow.field('outfit', pyarrow.string(), metadata={'description': 'Outfit set description'}),
|
||||||
pyarrow.field('vector_context', pyarrow.list_(pyarrow.float32(), vector_size), metadata={'description': 'Vector data of keyword+when'}),
|
pyarrow.field('vector_context', pyarrow.list_(pyarrow.float32(), vector_size), metadata={'description': 'Vector data of keyword+when'}),
|
||||||
])
|
])
|
||||||
db.create_table("knowledge_outfit_set", schema=schema_outfit_set)
|
|
||||||
|
|
||||||
schema_world = pyarrow.schema([
|
def _schema_world(vector_size):
|
||||||
|
return pyarrow.schema([
|
||||||
pyarrow.field('id', pyarrow.uuid(), metadata={'description': 'Unique identifier (UUID)'}),
|
pyarrow.field('id', pyarrow.uuid(), metadata={'description': 'Unique identifier (UUID)'}),
|
||||||
pyarrow.field('timestamp', pyarrow.timestamp('ms'), metadata={'description': 'When record created'}),
|
pyarrow.field('timestamp', pyarrow.timestamp('ms'), metadata={'description': 'When record created'}),
|
||||||
pyarrow.field('category', pyarrow.string(), metadata={'description': 'Category of world'}),
|
pyarrow.field('category', pyarrow.string(), metadata={'description': 'Category of world'}),
|
||||||
@ -70,9 +72,9 @@ def create_table(db_path, vector_size):
|
|||||||
pyarrow.field('description', pyarrow.string(), metadata={'description': 'Detailed world description'}),
|
pyarrow.field('description', pyarrow.string(), metadata={'description': 'Detailed world description'}),
|
||||||
pyarrow.field('vector_context', pyarrow.list_(pyarrow.float32(), vector_size), metadata={'description': 'Vector data of combined content'}),
|
pyarrow.field('vector_context', pyarrow.list_(pyarrow.float32(), vector_size), metadata={'description': 'Vector data of combined content'}),
|
||||||
])
|
])
|
||||||
db.create_table("knowledge_world", schema=schema_world)
|
|
||||||
|
|
||||||
schema_object = pyarrow.schema([
|
def _schema_object(vector_size):
|
||||||
|
return pyarrow.schema([
|
||||||
pyarrow.field('id', pyarrow.uuid(), metadata={'description': 'Unique identifier (UUID)'}),
|
pyarrow.field('id', pyarrow.uuid(), metadata={'description': 'Unique identifier (UUID)'}),
|
||||||
pyarrow.field('timestamp', pyarrow.timestamp('ms'), metadata={'description': 'When record created'}),
|
pyarrow.field('timestamp', pyarrow.timestamp('ms'), metadata={'description': 'When record created'}),
|
||||||
pyarrow.field('keyword', pyarrow.string(), metadata={'description': 'Keywords of object'}),
|
pyarrow.field('keyword', pyarrow.string(), metadata={'description': 'Keywords of object'}),
|
||||||
@ -84,11 +86,36 @@ def create_table(db_path, vector_size):
|
|||||||
pyarrow.field('usage', pyarrow.string(), metadata={'description': 'Object usage'}),
|
pyarrow.field('usage', pyarrow.string(), metadata={'description': 'Object usage'}),
|
||||||
pyarrow.field('vector_context', pyarrow.list_(pyarrow.float32(), vector_size), metadata={'description': 'Vector data of keyword+when'}),
|
pyarrow.field('vector_context', pyarrow.list_(pyarrow.float32(), vector_size), metadata={'description': 'Vector data of keyword+when'}),
|
||||||
])
|
])
|
||||||
db.create_table("knowledge_object", schema=schema_object)
|
|
||||||
|
|
||||||
# parameters: eat, drink, sleep
|
|
||||||
# Time: morning, afternoon, evening, night
|
|
||||||
|
|
||||||
|
_TABLE_SCHEMAS = {
|
||||||
|
'knowledge_user': _schema_user,
|
||||||
|
'knowledge_memories': _schema_memories,
|
||||||
|
'knowledge_scenario': _schema_scenario,
|
||||||
|
'knowledge_todo': _schema_todo,
|
||||||
|
'knowledge_outfit_set': _schema_outfit_set,
|
||||||
|
'knowledge_world': _schema_world,
|
||||||
|
'knowledge_object': _schema_object,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Table Management ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def create_table(db_path, vector_size):
|
||||||
|
db = lancedb.connect(db_path)
|
||||||
|
for name, schema_fn in _TABLE_SCHEMAS.items():
|
||||||
|
db.create_table(name, schema=schema_fn(vector_size))
|
||||||
|
del db
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_table(db_path, table_name, vector_size):
|
||||||
|
db = lancedb.connect(db_path)
|
||||||
|
existing = db.table_names()
|
||||||
|
if table_name not in existing:
|
||||||
|
schema_fn = _TABLE_SCHEMAS.get(table_name)
|
||||||
|
if schema_fn:
|
||||||
|
db.create_table(table_name, schema=schema_fn(vector_size))
|
||||||
del db
|
del db
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
|
||||||
@ -103,7 +130,7 @@ def embed_text(url, model, text):
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
return data["embeddings"][0]
|
return data["embeddings"][0]
|
||||||
|
|
||||||
def users_store(db_path, model_url, model_name, fullname, nickname, alias, persona, telegram_id, telegram_username, xampp_username):
|
def users_store(db_path, model_url, model_name, fullname, nickname, alias, salutation, persona, telegram_id, telegram_username, xampp_username):
|
||||||
try:
|
try:
|
||||||
db = lancedb.connect(db_path)
|
db = lancedb.connect(db_path)
|
||||||
table = db.open_table("knowledge_user")
|
table = db.open_table("knowledge_user")
|
||||||
@ -114,6 +141,7 @@ def users_store(db_path, model_url, model_name, fullname, nickname, alias, perso
|
|||||||
"fullname": fullname,
|
"fullname": fullname,
|
||||||
"nickname": nickname,
|
"nickname": nickname,
|
||||||
"alias": alias,
|
"alias": alias,
|
||||||
|
"salutation": salutation,
|
||||||
"persona": persona,
|
"persona": persona,
|
||||||
"telegram_id": telegram_id,
|
"telegram_id": telegram_id,
|
||||||
"telegram_username": telegram_username,
|
"telegram_username": telegram_username,
|
||||||
@ -213,7 +241,7 @@ def memories_store(db_path, model_url, model_name, character, user, event, categ
|
|||||||
"id": uuid.uuid4(),
|
"id": uuid.uuid4(),
|
||||||
"timestamp": datetime.now(),
|
"timestamp": datetime.now(),
|
||||||
"character": character,
|
"character": character,
|
||||||
"user": user,
|
"user": _uuid(user),
|
||||||
"event": event,
|
"event": event,
|
||||||
"category": category,
|
"category": category,
|
||||||
"detail": detail,
|
"detail": detail,
|
||||||
@ -242,7 +270,7 @@ def memories_filter(db_path, category=None, character=None, user=None):
|
|||||||
if character:
|
if character:
|
||||||
filters.append(f"character = '{character}'")
|
filters.append(f"character = '{character}'")
|
||||||
if user:
|
if user:
|
||||||
filters.append(f"user = '{user}'")
|
filters.append(f"user = X'{_uuid(user).hex}'")
|
||||||
if filters:
|
if filters:
|
||||||
query = " AND ".join(filters)
|
query = " AND ".join(filters)
|
||||||
|
|
||||||
@ -260,7 +288,7 @@ def memories_latest(db_path, character, user, limit=3):
|
|||||||
try:
|
try:
|
||||||
db = lancedb.connect(db_path)
|
db = lancedb.connect(db_path)
|
||||||
table = db.open_table("knowledge_memories")
|
table = db.open_table("knowledge_memories")
|
||||||
query = f"character = '{character}' AND user = '{user}'"
|
query = f"character = '{character}' AND user = X'{_uuid(user).hex}'"
|
||||||
results = table.search().where(query).to_list()
|
results = table.search().where(query).to_list()
|
||||||
|
|
||||||
results.sort(key=lambda x: x['timestamp'], reverse=True)
|
results.sort(key=lambda x: x['timestamp'], reverse=True)
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
import config, lib.ragroleplay as ragroleplay
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
ragroleplay.create_table(config.ragroleplay_db_path, config.ragroleplay_vector_size)
|
|
||||||
@ -200,8 +200,10 @@ class TelegramClient:
|
|||||||
u = results[0]
|
u = results[0]
|
||||||
context = (
|
context = (
|
||||||
f'[User Context]\n'
|
f'[User Context]\n'
|
||||||
|
f'ID: {u["id"]}\n'
|
||||||
f'Nama: {u["fullname"]} ({u["nickname"]})\n'
|
f'Nama: {u["fullname"]} ({u["nickname"]})\n'
|
||||||
f'Alias: {u.get("alias", "-") or "-"}\n'
|
f'Alias: {u.get("alias", "-") or "-"}\n'
|
||||||
|
f'Salutation: {u.get("salutation", "-") or "-"}\n'
|
||||||
f'Persona: {u.get("persona", "-") or "-"}\n'
|
f'Persona: {u.get("persona", "-") or "-"}\n'
|
||||||
f'Telegram: {u.get("telegram_id", "-") or "-"} / @{u.get("telegram_username", "-") or "-"}\n'
|
f'Telegram: {u.get("telegram_id", "-") or "-"} / @{u.get("telegram_username", "-") or "-"}\n'
|
||||||
f'XMPP: {u.get("xampp_username", "-") or "-"}\n'
|
f'XMPP: {u.get("xampp_username", "-") or "-"}\n'
|
||||||
|
|||||||
@ -304,8 +304,10 @@ class XMPPClient(ClientXMPP):
|
|||||||
u = results[0]
|
u = results[0]
|
||||||
context = (
|
context = (
|
||||||
f'[User Context]\n'
|
f'[User Context]\n'
|
||||||
|
f'ID: {u["id"]}\n'
|
||||||
f'Nama: {u["fullname"]} ({u["nickname"]})\n'
|
f'Nama: {u["fullname"]} ({u["nickname"]})\n'
|
||||||
f'Alias: {u.get("alias", "-") or "-"}\n'
|
f'Alias: {u.get("alias", "-") or "-"}\n'
|
||||||
|
f'Salutation: {u.get("salutation", "-") or "-"}\n'
|
||||||
f'Persona: {u.get("persona", "-") or "-"}\n'
|
f'Persona: {u.get("persona", "-") or "-"}\n'
|
||||||
f'Telegram: {u.get("telegram_id", "-") or "-"} / @{u.get("telegram_username", "-") or "-"}\n'
|
f'Telegram: {u.get("telegram_id", "-") or "-"} / @{u.get("telegram_username", "-") or "-"}\n'
|
||||||
f'XMPP: {u.get("xampp_username", "-") or "-"}\n'
|
f'XMPP: {u.get("xampp_username", "-") or "-"}\n'
|
||||||
|
|||||||
538
tools/rag.py
538
tools/rag.py
@ -1,538 +0,0 @@
|
|||||||
import glob as globmod
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import pandas as pd
|
|
||||||
import lancedb
|
|
||||||
from lancedb.pydantic import LanceModel
|
|
||||||
from sentence_transformers import SentenceTransformer
|
|
||||||
import config
|
|
||||||
|
|
||||||
# ── Embedding Setup ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def load_embedding_model():
|
|
||||||
"""
|
|
||||||
Logika pemuatan model embedding berdasarkan konfigurasi:
|
|
||||||
1. Jika model_path kosong -> gunakan default cache (~/.cache/...)
|
|
||||||
2. Jika model_path diisi tapi folder belum ada -> download lalu simpan ke folder tersebut
|
|
||||||
3. Jika model_path diisi dan folder sudah ada -> load langsung dari folder tersebut
|
|
||||||
"""
|
|
||||||
model_name = "all-MiniLM-L6-v2"
|
|
||||||
custom_path = config.RAG_MODEL_PATH.strip()
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not custom_path:
|
|
||||||
# Kasus 1: Pakai default cache
|
|
||||||
print(f"[RAG] Loading embedding model '{model_name}' from default cache...")
|
|
||||||
return SentenceTransformer(model_name)
|
|
||||||
|
|
||||||
# Kasus 2 & 3: Menggunakan path kustom
|
|
||||||
if os.path.exists(custom_path):
|
|
||||||
print(f"[RAG] Loading embedding model from custom path: {custom_path}")
|
|
||||||
return SentenceTransformer(custom_path)
|
|
||||||
else:
|
|
||||||
print(f"[RAG] Custom path {custom_path} not found. Downloading model first...")
|
|
||||||
model = SentenceTransformer(model_name)
|
|
||||||
# Buat direktori jika belum ada
|
|
||||||
os.makedirs(custom_path, exist_ok=True)
|
|
||||||
model.save(custom_path)
|
|
||||||
print(f"[RAG] Model successfully downloaded and saved to: {custom_path}")
|
|
||||||
return model
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[RAG] Critical Error loading embedding model: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Inisialisasi model saat startup
|
|
||||||
embedding_model = load_embedding_model()
|
|
||||||
|
|
||||||
def get_embedding(text):
|
|
||||||
"""Fungsi standar untuk menghasilkan embedding"""
|
|
||||||
if embedding_model is None:
|
|
||||||
raise Exception("Embedding model not loaded. Check your config or internet connection.")
|
|
||||||
return embedding_model.encode(text).tolist()
|
|
||||||
|
|
||||||
# Skema sederhana untuk menghindari konflik Pydantic
|
|
||||||
class DocumentSchema(LanceModel):
|
|
||||||
text: str
|
|
||||||
id: str
|
|
||||||
metadata: str
|
|
||||||
vector: list[float]
|
|
||||||
|
|
||||||
# ── LanceDB singleton ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
_db = None
|
|
||||||
|
|
||||||
def _get_db():
|
|
||||||
global _db
|
|
||||||
if _db is None:
|
|
||||||
_db = lancedb.connect(config.RAG_PERSIST_DIR)
|
|
||||||
return _db
|
|
||||||
|
|
||||||
def _get_table(name):
|
|
||||||
db = _get_db()
|
|
||||||
if name in db.table_names():
|
|
||||||
return db.open_table(name)
|
|
||||||
return db.create_table(name, schema=DocumentSchema)
|
|
||||||
|
|
||||||
# ── Tool schemas ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
schema_store_knowledge = {
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "store_knowledge",
|
|
||||||
"description": (
|
|
||||||
"Store one or more documents with arbitrary metadata into a RAG collection. "
|
|
||||||
"Metadata is a free-form dict — choose meaningful keys for future filtering "
|
|
||||||
"(e.g., restaurant, category, allergens, spice_level, taste_profile, price"
|
|
||||||
", customer_id, dietary)."
|
|
||||||
),
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"collection": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Target collection name (must be defined in config)"
|
|
||||||
},
|
|
||||||
"documents": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {"type": "string", "description": "Unique document ID"},
|
|
||||||
"text": {"type": "string", "description": "Document body text"},
|
|
||||||
"metadata": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Arbitrary key-value metadata",
|
|
||||||
"default": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["id", "text"]
|
|
||||||
},
|
|
||||||
"description": "List of documents to persist"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["collection", "documents"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema_search_knowledge = {
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "search_knowledge",
|
|
||||||
"description": (
|
|
||||||
"Semantically search a RAG collection. Optionally narrow with a "
|
|
||||||
"metadata filter using SQL-like syntax. "
|
|
||||||
"Example: \"metadata LIKE '%main_course%'\""
|
|
||||||
),
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"collection": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Collection name to search in"
|
|
||||||
},
|
|
||||||
"query": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Natural-language search query"
|
|
||||||
},
|
|
||||||
"n_results": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Max results to return (default 5)",
|
|
||||||
"default": 5
|
|
||||||
},
|
|
||||||
"filter": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Optional SQL-like filter for metadata JSON string",
|
|
||||||
"default": None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["collection", "query"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema_create_collection = {
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "create_collection",
|
|
||||||
"description": (
|
|
||||||
"Create a new RAG collection for a new topic/domain. Use a short, descriptive name "
|
|
||||||
"with underscores (e.g., 'tanaman_hias', 'customer_profiles'). Optionally provide a description."
|
|
||||||
),
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Collection name (lowercase, underscores for spaces)"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "What this collection stores",
|
|
||||||
"default": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["name"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema_delete_collection = {
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "delete_collection",
|
|
||||||
"description": "Permanently delete an entire RAG collection and all documents in it. Use with caution — this cannot be undone.",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Collection name to delete"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["name"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema_list_collections = {
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "list_collections",
|
|
||||||
"description": "List all existing RAG collections with their document count and description.",
|
|
||||||
"parameters": {"type": "object", "properties": {}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema_inspect_collection = {
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "inspect_collection",
|
|
||||||
"description": (
|
|
||||||
"Examine sample documents and metadata fields in a RAG collection. "
|
|
||||||
"Always call this before search_knowledge to learn what metadata keys "
|
|
||||||
"are available for filtering, then pass them in the filter parameter."
|
|
||||||
),
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"collection": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Collection name to inspect"
|
|
||||||
},
|
|
||||||
"sample_size": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Number of sample documents (default 3)",
|
|
||||||
"default": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["collection"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema_ingest_files = {
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "ingest_files",
|
|
||||||
"description": (
|
|
||||||
"Read one or more files (supports glob patterns like *.py or src/**/*.md) "
|
|
||||||
"and store their content into a RAG collection. "
|
|
||||||
"Optionally chunk files into smaller pieces by line count. "
|
|
||||||
"Automatically extracts metadata: filename, path, extension, size, modification time."
|
|
||||||
),
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"collection": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Target collection name (will be created if it doesn't exist)"
|
|
||||||
},
|
|
||||||
"paths": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "string"},
|
|
||||||
"description": "File paths or glob patterns (e.g., ['*.txt', 'src/**/*.py'])"
|
|
||||||
},
|
|
||||||
"chunk_size": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Lines per chunk (0 = whole file as one document)",
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
"chunk_overlap": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Line overlap between chunks (only used when chunk_size > 0)",
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
"recursive": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Search directories recursively when using glob patterns",
|
|
||||||
"default": True
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["collection", "paths"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Tool handlers ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def store_knowledge(collection, documents):
|
|
||||||
try:
|
|
||||||
table = _get_table(collection)
|
|
||||||
data = []
|
|
||||||
for doc in documents:
|
|
||||||
data.append({
|
|
||||||
"id": doc["id"],
|
|
||||||
"text": doc["text"],
|
|
||||||
"metadata": json.dumps(doc.get("metadata", {}), ensure_ascii=False),
|
|
||||||
"vector": get_embedding(doc["text"])
|
|
||||||
})
|
|
||||||
table.add(data)
|
|
||||||
return f"Stored {len(documents)} document(s) in '{collection}'."
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error: {e}"
|
|
||||||
|
|
||||||
def search_knowledge(collection, query, n_results=5, filter=None):
|
|
||||||
try:
|
|
||||||
table = _get_table(collection)
|
|
||||||
# LanceDB semantic search
|
|
||||||
query_vector = get_embedding(query)
|
|
||||||
res = table.search(query_vector).limit(n_results)
|
|
||||||
|
|
||||||
if filter:
|
|
||||||
res = table.search(query_vector).where(filter).limit(n_results)
|
|
||||||
|
|
||||||
df = res.to_pandas()
|
|
||||||
|
|
||||||
if df.empty:
|
|
||||||
return "No results found."
|
|
||||||
|
|
||||||
out = []
|
|
||||||
for _, row in df.iterrows():
|
|
||||||
did = row["id"]
|
|
||||||
txt = row["text"]
|
|
||||||
if len(txt) > 500:
|
|
||||||
txt = txt[:500] + "..."
|
|
||||||
meta = row["metadata"]
|
|
||||||
out.append(f"[{did}]\n text: {txt}\n metadata: {meta}")
|
|
||||||
|
|
||||||
return "\n---\n".join(out)
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error: {e}"
|
|
||||||
|
|
||||||
def create_collection(name, description=""):
|
|
||||||
try:
|
|
||||||
_get_table(name)
|
|
||||||
return f"Collection '{name}' is ready."
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error: {e}"
|
|
||||||
|
|
||||||
def delete_collection(name):
|
|
||||||
try:
|
|
||||||
db = _get_db()
|
|
||||||
table_path = os.path.join(config.RAG_PERSIST_DIR, name)
|
|
||||||
if os.path.exists(table_path):
|
|
||||||
import shutil
|
|
||||||
shutil.rmtree(table_path)
|
|
||||||
return f"Deleted collection '{name}'."
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error: {e}"
|
|
||||||
|
|
||||||
def list_collections():
|
|
||||||
try:
|
|
||||||
db = _get_db()
|
|
||||||
cols = db.table_names()
|
|
||||||
if not cols:
|
|
||||||
return "No collections exist yet."
|
|
||||||
|
|
||||||
out = ["Available collections:"]
|
|
||||||
for col in cols:
|
|
||||||
table = db.open_table(col)
|
|
||||||
cnt = len(table.to_pandas())
|
|
||||||
out.append(f"- {col} [{cnt} docs]")
|
|
||||||
return "\n".join(out)
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error: {e}"
|
|
||||||
|
|
||||||
def inspect_collection(collection, sample_size=3):
|
|
||||||
try:
|
|
||||||
table = _get_table(collection)
|
|
||||||
df = table.to_pandas()
|
|
||||||
cnt = len(df)
|
|
||||||
if cnt == 0:
|
|
||||||
return f"Collection '{collection}' is empty."
|
|
||||||
|
|
||||||
n = min(sample_size, cnt)
|
|
||||||
sample = df.head(n)
|
|
||||||
|
|
||||||
out = [f"Collection: {collection} | Total documents: {cnt}", f"Sample ({n}):"]
|
|
||||||
for _, row in sample.iterrows():
|
|
||||||
txt = row["text"]
|
|
||||||
if len(txt) > 200:
|
|
||||||
txt = txt[:200] + "..."
|
|
||||||
meta = row["metadata"]
|
|
||||||
out.append(f"\n [{row['id']}] text: {txt} metadata: {meta}")
|
|
||||||
|
|
||||||
keys = set()
|
|
||||||
for m_str in sample["metadata"]:
|
|
||||||
try:
|
|
||||||
m_dict = json.loads(m_str)
|
|
||||||
keys.update(m_dict.keys())
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if keys:
|
|
||||||
out.append(f"\nMetadata keys: {', '.join(sorted(keys))}")
|
|
||||||
|
|
||||||
return "\n".join(out)
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error: {e}"
|
|
||||||
|
|
||||||
def ingest_files(collection, paths, chunk_size=0, chunk_overlap=0, recursive=True):
|
|
||||||
try:
|
|
||||||
table = _get_table(collection)
|
|
||||||
all_data = []
|
|
||||||
processed, skipped = 0, 0
|
|
||||||
|
|
||||||
file_set = set()
|
|
||||||
for p in paths:
|
|
||||||
expanded = globmod.glob(p, recursive=recursive)
|
|
||||||
if expanded:
|
|
||||||
file_set.update(expanded)
|
|
||||||
elif os.path.isfile(p):
|
|
||||||
file_set.add(p)
|
|
||||||
else:
|
|
||||||
skipped += 1
|
|
||||||
|
|
||||||
if not file_set:
|
|
||||||
return "No matching files found."
|
|
||||||
|
|
||||||
for fpath in sorted(file_set):
|
|
||||||
if not os.path.isfile(fpath):
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
ext = os.path.splitext(fpath)[1].lower()
|
|
||||||
stat = os.stat(fpath)
|
|
||||||
base_meta = {
|
|
||||||
"filename": os.path.basename(fpath),
|
|
||||||
"path": os.path.relpath(fpath),
|
|
||||||
"extension": ext,
|
|
||||||
"size": stat.st_size,
|
|
||||||
"mtime": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(stat.st_mtime)),
|
|
||||||
}
|
|
||||||
base_name = os.path.splitext(os.path.basename(fpath))[0]
|
|
||||||
|
|
||||||
if ext in (".xlsx", ".xlsm"):
|
|
||||||
try:
|
|
||||||
import openpyxl
|
|
||||||
except ImportError:
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
wb = openpyxl.load_workbook(fpath, read_only=True, data_only=True)
|
|
||||||
for sheet_name in wb.sheetnames:
|
|
||||||
ws = wb[sheet_name]
|
|
||||||
rows = []
|
|
||||||
for row in ws.iter_rows(values_only=True):
|
|
||||||
vals = [str(c) if c is not None else "" for c in row]
|
|
||||||
rows.append("\t".join(vals))
|
|
||||||
|
|
||||||
lines = rows
|
|
||||||
content = "\n".join(lines)
|
|
||||||
if not content.strip():
|
|
||||||
continue
|
|
||||||
|
|
||||||
sheet_meta = dict(base_meta)
|
|
||||||
sheet_meta["sheet"] = sheet_name
|
|
||||||
|
|
||||||
if chunk_size > 0:
|
|
||||||
n_lines = len(lines)
|
|
||||||
cid = 0
|
|
||||||
start = 0
|
|
||||||
while start < n_lines:
|
|
||||||
end = min(start + chunk_size, n_lines)
|
|
||||||
chunk_text = "\n".join(lines[start:end])
|
|
||||||
doc_id = f"{base_name}_{sheet_name}_chunk_{cid}"
|
|
||||||
meta = dict(sheet_meta)
|
|
||||||
meta["chunk_index"] = cid
|
|
||||||
meta["chunk_lines"] = end - start
|
|
||||||
meta["chunk_start_line"] = start + 1
|
|
||||||
all_data.append({
|
|
||||||
"id": doc_id,
|
|
||||||
"text": chunk_text,
|
|
||||||
"metadata": json.dumps(meta, ensure_ascii=False),
|
|
||||||
"vector": get_embedding(chunk_text)
|
|
||||||
})
|
|
||||||
cid += 1
|
|
||||||
step = chunk_size - chunk_overlap
|
|
||||||
start += step if step > 0 else 1
|
|
||||||
processed += 1
|
|
||||||
else:
|
|
||||||
doc_id = f"{base_name}_{sheet_name}"
|
|
||||||
all_data.append({
|
|
||||||
"id": doc_id,
|
|
||||||
"text": content,
|
|
||||||
"metadata": json.dumps(sheet_meta, ensure_ascii=False),
|
|
||||||
"vector": get_embedding(content)
|
|
||||||
})
|
|
||||||
processed += 1
|
|
||||||
wb.close()
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
with open(fpath, "r", encoding="utf-8", errors="replace") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
except Exception:
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
content = "".join(lines)
|
|
||||||
if not content.strip():
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if chunk_size > 0:
|
|
||||||
n_lines = len(lines)
|
|
||||||
cid = 0
|
|
||||||
start = 0
|
|
||||||
while start < n_lines:
|
|
||||||
end = min(start + chunk_size, n_lines)
|
|
||||||
chunk_text = "".join(lines[start:end])
|
|
||||||
doc_id = f"{base_name}_chunk_{cid}"
|
|
||||||
meta = dict(base_meta)
|
|
||||||
meta["chunk_index"] = cid
|
|
||||||
meta["chunk_lines"] = end - start
|
|
||||||
meta["chunk_start_line"] = start + 1
|
|
||||||
all_data.append({
|
|
||||||
"id": doc_id,
|
|
||||||
"text": chunk_text,
|
|
||||||
"metadata": json.dumps(meta, ensure_ascii=False),
|
|
||||||
"vector": get_embedding(chunk_text)
|
|
||||||
})
|
|
||||||
cid += 1
|
|
||||||
step = chunk_size - chunk_overlap
|
|
||||||
start += step if step > 0 else 1
|
|
||||||
processed += 1
|
|
||||||
else:
|
|
||||||
doc_id = base_name
|
|
||||||
all_data.append({
|
|
||||||
"id": doc_id,
|
|
||||||
"text": content,
|
|
||||||
"metadata": json.dumps(base_meta, ensure_ascii=False),
|
|
||||||
"vector": get_embedding(content)
|
|
||||||
})
|
|
||||||
processed += 1
|
|
||||||
|
|
||||||
if all_data:
|
|
||||||
table.add(all_data)
|
|
||||||
|
|
||||||
parts = [f"Ingested {processed} file(s) into '{collection}'"]
|
|
||||||
if processed > 0:
|
|
||||||
parts.append(f"({len(all_data)} document(s) total)")
|
|
||||||
if skipped > 0:
|
|
||||||
parts.append(f"({skipped} file(s) skipped)")
|
|
||||||
return " ".join(parts)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error: {e}"
|
|
||||||
@ -19,6 +19,7 @@ schema_users_store = {
|
|||||||
"fullname": {"type": "string", "description": "Full name of the user"},
|
"fullname": {"type": "string", "description": "Full name of the user"},
|
||||||
"nickname": {"type": "string", "description": "Nickname of the user"},
|
"nickname": {"type": "string", "description": "Nickname of the user"},
|
||||||
"alias": {"type": "string", "description": "Alias of the user"},
|
"alias": {"type": "string", "description": "Alias of the user"},
|
||||||
|
"salutation": {"type": "string", "description": "How to address/greet the user (e.g., Mas, Mbak, Kak)"},
|
||||||
"persona": {"type": "string", "description": "Detailed persona or description of the user"},
|
"persona": {"type": "string", "description": "Detailed persona or description of the user"},
|
||||||
"telegram_id": {"type": "string", "description": "Telegram user ID"},
|
"telegram_id": {"type": "string", "description": "Telegram user ID"},
|
||||||
"telegram_username": {"type": "string", "description": "Telegram username"},
|
"telegram_username": {"type": "string", "description": "Telegram username"},
|
||||||
@ -58,6 +59,7 @@ schema_users_update = {
|
|||||||
"fullname": {"type": "string", "description": "Updated full name"},
|
"fullname": {"type": "string", "description": "Updated full name"},
|
||||||
"nickname": {"type": "string", "description": "Updated nickname"},
|
"nickname": {"type": "string", "description": "Updated nickname"},
|
||||||
"alias": {"type": "string", "description": "Updated alias"},
|
"alias": {"type": "string", "description": "Updated alias"},
|
||||||
|
"salutation": {"type": "string", "description": "Updated salutation"},
|
||||||
"persona": {"type": "string", "description": "Updated persona description"},
|
"persona": {"type": "string", "description": "Updated persona description"},
|
||||||
"telegram_id": {"type": "string", "description": "Updated telegram ID"},
|
"telegram_id": {"type": "string", "description": "Updated telegram ID"},
|
||||||
"telegram_username": {"type": "string", "description": "Updated telegram username"},
|
"telegram_username": {"type": "string", "description": "Updated telegram username"},
|
||||||
@ -109,7 +111,7 @@ schema_memories_store = {
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"character": {"type": "string", "description": "The active character name"},
|
"character": {"type": "string", "description": "The active character name"},
|
||||||
"user": {"type": "string", "description": "The active user name"},
|
"user": {"type": "string", "description": "The UUID of the active user"},
|
||||||
"event": {"type": "string", "description": "Brief summary of the event"},
|
"event": {"type": "string", "description": "Brief summary of the event"},
|
||||||
"category": {"type": "string", "description": "Category of the memory (e.g., 'Emotional', 'Physical', 'Relationship')"},
|
"category": {"type": "string", "description": "Category of the memory (e.g., 'Emotional', 'Physical', 'Relationship')"},
|
||||||
"detail": {"type": "string", "description": "Detailed description of the event"},
|
"detail": {"type": "string", "description": "Detailed description of the event"},
|
||||||
@ -131,7 +133,7 @@ schema_memories_filter = {
|
|||||||
"properties": {
|
"properties": {
|
||||||
"category": {"type": "string", "description": "Filter by memory category"},
|
"category": {"type": "string", "description": "Filter by memory category"},
|
||||||
"character": {"type": "string", "description": "Filter by character name"},
|
"character": {"type": "string", "description": "Filter by character name"},
|
||||||
"user": {"type": "string", "description": "Filter by user name"}
|
"user": {"type": "string", "description": "Filter by user UUID"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,7 +164,7 @@ schema_memories_latest = {
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"character": {"type": "string", "description": "The name of the character"},
|
"character": {"type": "string", "description": "The name of the character"},
|
||||||
"user": {"type": "string", "description": "The name of the user"},
|
"user": {"type": "string", "description": "The UUID of the user"},
|
||||||
"limit": {"type": "integer", "description": "Number of recent memories to retrieve", "default": 3}
|
"limit": {"type": "integer", "description": "Number of recent memories to retrieve", "default": 3}
|
||||||
},
|
},
|
||||||
"required": ["character", "user"]
|
"required": ["character", "user"]
|
||||||
@ -522,7 +524,7 @@ def memories_store(character, user, event, category, detail, physical, emotional
|
|||||||
config.ragroleplay_model_url,
|
config.ragroleplay_model_url,
|
||||||
config.ragroleplay_model_name,
|
config.ragroleplay_model_name,
|
||||||
character,
|
character,
|
||||||
user,
|
_uuid(user),
|
||||||
event,
|
event,
|
||||||
category,
|
category,
|
||||||
detail,
|
detail,
|
||||||
@ -538,7 +540,7 @@ def memories_store(character, user, event, category, detail, physical, emotional
|
|||||||
|
|
||||||
def memories_filter(category=None, character=None, user=None):
|
def memories_filter(category=None, character=None, user=None):
|
||||||
try:
|
try:
|
||||||
results = ragroleplay_lib.memories_filter(config.ragroleplay_db_path, category, character, user)
|
results = ragroleplay_lib.memories_filter(config.ragroleplay_db_path, category, character, _uuid(user) if user else None)
|
||||||
if not results:
|
if not results:
|
||||||
return "Tidak ada memori yang sesuai dengan filter tersebut."
|
return "Tidak ada memori yang sesuai dengan filter tersebut."
|
||||||
|
|
||||||
@ -572,7 +574,7 @@ def memories_summarize(prompt_text, search_limit=5):
|
|||||||
|
|
||||||
def memories_latest(character, user, limit=3):
|
def memories_latest(character, user, limit=3):
|
||||||
try:
|
try:
|
||||||
results = ragroleplay_lib.memories_latest(config.ragroleplay_db_path, character, user, limit)
|
results = ragroleplay_lib.memories_latest(config.ragroleplay_db_path, character, _uuid(user), limit)
|
||||||
if not results:
|
if not results:
|
||||||
return f"Tidak ada memori terbaru yang ditemukan antara {user} dan {character}."
|
return f"Tidak ada memori terbaru yang ditemukan antara {user} dan {character}."
|
||||||
|
|
||||||
@ -617,7 +619,7 @@ def memories_delete(memory_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error while deleting memory: {str(e)}"
|
return f"Error while deleting memory: {str(e)}"
|
||||||
|
|
||||||
def users_store(fullname, nickname, alias=None, persona=None, telegram_id=None, telegram_username=None, xampp_username=None):
|
def users_store(fullname, nickname, alias=None, salutation=None, persona=None, telegram_id=None, telegram_username=None, xampp_username=None):
|
||||||
try:
|
try:
|
||||||
success = ragroleplay_lib.users_store(
|
success = ragroleplay_lib.users_store(
|
||||||
config.ragroleplay_db_path,
|
config.ragroleplay_db_path,
|
||||||
@ -626,6 +628,7 @@ def users_store(fullname, nickname, alias=None, persona=None, telegram_id=None,
|
|||||||
fullname,
|
fullname,
|
||||||
nickname,
|
nickname,
|
||||||
alias,
|
alias,
|
||||||
|
salutation,
|
||||||
persona,
|
persona,
|
||||||
telegram_id,
|
telegram_id,
|
||||||
telegram_username,
|
telegram_username,
|
||||||
@ -651,6 +654,7 @@ def users_filter(query_name=None, unique_id=None, persona_keyword=None, user_id=
|
|||||||
f"User #{i}\n"
|
f"User #{i}\n"
|
||||||
f"ID: {row['id']}\n"
|
f"ID: {row['id']}\n"
|
||||||
f"Nama: {row['fullname']} ({row['nickname']})\n"
|
f"Nama: {row['fullname']} ({row['nickname']})\n"
|
||||||
|
f"Salutation: {row.get('salutation', '-') or '-'}\n"
|
||||||
f"Persona: {row['persona']}\n"
|
f"Persona: {row['persona']}\n"
|
||||||
)
|
)
|
||||||
output.append(user_info)
|
output.append(user_info)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user