Initial commit: Natiris AI Agent Orchestration System
This commit is contained in:
251
tui/natiris_tui.py
Executable file
251
tui/natiris_tui.py
Executable file
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Natiris TUI – Text User Interface für den Companion
|
||||
Features:
|
||||
- Realtime State Anzeige
|
||||
- Chat Interface (simuliert)
|
||||
- Admin-Befehle
|
||||
- Quick Actions
|
||||
"""
|
||||
|
||||
import curses
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
PATHS = {
|
||||
"state": os.path.expanduser("~/natiris/core/natiris_full_state.json"),
|
||||
"config": os.path.expanduser("~/natiris/config/character_genesis.json"),
|
||||
"ollama": os.path.expanduser("~/natiris/bridges/ollama_response.json"),
|
||||
"log": os.path.expanduser("~/natiris/admin/admin_log.json"),
|
||||
}
|
||||
|
||||
class NatirisTUI:
|
||||
def __init__(self, stdscr):
|
||||
self.stdscr = stdscr
|
||||
self.running = True
|
||||
self.mode = "STATE" # STATE, CHAT, ADMIN
|
||||
self.user_input = ""
|
||||
self.log_lines = []
|
||||
self.last_update = 0
|
||||
self.state = {}
|
||||
self.response = ""
|
||||
|
||||
# Init
|
||||
curses.curs_set(0)
|
||||
curses.start_color()
|
||||
curses.use_default_colors()
|
||||
curses.init_pair(1, curses.COLOR_CYAN, -1)
|
||||
curses.init_pair(2, curses.COLOR_GREEN, -1)
|
||||
curses.init_pair(3, curses.COLOR_YELLOW, -1)
|
||||
curses.init_pair(4, curses.COLOR_RED, -1)
|
||||
curses.init_pair(5, curses.COLOR_WHITE, -1)
|
||||
self.stdscr.nodelay(True)
|
||||
|
||||
# Initial load
|
||||
self.update_state()
|
||||
|
||||
def update_state(self):
|
||||
try:
|
||||
with open(PATHS["state"]) as f:
|
||||
self.state = json.load(f)
|
||||
except Exception:
|
||||
self.state = {}
|
||||
|
||||
try:
|
||||
with open(PATHS["ollama"]) as f:
|
||||
resp = json.load(f)
|
||||
self.response = resp.get("response", "")[:120]
|
||||
except Exception:
|
||||
self.response = ""
|
||||
|
||||
self.last_update = time.time()
|
||||
|
||||
def draw_header(self):
|
||||
h, w = self.stdscr.getmaxyx()
|
||||
self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
|
||||
self.stdscr.addstr(0, 2, " NATIRIS COMPANION v4.x ")
|
||||
self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
|
||||
self.stdscr.addstr(1, 2, f" Uptime: {int(time.time() - os.path.getmtime(PATHS['state']) if os.path.exists(PATHS['state']) else time.time())}s ")
|
||||
self.stdscr.addstr(2, 2, f" Mode: {self.mode} | {datetime.now(timezone.utc).strftime('%H:%M:%S')} ")
|
||||
|
||||
def draw_state_panel(self, top, left, height, width):
|
||||
box_top = top
|
||||
box_left = left
|
||||
box_height = height
|
||||
box_width = width
|
||||
|
||||
# Box frame
|
||||
self.stdscr.attron(curses.color_pair(5))
|
||||
for y in range(box_top, box_top + box_height):
|
||||
self.stdscr.addstr(y, box_left, "│")
|
||||
self.stdscr.addstr(y, box_left + box_width - 1, "│")
|
||||
self.stdscr.addstr(box_top, box_left, "┌")
|
||||
self.stdscr.addstr(box_top, box_left + box_width - 1, "┐")
|
||||
self.stdscr.addstr(box_top + box_height - 1, box_left, "└")
|
||||
self.stdscr.addstr(box_top + box_height - 1, box_left + box_width - 1, "┘")
|
||||
self.stdscr.addstr(box_top, box_left + 1, "─" * (box_width - 2))
|
||||
self.stdscr.addstr(box_top + box_height - 1, box_left + 1, "─" * (box_width - 2))
|
||||
self.stdscr.attroff(curses.color_pair(5))
|
||||
|
||||
# Title
|
||||
self.stdscr.attron(curses.color_pair(2) | curses.A_BOLD)
|
||||
self.stdscr.addstr(box_top, box_left + 2, " CORE STATE ")
|
||||
self.stdscr.attroff(curses.color_pair(2) | curses.A_BOLD)
|
||||
|
||||
state = self.state.get("modules", {})
|
||||
core = self.state.get("core_state", {})
|
||||
|
||||
lines = [
|
||||
f" Mood: {core.get('mood', '?')}/10",
|
||||
f" Loneliness: {core.get('loneliness', '?')}/10",
|
||||
f" Anxiety: {core.get('anxiety', '?')}/10",
|
||||
f" Bonded: {core.get('bonded_to') or 'none'}",
|
||||
f" Exclusivity: {state.get('Bond', {}).get('exclusivity_active', False)}",
|
||||
f" Tone: {state.get('Expression', {}).get('tone', 'neutral')}",
|
||||
f" Response: {self.response}",
|
||||
]
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
self.stdscr.addstr(box_top + 2 + i, box_left + 2, line[:box_width - 4])
|
||||
|
||||
def draw_chat_panel(self, top, left, height, width):
|
||||
box_top = top
|
||||
box_left = left
|
||||
box_height = height
|
||||
box_width = width
|
||||
|
||||
self.stdscr.attron(curses.color_pair(5))
|
||||
for y in range(box_top, box_top + box_height):
|
||||
self.stdscr.addstr(y, box_left, "│")
|
||||
self.stdscr.addstr(y, box_left + box_width - 1, "│")
|
||||
self.stdscr.addstr(box_top, box_left, "┌")
|
||||
self.stdscr.addstr(box_top, box_left + box_width - 1, "┐")
|
||||
self.stdscr.addstr(box_top + box_height - 1, box_left, "└")
|
||||
self.stdscr.addstr(box_top + box_height - 1, box_left + box_width - 1, "┘")
|
||||
self.stdscr.addstr(box_top, box_left + 1, "─" * (box_width - 2))
|
||||
self.stdscr.addstr(box_top + box_height - 1, box_left + 1, "─" * (box_width - 2))
|
||||
self.stdscr.attroff(curses.color_pair(5))
|
||||
|
||||
self.stdscr.attron(curses.color_pair(3) | curses.A_BOLD)
|
||||
self.stdscr.addstr(box_top, box_left + 2, " CHAT OUTPUT ")
|
||||
self.stdscr.attroff(curses.color_pair(3) | curses.A_BOLD)
|
||||
|
||||
if self.mode == "CHAT":
|
||||
self.stdscr.addstr(box_top + box_height - 1, box_left + 2, f"> {self.user_input}_")
|
||||
else:
|
||||
self.stdscr.addstr(box_top + box_height - 1, box_left + 2, " [Press 'c' for chat, 'a' for admin] ")
|
||||
|
||||
def draw_admin_panel(self, top, left, height, width):
|
||||
box_top = top
|
||||
box_left = left
|
||||
box_height = height
|
||||
box_width = width
|
||||
|
||||
self.stdscr.attron(curses.color_pair(5))
|
||||
for y in range(box_top, box_top + box_height):
|
||||
self.stdscr.addstr(y, box_left, "│")
|
||||
self.stdscr.addstr(y, box_left + box_width - 1, "│")
|
||||
self.stdscr.addstr(box_top, box_left, "┌")
|
||||
self.stdscr.addstr(box_top, box_left + box_width - 1, "┐")
|
||||
self.stdscr.addstr(box_top + box_height - 1, box_left, "└")
|
||||
self.stdscr.addstr(box_top + box_height - 1, box_left + box_width - 1, "┘")
|
||||
self.stdscr.addstr(box_top, box_left + 1, "─" * (box_width - 2))
|
||||
self.stdscr.addstr(box_top + box_height - 1, box_left + 1, "─" * (box_width - 2))
|
||||
self.stdscr.attroff(curses.color_pair(5))
|
||||
|
||||
self.stdscr.attron(curses.color_pair(4) | curses.A_BOLD)
|
||||
self.stdscr.addstr(box_top, box_left + 2, " ADMIN ")
|
||||
self.stdscr.attroff(curses.color_pair(4) | curses.A_BOLD)
|
||||
|
||||
help_lines = [
|
||||
" Commands:",
|
||||
" s = status",
|
||||
" e = emotion reset",
|
||||
" m <val> = mood set",
|
||||
" t <val> = trust set",
|
||||
" r = bond reset",
|
||||
" d = debug",
|
||||
]
|
||||
|
||||
for i, line in enumerate(help_lines):
|
||||
self.stdscr.addstr(box_top + 2 + i, box_left + 2, line[:box_width - 4])
|
||||
|
||||
def handle_input(self):
|
||||
try:
|
||||
key = self.stdscr.getch()
|
||||
if key == -1:
|
||||
return
|
||||
|
||||
if key == ord('q'):
|
||||
self.running = False
|
||||
elif key == ord('c'):
|
||||
self.mode = "CHAT"
|
||||
elif key == ord('a'):
|
||||
self.mode = "ADMIN"
|
||||
elif key == ord('s') and self.mode == "ADMIN":
|
||||
os.system("cd ~/natiris/admin && python3 AdminInterface.py status")
|
||||
self.update_state()
|
||||
elif key == ord('e') and self.mode == "ADMIN":
|
||||
os.system("cd ~/natiris/admin && python3 AdminInterface.py emotion reset")
|
||||
self.update_state()
|
||||
elif key == ord('r') and self.mode == "ADMIN":
|
||||
os.system("cd ~/natiris/admin && python3 AdminInterface.py bond reset")
|
||||
self.update_state()
|
||||
elif key == ord('t') and self.mode == "ADMIN":
|
||||
os.system("cd ~/natiris/admin && python3 AdminInterface.py trust set 7")
|
||||
self.update_state()
|
||||
elif key == ord('m') and self.mode == "ADMIN":
|
||||
os.system("cd ~/natiris/admin && python3 AdminInterface.py mood set 7.5")
|
||||
self.update_state()
|
||||
elif key == curses.KEY_BACKSPACE or key == 127 or key == 8:
|
||||
if self.mode == "CHAT" and self.user_input:
|
||||
self.user_input = self.user_input[:-1]
|
||||
elif key >= 32 and key < 127:
|
||||
if self.mode == "CHAT":
|
||||
self.user_input += chr(key)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
while self.running:
|
||||
try:
|
||||
# Update every 2s
|
||||
if time.time() - self.last_update > 2:
|
||||
self.update_state()
|
||||
|
||||
self.stdscr.clear()
|
||||
h, w = self.stdscr.getmaxyx()
|
||||
|
||||
# Header
|
||||
self.draw_header()
|
||||
|
||||
# Panels
|
||||
panel_height = (h - 4) // 2
|
||||
panel_width = w // 2 - 4
|
||||
|
||||
self.draw_state_panel(4, 2, panel_height, w // 2)
|
||||
if self.mode == "ADMIN":
|
||||
self.draw_admin_panel(4 + panel_height, 2, panel_height + 2, w // 2)
|
||||
self.draw_chat_panel(4 + panel_height, w // 2 + 2, panel_height + 2, w // 2 - 2)
|
||||
else:
|
||||
self.draw_chat_panel(4, w // 2 + 2, h - 6, w // 2 - 4)
|
||||
|
||||
self.handle_input()
|
||||
self.stdscr.refresh()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
self.running = False
|
||||
|
||||
def main():
|
||||
try:
|
||||
curses.wrapper(NatirisTUI)
|
||||
except Exception as e:
|
||||
print(f"TUI error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user