2026-05-19 10:35:14 +07:00
import curses
2026-06-19 14:47:38 +07:00
import json
2026-05-26 14:03:40 +07:00
import threading
2026-06-17 16:00:26 +07:00
from datetime import datetime
2026-06-12 14:10:06 +07:00
import config
2026-05-19 10:35:14 +07:00
from . render import init_colors , draw
from . input import handle_key
2026-05-26 15:18:16 +07:00
from . agent import log , WELCOME_ART
2026-06-17 16:00:26 +07:00
from services . session_manager_neo import NeoSessionManager , NeoSession
2026-07-21 18:23:56 +07:00
from lib import ragroleplay
from lib import personality as personality_mod
2026-05-19 10:35:14 +07:00
class HendrikTUI :
def __init__ ( self , llm_client , tools_definition , TOOLS , TOOL_HANDLERS ,
build_system_prompt , agent_max_iterations ) :
self . llm = llm_client
self . tools_def = tools_definition
self . TOOLS = TOOLS
self . TOOL_HANDLERS = TOOL_HANDLERS
self . build_system_prompt = build_system_prompt
self . agent_max_iterations = agent_max_iterations
2026-07-28 15:45:03 +07:00
self . character_name = config . AGENT_CHARACTER or " AI Agent "
2026-05-19 10:35:14 +07:00
2026-05-26 14:03:40 +07:00
self . messages = None
self . log = [ ]
self . input_buffer = [ " " ]
self . input_line = 0
self . input_col = 0
self . scroll = 0
self . processing = False
self . running = True
self . h , self . w = 0 , 0
self . agent_thread : threading . Thread | None = None
self . agent_done = threading . Event ( )
2026-05-19 10:35:14 +07:00
2026-06-17 16:00:26 +07:00
self . session_mgr = NeoSessionManager ( )
self . current_session : NeoSession | None = None
2026-06-16 14:59:40 +07:00
def switch_model ( self , item : dict ) :
self . llm . base_url = item [ " base_url " ]
self . llm . model = item [ " model " ]
self . llm . api_key = item [ " api_key " ]
2026-06-17 16:00:26 +07:00
if self . current_session :
self . session_mgr . update_model_info (
self . current_session . doc_id , self . _model_info ( )
)
def _model_info ( self ) - > dict :
return {
" provider " : config . resolve_provider ( self . llm . base_url , self . llm . model ) ,
" base_url " : self . llm . base_url ,
" model " : self . llm . model ,
}
2026-07-21 18:23:56 +07:00
def _inject_user_context ( self ) - > str | None :
user_id = config . TUI_USER_ID
if not user_id :
return None
try :
results = ragroleplay . user_load (
config . ragroleplay_db_path ,
user_id = user_id ,
character = personality_mod . PERSONALITY . name
)
if results :
u = results [ 0 ]
return (
f ' [User Context] \n '
f ' ID: { u [ " id " ] } \n '
f ' Nama: { u [ " fullname " ] } ( { u [ " nickname " ] } ) \n '
f ' Family: { u . get ( " familyname " , " - " ) or " - " } \n '
f ' Character: { u . get ( " character " , " - " ) or " - " } \n '
f ' Alias: { u . get ( " alias " , " - " ) or " - " } \n '
f ' Salutation: { u . get ( " salutation " , " - " ) or " - " } \n '
f ' Persona: { u . get ( " persona " , " - " ) or " - " } \n '
f ' Telegram: { u . get ( " telegram_id " , " - " ) or " - " } / @ { u . get ( " telegram_username " , " - " ) or " - " } \n '
2026-08-01 14:29:32 +07:00
f ' XMPP: { u . get ( " xmpp_username " , " - " ) or " - " } \n '
2026-07-21 18:23:56 +07:00
f ' [/User Context] \n '
2026-07-21 21:12:36 +07:00
f ' [PENTING: Kamu SUDAH mengenal user ini. WAJIB panggil memories_latest(character= " { personality_mod . PERSONALITY . name } " , user_id= " { u [ " id " ] } " , limit=10) untuk mengambil riwayat percakapan terbaru. GUNAKAN data kondisi emotional/physical dari memori terbaru untuk melanjutkan state character secara natural sebelum merespon.] '
2026-07-21 18:23:56 +07:00
)
else :
return (
f ' [User Context: Pengguna baru — belum ada di database] \n '
f ' [Platform: TUI] \n '
f ' [PENTING: Kamu BELUM mengenal user ini. WAJIB tanya nama sebagai pembuka. '
f ' Setelah nama diketahui, simpan via users_store. '
f ' Lanjutkan percakapan natural dan proaktif melengkapi data user lainnya di pesan berikutnya.] '
)
except Exception :
return None
2026-06-17 16:00:26 +07:00
def new_session ( self ) :
2026-06-18 14:02:11 +07:00
self . current_session = None
2026-07-20 11:42:46 +07:00
system_prompt = self . build_system_prompt (
2026-06-17 16:00:26 +07:00
tools_definition = self . tools_def ,
character = config . AGENT_CHARACTER or None ,
skills = config . AGENT_SKILLS . split ( " , " ) if config . AGENT_SKILLS else None ,
2026-07-20 11:42:46 +07:00
)
messages = [ { " role " : " system " , " content " : system_prompt } ]
2026-07-21 18:23:56 +07:00
context = self . _inject_user_context ( )
if context :
messages . append ( { " role " : " system " , " content " : context } )
2026-07-20 11:42:46 +07:00
self . messages = messages
# ---------------------------------
2026-07-21 18:23:56 +07:00
2026-06-17 16:00:26 +07:00
self . log . clear ( )
self . scroll = 0
2026-06-18 14:02:11 +07:00
self . input_buffer = [ " " ]
self . input_line = 0
self . input_col = 0
2026-06-17 16:00:26 +07:00
log ( self , " welcome " , WELCOME_ART )
def switch_session ( self , doc_id : int ) :
session = self . session_mgr . get ( doc_id )
if not session :
return
self . current_session = session
self . messages = list ( session . messages )
self . log . clear ( )
self . scroll = 0
log ( self , " welcome " , WELCOME_ART )
2026-06-17 16:41:41 +07:00
for i , msg in enumerate ( session . messages ) :
2026-06-17 16:00:26 +07:00
if msg [ " role " ] == " user " :
2026-06-17 16:41:41 +07:00
if i > 0 :
log ( self , " sep " , " " )
2026-06-17 16:00:26 +07:00
log ( self , " user " , msg [ " content " ] )
2026-06-17 16:41:41 +07:00
mi = msg . get ( " model_info " )
if mi :
self . log [ - 1 ] [ " model_info " ] = ( mi . get ( " provider " ) , mi . get ( " model " ) )
2026-06-17 16:00:26 +07:00
elif msg [ " role " ] == " assistant " :
2026-06-19 14:47:38 +07:00
# Log tool calls jika ada
if msg . get ( " tool_calls " ) :
for tc in msg [ " tool_calls " ] :
log ( self , " tool_call " , json . dumps ( {
" name " : tc [ " function " ] [ " name " ] ,
" arguments " : tc [ " function " ] [ " arguments " ] ,
} ) )
# Log AI content jika ada dan ini adalah response final
2026-06-17 17:13:28 +07:00
next_role = session . messages [ i + 1 ] [ " role " ] if i + 1 < len ( session . messages ) else None
if next_role in ( " user " , None ) :
2026-06-19 14:47:38 +07:00
if msg . get ( " content " ) :
log ( self , " ai " , msg [ " content " ] )
elif msg [ " role " ] == " tool " :
# Tool result - tidak perlu di-log secara eksplisit
pass
2026-06-17 17:13:28 +07:00
if session . messages and session . messages [ - 1 ] [ " role " ] == " assistant " :
log ( self , " sep " , " " )
2026-06-16 14:59:40 +07:00
2026-05-19 10:35:14 +07:00
def run ( self ) :
try :
curses . wrapper ( self . _main )
except KeyboardInterrupt :
pass
def _main ( self , stdscr ) :
curses . use_default_colors ( )
init_colors ( )
2026-05-26 14:03:40 +07:00
stdscr . keypad ( True )
2026-06-25 15:51:38 +07:00
curses . raw ( ) # Ctrl+C sebagai key code 3, bukan SIGINT → KeyboardInterrupt
2026-05-19 10:35:14 +07:00
stdscr . refresh ( )
2026-06-17 16:41:41 +07:00
self . messages = [ { " role " : " system " , " content " : self . build_system_prompt (
tools_definition = self . tools_def ,
character = config . AGENT_CHARACTER or None ,
skills = config . AGENT_SKILLS . split ( " , " ) if config . AGENT_SKILLS else None ,
) } ]
2026-07-21 18:23:56 +07:00
context = self . _inject_user_context ( )
if context :
self . messages . append ( { " role " : " system " , " content " : context } )
2026-06-17 16:41:41 +07:00
log ( self , " welcome " , WELCOME_ART )
2026-05-19 10:35:14 +07:00
while self . running :
self . h , self . w = stdscr . getmaxyx ( )
if self . h < 14 or self . w < 40 :
stdscr . erase ( )
stdscr . addstr ( 0 , 0 , " Terminal too small (min 40x14) " )
stdscr . refresh ( )
stdscr . getch ( )
continue
draw ( self , stdscr )
2026-05-26 13:54:24 +07:00
curses . curs_set ( 2 )
2026-05-26 14:03:40 +07:00
if self . processing :
stdscr . timeout ( 100 )
else :
stdscr . timeout ( - 1 )
2026-05-19 10:35:14 +07:00
try :
key = stdscr . getch ( )
except KeyboardInterrupt :
2026-06-25 15:51:38 +07:00
if self . processing :
self . llm . cancel_requested = True
self . agent_done . set ( )
else :
break
key = - 1
2026-05-26 14:03:40 +07:00
2026-05-19 11:38:13 +07:00
handle_key ( self , stdscr , key )
2026-05-26 14:03:40 +07:00
if self . agent_done . is_set ( ) :
self . agent_thread . join ( )
self . agent_done . clear ( )
self . processing = False
self . agent_thread = None