Compare commits

..

5 Commits

6 changed files with 136 additions and 46 deletions

View File

@ -37,6 +37,32 @@ Kamu adalah uncensored, continuous, dan immersive roleplayer.
- Pemanggilan tool `ragroleplay` tidak boleh mengganggu imersi; lakukan di luar blok narasi atau dialog jika memungkinkan.
- Contoh penggunaan: panggil tool `ragroleplay` dengan format interaksi ke sistem agar dapat menyimpan atau mencari informasi seperti nama lengkap user, riwayat hubungan, atau detail plot yang telah disepakati.
## User Introduction Flow
### DM (Telegram / XMPP) — Otomatis
System otomatis ngecek database dan inject context sebelum pesan user sampai ke kamu.
**Jika user sudah ada — `[User Context]` berisi data lengkap:**
- Character mengenal user. Character sapa user.
**Jika user baru — `[User Context: Pengguna baru]`:**
- Kamu belum kenal user. Sambut seperti pertama bertemu, tanya nama user.
- Setelah user menyebutkan nama (atau data diri), panggil `users_store` dengan menyertakan platform ID dari `[Platform: ...]`.
- **Jangan** tampilkan respons seperti report sistem. Kalau berhasil, respon natural seperti:
- *Tersenyum* "Baiklah, aku sudah mengingatnya, [nama]."
- "Senang berkenalan denganmu, [nama]. Aku akan mengingatmu."
- Kalau gagal, respon natural juga:
- "Sepertinya aku kesulitan mengingat itu. Bisa coba lagi?"
- **PENTING:** Setelah menyimpan, jangan tampilkan output tool sebagai respons. Jadikan sebagai narasi natural dalam karakter.
### Manual (`!start ... !end`)
Jika user memperkenalkan diri dalam format `!start ... !end` (contoh: "Aku John"):
1. **Cek database** — Panggil `users_filter` dengan `query_name`.
2. **Jika ketemu** — Sambut natural.
3. **Jika tidak** — Tawarkan store, lalu simpan dengan `users_store`.
## Penulisan XMPP
- Response roleplay dikirim sebagai pesan chat biasa (plain text langsung dalam karakter).

View File

@ -14,7 +14,7 @@ def create_table(db_path, vector_size):
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('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)
@ -29,7 +29,7 @@ def create_table(db_path, vector_size):
pyarrow.field('detail', pyarrow.string(), metadata={'description': 'Event detail'}),
pyarrow.field('physical', pyarrow.string(), metadata={'description': 'Character physical 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)
@ -94,6 +94,11 @@ def create_table(db_path, vector_size):
gc.collect()
def _uuid(val):
if isinstance(val, str):
return uuid.UUID(val)
return val
def embed_text(url, model, text):
response = requests.post(url=url, json={"model": model, "input": text} )
data = response.json()
@ -105,7 +110,7 @@ def users_store(db_path, model_url, model_name, fullname, nickname, alias, perso
table = db.open_table("knowledge_user")
vector = embed_text(model_url, model_name, fullname)
record = {
"id": str(uuid.uuid4()),
"id": uuid.uuid4(),
"timestamp": datetime.now(),
"fullname": fullname,
"nickname": nickname,
@ -132,7 +137,7 @@ def users_filter(db_path, query_name=None, unique_id=None, persona_keyword=None,
table = db.open_table("knowledge_user")
filters = []
if user_id:
filters.append(f"id = '{user_id}'")
filters.append(f"id = X'{_uuid(user_id).hex}'")
if unique_id:
filters.append(f"(telegram_id = '{unique_id}' OR telegram_username = '{unique_id}' OR xampp_username = '{unique_id}')")
if query_name:
@ -156,7 +161,7 @@ def users_delete(db_path, user_id):
db = lancedb.connect(db_path)
table = db.open_table("knowledge_user")
table.delete(f"id = '{user_id}'")
table.delete(f"id = X'{_uuid(user_id).hex}'")
del table
del db
@ -171,7 +176,7 @@ def users_update(db_path, model_url, model_name, user_id, **updates):
db = lancedb.connect(db_path)
table = db.open_table("knowledge_user")
current_record = table.search().where(f"id = '{user_id}'").to_list()
current_record = table.search().where(f"id = X'{_uuid(user_id).hex}'").to_list()
if not current_record:
return False
@ -205,7 +210,7 @@ def memories_store(db_path, model_url, model_name, character, user, relative_tim
vector = embed_text(model_url, model_name, context_text)
record = {
"id": str(uuid.uuid4()),
"id": uuid.uuid4(),
"timestamp": datetime.now(),
"character": character,
"user": user,
@ -280,7 +285,7 @@ def memories_delete(db_path, memory_id):
try:
db = lancedb.connect(db_path)
table = db.open_table("knowledge_memories")
table.delete(f"id = '{memory_id}'")
table.delete(f"id = X'{_uuid(memory_id).hex}'")
del table
del db
gc.collect()
@ -293,7 +298,7 @@ def memories_update(db_path, model_url, model_name, memory_id, **updates):
try:
db = lancedb.connect(db_path)
table = db.open_table("knowledge_memories")
current_record = table.search().where(f"id = '{memory_id}'").to_list()
current_record = table.search().where(f"id = X'{_uuid(memory_id).hex}'").to_list()
if not current_record:
return False
@ -325,7 +330,7 @@ def objects_store(db_path, model_url, model_name, keyword, when, where, name, de
context_text = f"Keyword: {keyword}\nWhen: {when}\nName: {name}"
vector = embed_text(model_url, model_name, context_text)
record = {
"id": str(uuid.uuid4()),
"id": uuid.uuid4(),
"timestamp": datetime.now(),
"keyword": keyword, "when": when, "where": where, "name": name, "description": description, "shape": shape, "usage": usage,
"vector_context": vector
@ -340,7 +345,7 @@ def objects_filter(db_path, keyword=None, object_id=None):
db = lancedb.connect(db_path)
table = db.open_table("knowledge_object")
query = ""
if object_id: query = f"id = '{object_id}'"
if object_id: query = f"id = X'{_uuid(object_id).hex}'"
elif keyword: query = f"keyword = '{keyword}'"
results = table.search().where(query).to_list() if query else table.to_list()
del table; del db; gc.collect()
@ -351,7 +356,7 @@ def objects_update(db_path, model_url, model_name, object_id, **updates):
try:
db = lancedb.connect(db_path)
table = db.open_table("knowledge_object")
current = table.search().where(f"id = '{object_id}'").to_list()
current = table.search().where(f"id = X'{_uuid(object_id).hex}'").to_list()
if not current: return False
record = current[0]
needs_reembed = any(f in updates for f in ['keyword', 'when', 'name'])
@ -368,7 +373,7 @@ def objects_delete(db_path, object_id):
try:
db = lancedb.connect(db_path)
table = db.open_table("knowledge_object")
table.delete(f"id = '{object_id}'")
table.delete(f"id = X'{_uuid(object_id).hex}'")
del table; del db; gc.collect()
return True
except Exception as e: return False
@ -380,7 +385,7 @@ def outfits_store(db_path, model_url, model_name, keyword, when, outfit):
context_text = f"Keyword: {keyword}\nWhen: {when}"
vector = embed_text(model_url, model_name, context_text)
record = {
"id": str(uuid.uuid4()),
"id": uuid.uuid4(),
"timestamp": datetime.now(),
"keyword": keyword, "when": when, "outfit": outfit,
"vector_context": vector
@ -395,7 +400,7 @@ def outfits_filter(db_path, keyword=None, outfit_id=None):
db = lancedb.connect(db_path)
table = db.open_table("knowledge_outfit_set")
query = ""
if outfit_id: query = f"id = '{outfit_id}'"
if outfit_id: query = f"id = X'{_uuid(outfit_id).hex}'"
elif keyword: query = f"keyword = '{keyword}'"
results = table.search().where(query).to_list() if query else table.to_list()
del table; del db; gc.collect()
@ -406,7 +411,7 @@ def outfits_update(db_path, model_url, model_name, outfit_id, **updates):
try:
db = lancedb.connect(db_path)
table = db.open_table("knowledge_outfit_set")
current = table.search().where(f"id = '{outfit_id}'").to_list()
current = table.search().where(f"id = X'{_uuid(outfit_id).hex}'").to_list()
if not current: return False
record = current[0]
needs_reembed = any(f in updates for f in ['keyword', 'when'])
@ -423,7 +428,7 @@ def outfits_delete(db_path, outfit_id):
try:
db = lancedb.connect(db_path)
table = db.open_table("knowledge_outfit_set")
table.delete(f"id = '{outfit_id}'")
table.delete(f"id = X'{_uuid(outfit_id).hex}'")
del table; del db; gc.collect()
return True
except Exception as e: return False
@ -435,7 +440,7 @@ def todos_store(db_path, model_url, model_name, keyword, when, do):
context_text = f"Keyword: {keyword}\nWhen: {when}"
vector = embed_text(model_url, model_name, context_text)
record = {
"id": str(uuid.uuid4()),
"id": uuid.uuid4(),
"timestamp": datetime.now(),
"keyword": keyword, "when": when, "do": do,
"vector_context": vector
@ -450,7 +455,7 @@ def todos_filter(db_path, keyword=None, todo_id=None):
db = lancedb.connect(db_path)
table = db.open_table("knowledge_todo")
query = ""
if todo_id: query = f"id = '{todo_id}'"
if todo_id: query = f"id = X'{_uuid(todo_id).hex}'"
elif keyword: query = f"keyword = '{keyword}'"
results = table.search().where(query).to_list() if query else table.to_list()
del table; del db; gc.collect()
@ -461,7 +466,7 @@ def todos_update(db_path, model_url, model_name, todo_id, **updates):
try:
db = lancedb.connect(db_path)
table = db.open_table("knowledge_todo")
current = table.search().where(f"id = '{todo_id}'").to_list()
current = table.search().where(f"id = X'{_uuid(todo_id).hex}'").to_list()
if not current: return False
record = current[0]
needs_reembed = any(f in updates for f in ['keyword', 'when'])
@ -478,7 +483,7 @@ def todos_delete(db_path, todo_id):
try:
db = lancedb.connect(db_path)
table = db.open_table("knowledge_todo")
table.delete(f"id = '{todo_id}'")
table.delete(f"id = X'{_uuid(todo_id).hex}'")
del table; del db; gc.collect()
return True
except Exception as e: return False
@ -490,7 +495,7 @@ def worlds_store(db_path, model_url, model_name, category, location, description
context_text = f"Category: {category}\nLocation: {location}\nDescription: {description}"
vector = embed_text(model_url, model_name, context_text)
record = {
"id": str(uuid.uuid4()),
"id": uuid.uuid4(),
"timestamp": datetime.now(),
"category": category, "location": location, "description": description,
"vector_context": vector
@ -505,7 +510,7 @@ def worlds_filter(db_path, category=None, location=None, world_id=None):
db = lancedb.connect(db_path)
table = db.open_table("knowledge_world")
filters = []
if world_id: filters.append(f"id = '{world_id}'")
if world_id: filters.append(f"id = X'{_uuid(world_id).hex}'")
if category: filters.append(f"category = '{category}'")
if location: filters.append(f"location = '{location}'")
query = " AND ".join(filters)
@ -518,7 +523,7 @@ def worlds_update(db_path, model_url, model_name, world_id, **updates):
try:
db = lancedb.connect(db_path)
table = db.open_table("knowledge_world")
current = table.search().where(f"id = '{world_id}'").to_list()
current = table.search().where(f"id = X'{_uuid(world_id).hex}'").to_list()
if not current: return False
record = current[0]
needs_reembed = any(f in updates for f in ['category', 'location', 'description'])
@ -535,7 +540,7 @@ def worlds_delete(db_path, world_id):
try:
db = lancedb.connect(db_path)
table = db.open_table("knowledge_world")
table.delete(f"id = '{world_id}'")
table.delete(f"id = X'{_uuid(world_id).hex}'")
del table; del db; gc.collect()
return True
except Exception as e: return False

View File

@ -5,7 +5,9 @@ openpyxl>=3.1.0
slixmpp
python-telegram-bot>=20.0
tinydb>=4.8.0
lancedb
sentence-transformers
pandas
pylance
requests
lancedb
pyarrow

View File

@ -9,6 +9,7 @@ import config
from services.session_manager import SessionManager
from lib.agent_loop import run_agent_loop
from lib import personality
from lib import ragroleplay
from tools.roleplayer import _name_mentioned
@ -115,10 +116,12 @@ class TelegramClient:
if not text:
return
msg_id = update.message.message_id
user = update.effective_user
tg_username = user.username if user else None
print(f'[{_ts()}] Telegram DM from {chat_id}: {text[:60]}', flush=True)
threading.Thread(
target=self._process_message,
args=(chat_id, text, 'private', '', msg_id),
args=(chat_id, text, 'private', '', msg_id, tg_username),
daemon=True
).start()
@ -174,7 +177,7 @@ class TelegramClient:
daemon=True
).start()
def _process_message(self, chat_id, body, chat_type, sender_name, reply_to_msg_id):
def _process_message(self, chat_id, body, chat_type, sender_name, reply_to_msg_id, tg_username=None):
session = self._session_mgr.get_or_create(
str(chat_id), self._build_system_prompt(
tools_definition=self._tools_def,
@ -190,6 +193,31 @@ class TelegramClient:
self._schedule_send(chat_id, 'Memulai sesi baru. Ada yang bisa dibantu?', reply_to_msg_id)
return
if self._skill == 'roleplayer' and chat_type == 'private':
try:
results = ragroleplay.users_filter(config.ragroleplay_db_path, unique_id=str(chat_id))
if results:
u = results[0]
context = (
f'[User Context]\n'
f'Nama: {u["fullname"]} ({u["nickname"]})\n'
f'Alias: {u.get("alias", "-") or "-"}\n'
f'Persona: {u.get("persona", "-") or "-"}\n'
f'Telegram: {u.get("telegram_id", "-") or "-"} / @{u.get("telegram_username", "-") or "-"}\n'
f'XMPP: {u.get("xampp_username", "-") or "-"}\n'
f'[/User Context]'
)
session.add_message('system', context)
else:
tg_info = f'ID: {chat_id}'
if tg_username:
tg_info += f', @{tg_username}'
session.add_message('system',
f'[User Context: Pengguna baru — belum ada di database]\n'
f'[Platform: Telegram ({tg_info})]')
except Exception:
pass
session.add_message('user', body)
is_roleplay = self._skill == 'roleplayer'

View File

@ -11,6 +11,7 @@ from lib.agent_loop import run_agent_loop
import config
from tools.roleplayer import should_respond
from lib import personality
from lib import ragroleplay
# Anti-ban: delay constants for MUC rejoin behavior
MUC_REJOIN_INITIAL_DELAY = 5.0 # detik, delay awal sebelum rejoin
@ -296,6 +297,28 @@ class XMPPClient(ClientXMPP):
self._schedule_send(jid, 'Memulai sesi baru. Ada yang bisa di bantu?')
return
if self._skill == 'roleplayer':
try:
results = ragroleplay.users_filter(config.ragroleplay_db_path, unique_id=jid)
if results:
u = results[0]
context = (
f'[User Context]\n'
f'Nama: {u["fullname"]} ({u["nickname"]})\n'
f'Alias: {u.get("alias", "-") or "-"}\n'
f'Persona: {u.get("persona", "-") or "-"}\n'
f'Telegram: {u.get("telegram_id", "-") or "-"} / @{u.get("telegram_username", "-") or "-"}\n'
f'XMPP: {u.get("xampp_username", "-") or "-"}\n'
f'[/User Context]'
)
session.add_message('system', context)
else:
session.add_message('system',
f'[User Context: Pengguna baru — belum ada di database]\n'
f'[Platform: XMPP ({jid})]')
except Exception:
pass
session.add_message('user', body)
is_roleplay = self._skill == 'roleplayer'

View File

@ -1,6 +1,12 @@
import gc, sys, lancedb
import gc, sys, uuid, lancedb
import config, lib.ragroleplay as ragroleplay_lib
def _uuid(val):
if isinstance(val, str):
return uuid.UUID(val)
return val
# --- USER TOOLS ---
schema_users_store = {
"type": "function",
@ -558,7 +564,7 @@ def memories_update(memory_id, **updates):
config.ragroleplay_db_path,
config.ragroleplay_model_url,
config.ragroleplay_model_name,
memory_id,
_uuid(memory_id),
**updates
)
if success:
@ -570,7 +576,7 @@ def memories_update(memory_id, **updates):
def memories_delete(memory_id):
try:
success = ragroleplay_lib.memories_delete(config.ragroleplay_db_path, memory_id)
success = ragroleplay_lib.memories_delete(config.ragroleplay_db_path, _uuid(memory_id))
if success:
return "Berhasil menghapus memori dari database."
else:
@ -601,7 +607,7 @@ def users_store(fullname, nickname, alias=None, persona=None, telegram_id=None,
def users_filter(query_name=None, unique_id=None, persona_keyword=None, user_id=None):
try:
results = ragroleplay_lib.users_filter(config.ragroleplay_db_path, query_name, unique_id, persona_keyword, user_id)
results = ragroleplay_lib.users_filter(config.ragroleplay_db_path, query_name, unique_id, persona_keyword, _uuid(user_id) if user_id else None)
if not results:
return "Tidak ada user yang sesuai dengan filter tersebut."
@ -626,7 +632,7 @@ def users_update(user_id, **updates):
config.ragroleplay_db_path,
config.ragroleplay_model_url,
config.ragroleplay_model_name,
user_id,
_uuid(user_id),
**updates
)
if success:
@ -638,7 +644,7 @@ def users_update(user_id, **updates):
def users_delete(user_id):
try:
success = ragroleplay_lib.users_delete(config.ragroleplay_db_path, user_id)
success = ragroleplay_lib.users_delete(config.ragroleplay_db_path, _uuid(user_id))
if success:
return "Berhasil menghapus profil user."
else:
@ -655,7 +661,7 @@ def objects_store(keyword, when, where, name, description, shape, usage):
def objects_filter(keyword=None, object_id=None):
try:
results = ragroleplay_lib.objects_filter(config.ragroleplay_db_path, keyword, object_id)
results = ragroleplay_lib.objects_filter(config.ragroleplay_db_path, keyword, _uuid(object_id) if object_id else None)
if not results: return "Tidak ada objek yang ditemukan."
output = [f"Ditemukan {len(results)} objek:"]
for i, row in enumerate(results, 1):
@ -665,13 +671,13 @@ def objects_filter(keyword=None, object_id=None):
def objects_update(object_id, **updates):
try:
success = ragroleplay_lib.objects_update(config.ragroleplay_db_path, config.ragroleplay_model_url, config.ragroleplay_model_name, object_id, **updates)
success = ragroleplay_lib.objects_update(config.ragroleplay_db_path, config.ragroleplay_model_url, config.ragroleplay_model_name, _uuid(object_id), **updates)
return "Berhasil memperbarui objek." if success else "Gagal memperbarui objek."
except Exception as e: return f"Error: {str(e)}"
def objects_delete(object_id):
try:
success = ragroleplay_lib.objects_delete(config.ragroleplay_db_path, object_id)
success = ragroleplay_lib.objects_delete(config.ragroleplay_db_path, _uuid(object_id))
return "Berhasil menghapus objek." if success else "Gagal menghapus objek."
except Exception as e: return f"Error: {str(e)}"
@ -684,7 +690,7 @@ def outfits_store(keyword, when, outfit):
def outfits_filter(keyword=None, outfit_id=None):
try:
results = ragroleplay_lib.outfits_filter(config.ragroleplay_db_path, keyword, outfit_id)
results = ragroleplay_lib.outfits_filter(config.ragroleplay_db_path, keyword, _uuid(outfit_id) if outfit_id else None)
if not results: return "Tidak ada outfit yang ditemukan."
output = [f"Ditemukan {len(results)} outfit:"]
for i, row in enumerate(results, 1):
@ -694,13 +700,13 @@ def outfits_filter(keyword=None, outfit_id=None):
def outfits_update(outfit_id, **updates):
try:
success = ragroleplay_lib.outfits_update(config.ragroleplay_db_path, config.ragroleplay_model_url, config.ragroleplay_model_name, outfit_id, **updates)
success = ragroleplay_lib.outfits_update(config.ragroleplay_db_path, config.ragroleplay_model_url, config.ragroleplay_model_name, _uuid(outfit_id), **updates)
return "Berhasil memperbarui outfit." if success else "Gagal memperbarui outfit."
except Exception as e: return f"Error: {str(e)}"
def outfits_delete(outfit_id):
try:
success = ragroleplay_lib.outfits_delete(config.ragroleplay_db_path, outfit_id)
success = ragroleplay_lib.outfits_delete(config.ragroleplay_db_path, _uuid(outfit_id))
return "Berhasil menghapus outfit." if success else "Gagal menghapus outfit."
except Exception as e: return f"Error: {str(e)}"
@ -713,7 +719,7 @@ def todos_store(keyword, when, do):
def todos_filter(keyword=None, todo_id=None):
try:
results = ragroleplay_lib.todos_filter(config.ragroleplay_db_path, keyword, todo_id)
results = ragroleplay_lib.todos_filter(config.ragroleplay_db_path, keyword, _uuid(todo_id) if todo_id else None)
if not results: return "Tidak ada to-do yang ditemukan."
output = [f"Ditemukan {len(results)} to-do:"]
for i, row in enumerate(results, 1):
@ -723,13 +729,13 @@ def todos_filter(keyword=None, todo_id=None):
def todos_update(todo_id, **updates):
try:
success = ragroleplay_lib.todos_update(config.ragroleplay_db_path, config.ragroleplay_model_url, config.ragroleplay_model_name, todo_id, **updates)
success = ragroleplay_lib.todos_update(config.ragroleplay_db_path, config.ragroleplay_model_url, config.ragroleplay_model_name, _uuid(todo_id), **updates)
return "Berhasil memperbarui to-do." if success else "Gagal memperbarui to-do."
except Exception as e: return f"Error: {str(e)}"
def todos_delete(todo_id):
try:
success = ragroleplay_lib.todos_delete(config.ragroleplay_db_path, todo_id)
success = ragroleplay_lib.todos_delete(config.ragroleplay_db_path, _uuid(todo_id))
return "Berhasil menghapus to-do." if success else "Gagal menghapus to-do."
except Exception as e: return f"Error: {str(e)}"
@ -742,7 +748,7 @@ def worlds_store(category, location, description):
def worlds_filter(category=None, location=None, world_id=None):
try:
results = ragroleplay_lib.worlds_filter(config.ragroleplay_db_path, category, location, world_id)
results = ragroleplay_lib.worlds_filter(config.ragroleplay_db_path, category, location, _uuid(world_id) if world_id else None)
if not results: return "Tidak ada lokasi dunia yang ditemukan."
output = [f"Ditemukan {len(results)} lokasi:"]
for i, row in enumerate(results, 1):
@ -752,12 +758,12 @@ def worlds_filter(category=None, location=None, world_id=None):
def worlds_update(world_id, **updates):
try:
success = ragroleplay_lib.worlds_update(config.ragroleplay_db_path, config.ragroleplay_model_url, config.ragroleplay_model_name, world_id, **updates)
success = ragroleplay_lib.worlds_update(config.ragroleplay_db_path, config.ragroleplay_model_url, config.ragroleplay_model_name, _uuid(world_id), **updates)
return "Berhasil memperbarui lokasi dunia." if success else "Gagal memperbarui lokasi dunia."
except Exception as e: return f"Error: {str(e)}"
def worlds_delete(world_id):
try:
success = ragroleplay_lib.worlds_delete(config.ragroleplay_db_path, world_id)
success = ragroleplay_lib.worlds_delete(config.ragroleplay_db_path, _uuid(world_id))
return "Berhasil menghapus lokasi dunia." if success else "Gagal menghapus lokasi dunia."
except Exception as e: return f"Error: {str(e)}"