305 lines
16 KiB
Python
305 lines
16 KiB
Python
import os
|
|
from typing import Optional, List, Dict
|
|
from nicegui import ui, binding
|
|
from components import AsyncElement
|
|
from llm_connector import LLMBackend
|
|
from living_agents import Character
|
|
from .character import CharacterCard
|
|
from .scene_control import SceneControl
|
|
from .conversation_history import ConversationHistory
|
|
from .create_character import CharacterTemplateImportDialog, CharacterCreationDialog
|
|
from living_agents import RoleplaySystem, Character, LLMAgent
|
|
from utils import load_characters
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MainPage(AsyncElement):
|
|
backend: LLMBackend
|
|
system: RoleplaySystem
|
|
selected_character: Optional[Character] = None
|
|
chat_container = None
|
|
scene_info_container = None
|
|
characters: List[Character]
|
|
initial_memories: Dict[Character, List[str]]
|
|
memory_filter: ui.select
|
|
|
|
is_generating = binding.BindableProperty()
|
|
|
|
async def setup_characters(self):
|
|
self.is_generating = True
|
|
await self.system.setup_characters(self.characters)
|
|
|
|
for char, memories in self.initial_memories.items():
|
|
for memory in memories:
|
|
await self.system.agents[char].perceive(memory)
|
|
self.is_generating = False
|
|
|
|
async def build(self): # pylint: disable=W0221
|
|
|
|
loaded_characters = load_characters()
|
|
|
|
self.characters = [char for char in loaded_characters]
|
|
self.initial_memories = {char: memories for char, memories in loaded_characters.items()}
|
|
self.system = RoleplaySystem()
|
|
|
|
# Header
|
|
with self.element:
|
|
with ui.row().classes(
|
|
'bg-gradient-to-r from-purple-600 to-indigo-600 text-white q-pa-sm items-center w-full'):
|
|
ui.label('🎭 Living Agents').classes('text-2xl font-bold')
|
|
ui.label('Multi-Agent Roleplay with Stanford Memory Architecture').classes('text-sm opacity-90')
|
|
|
|
# Top Row: Characters
|
|
with ui.row().classes('w-full gap-2 items-center'):
|
|
for char in self.characters:
|
|
(await CharacterCard.create(ui.card, char)) \
|
|
.classes('p-2 cursor-pointer hover:bg-gray-50 gap-1') \
|
|
.on('click', lambda c=char: self._select_character(c))
|
|
ui.space()
|
|
ui.button(icon='add', on_click=self._open_character_creation).props('fab')
|
|
|
|
# Selected Character
|
|
with ui.row().classes('w-full flex-1'):
|
|
# Character card
|
|
with ui.card().classes('h-full max-w-96'):
|
|
await self._character_view() # type: ignore
|
|
with ui.row().classes('w-full items-center mb-2'):
|
|
ui.label('🧠 Memories').classes('text-lg font-bold')
|
|
ui.space()
|
|
# Memory type filter
|
|
self.memory_filter = ui.select(['all', 'observation', 'reflection', 'plan'], value='all',
|
|
on_change=self._memory_view.refresh).props(
|
|
'dense outlined').classes('w-24')
|
|
await self._memory_view() # type: ignore
|
|
# Conversation History (takes full height)
|
|
with ui.card().classes('flex-1 h-full'):
|
|
ui.label('🗨️ Conversation History').classes('text-lg font-bold')
|
|
|
|
# Scrollable chat container - takes remaining height
|
|
with ui.scroll_area().classes('w-full').style('height: calc(100% - 3rem)'):
|
|
self.chat_container = ui.column().classes('w-full gap-2')
|
|
with self.chat_container:
|
|
# Welcome message
|
|
with ui.chat_message(name='System', sent=False).classes('w-full'):
|
|
ui.label(
|
|
'Welcome to the Living Agents roleplay system! ''Select a character and start interacting.').classes(
|
|
'text-sm')
|
|
|
|
# ui.timer(0.5, self.setup_characters, once=True)
|
|
|
|
# Main container as a column
|
|
with ui.row().classes('w-full'):
|
|
with ui.column().classes('flex-1 bg-red-200 h-full'):
|
|
# Interactions Card
|
|
with ui.card().classes('w-full'):
|
|
ui.label('💬 Interactions').classes('text-lg font-bold mb-1')
|
|
|
|
# Character-to-User interaction
|
|
with ui.column().classes('w-full gap-1'):
|
|
ui.label('Talk to Character').classes('font-semibold text-sm')
|
|
with ui.row().classes('w-full gap-1'):
|
|
self.user_input = ui.input(
|
|
placeholder='Say something to the selected character...').classes('flex-1').props(
|
|
'dense')
|
|
ui.button('Send', on_click=self._send_to_character).props(
|
|
'icon=send color=primary').bind_enabled_from(self, 'is_generating',
|
|
backward=lambda v: not v)
|
|
|
|
ui.separator()
|
|
|
|
# Character-to-Character interaction
|
|
with ui.column().classes('w-full gap-2 mt-2'):
|
|
ui.label('Character Interaction').classes('font-semibold text-sm')
|
|
with ui.row().classes('w-full gap-2'):
|
|
self.char1_select = ui.select(['Alice', 'Bob', 'Emma'], label='Character 1',
|
|
value='Alice').classes('flex-1').props('dense')
|
|
self.char2_select = ui.select(['Alice', 'Bob', 'Emma'], label='Character 2',
|
|
value='Bob').classes('flex-1').props('dense')
|
|
self.interaction_context = ui.input(placeholder='Context for interaction...').classes(
|
|
'w-full').props('dense')
|
|
ui.button('Make them interact', on_click=self._character_interaction).props(
|
|
'icon=forum color=secondary').classes('w-full').bind_enabled_from(self, 'is_generating',
|
|
backward=lambda
|
|
v: not v)
|
|
# Bottom Row: Scene Control, Interaction
|
|
with ui.card().classes('h-full'):
|
|
ui.label('📍 Scene Control').classes('text-lg font-bold mb-1')
|
|
|
|
with ui.row().classes('w-full'):
|
|
with ui.column().classes('gap-2'):
|
|
with ui.row().classes('justify-between'):
|
|
ui.label('Location:').classes('text-sm font-semibold')
|
|
ui.label('Cozy Coffee Shop').classes('text-sm')
|
|
with ui.row().classes('justify-between'):
|
|
ui.label('Time:').classes('text-sm font-semibold')
|
|
ui.label('2:30 PM').classes('text-sm')
|
|
with ui.row().classes('justify-between'):
|
|
ui.label('Atmosphere:').classes('text-sm font-semibold')
|
|
ui.label('Quiet and peaceful').classes('text-sm')
|
|
ui.space()
|
|
with ui.column().classes('gap-2'):
|
|
ui.button('⏰ +1 Hour').classes('flex-1').bind_enabled_from(self, 'is_generating',
|
|
backward=lambda v: not v)
|
|
ui.button('📅 +1 Day').classes('flex-1').bind_enabled_from(self, 'is_generating',
|
|
backward=lambda v: not v)
|
|
|
|
# move outside of page generation
|
|
# ui.timer(0.5, self.setup_characters, once=True)
|
|
|
|
async def _open_character_creation(self):
|
|
# with ui.dialog() as dialog, ui.card():
|
|
# (await CharacterCreationDialog.create(ui.column)).classes('w-full')
|
|
dialog = (await CharacterTemplateImportDialog.create(ui.dialog)).classes('w-full')
|
|
result = await dialog
|
|
if result:
|
|
dialog = (await CharacterCreationDialog.create(ui.dialog, template=result)).classes('w-full').props(
|
|
'persistent')
|
|
result = await dialog
|
|
print(result)
|
|
|
|
async def _select_character(self, character: Character):
|
|
"""Select a character and update UI"""
|
|
self.selected_character = character
|
|
self._character_view.refresh() # type: ignore
|
|
|
|
self._memory_view.refresh()
|
|
|
|
@ui.refreshable
|
|
async def _character_view(self):
|
|
with ui.column().classes('w-full gap-0'):
|
|
if self.selected_character is None:
|
|
ui.label('Select a character to see their summary and Memories').classes(
|
|
'text-sm text-gray-500 italic')
|
|
return
|
|
ui.label(f'{self.selected_character.name}').classes('font-bold text-lg')
|
|
ui.separator()
|
|
with ui.column().classes('gap-0'):
|
|
ui.label(f'Age: {self.selected_character.age}').classes('text-sm')
|
|
ui.label(f'Occupation: {self.selected_character.occupation}').classes('text-sm')
|
|
ui.label(f'Personality: {self.selected_character.personality}').classes('text-sm')
|
|
ui.separator()
|
|
ui.label('Goals').classes('font-bold text-lg')
|
|
with ui.list().props('dense separator'):
|
|
for goal in self.selected_character.goals:
|
|
ui.item(goal)
|
|
|
|
@ui.refreshable
|
|
async def _memory_view(self):
|
|
"""Update the memory stream viewer"""
|
|
if self.selected_character:
|
|
with ui.scroll_area().classes('w-full border rounded p-0 flex-1'):
|
|
with ui.column().classes('gap-2'):
|
|
memories = self.system.get_character_memories(self.selected_character,
|
|
self.memory_filter.value if self.memory_filter.value else 'all')
|
|
for memory in memories:
|
|
with ui.card().classes('w-full p-1'):
|
|
with ui.row().classes('gap-2'):
|
|
with ui.column().classes('flex-1'):
|
|
ui.label(memory.description).classes('text-sm')
|
|
with ui.row().classes('gap-2 mt-1 items-center'):
|
|
if memory.memory_type == 'observation':
|
|
ui.icon('visibility', size='xs').classes('text-blue-500 mt-1')
|
|
elif memory.memory_type == 'reflection':
|
|
ui.icon('psychology', size='xs').classes('text-purple-500 mt-1')
|
|
else:
|
|
ui.icon('event', size='xs').classes('text-green-500 mt-1')
|
|
ui.badge(f'⭐ {memory.importance_score}', color='orange').classes('text-xs')
|
|
ui.label(memory.creation_time.strftime('%Y-%m-%d %H:%M:%S')).classes(
|
|
'text-xs text-gray-500')
|
|
|
|
async def _send_to_character(self):
|
|
"""Send message to selected character"""
|
|
if not self.selected_character:
|
|
ui.notify('Please select a character first', type='warning')
|
|
return
|
|
|
|
if not self.user_input.value:
|
|
return
|
|
|
|
message = self.user_input.value
|
|
self.user_input.value = ''
|
|
|
|
# Add user message to chat
|
|
with self.chat_container:
|
|
with ui.chat_message(name='You', sent=True).classes('w-full'):
|
|
ui.label(message).classes('text-sm')
|
|
|
|
# Mock response (will be replaced with actual agent response)
|
|
with self.chat_container:
|
|
with ui.chat_message(name=self.selected_character.name, sent=False).classes('w-full'):
|
|
spinner = ui.spinner('dots')
|
|
|
|
# Simulate thinking
|
|
await ui.run_javascript('window.scrollTo(0, document.body.scrollHeight)')
|
|
ui.notify(f'🧠 {self.selected_character} is thinking...', type='info')
|
|
|
|
# Mock response after delay
|
|
await ui.timer(1.5, lambda: self._add_character_response(spinner))
|
|
|
|
def _add_character_response(self, spinner):
|
|
"""Add character response to chat"""
|
|
spinner.delete()
|
|
parent = spinner.parent_slot.parent
|
|
with parent:
|
|
if self.selected_character == 'Alice':
|
|
ui.label(
|
|
"*nervously adjusts glasses* Oh, um, hello there. I was just working on my thesis chapter about Victorian gothic literature. The coffee here helps me concentrate.").classes(
|
|
'text-sm')
|
|
elif self.selected_character == 'Bob':
|
|
ui.label(
|
|
"Hey! Yeah, I'm actually debugging some code right now. This new feature is giving me some trouble, but I think I'm close to solving it. How's your day going?").classes(
|
|
'text-sm')
|
|
else:
|
|
ui.label(
|
|
"Hi! Welcome to our little coffee shop! I just finished a new sketch during my break - been trying to capture the afternoon light through the windows. Can I get you anything?").classes(
|
|
'text-sm')
|
|
|
|
async def _character_interaction(self):
|
|
"""Make two characters interact"""
|
|
char1 = self.char1_select.value
|
|
char2 = self.char2_select.value
|
|
context = self.interaction_context.value or "meeting at the coffee shop"
|
|
|
|
if char1 == char2:
|
|
ui.notify("Characters can't interact with themselves", type='warning')
|
|
return
|
|
|
|
# Add interaction to chat
|
|
with self.chat_container:
|
|
with ui.chat_message(name='Scene', sent=False).classes('w-full'):
|
|
ui.label(f'🎬 {char1} and {char2} interact: {context}').classes('text-sm italic text-gray-600')
|
|
|
|
# Mock interaction
|
|
with ui.chat_message(name=char1, sent=False).classes('w-full'):
|
|
ui.label("Oh, hi there! I didn't expect to see you here today.").classes('text-sm')
|
|
|
|
with ui.chat_message(name=char2, sent=False).classes('w-full'):
|
|
ui.label("Hey! Yeah, this is my usual spot. How have you been?").classes('text-sm')
|
|
|
|
ui.notify(f'💬 {char1} and {char2} had an interaction', type='positive')
|
|
|
|
def _advance_time(self, hours: int):
|
|
"""Advance scene time"""
|
|
ui.notify(f'⏰ Advanced time by {hours} hour(s)', type='info')
|
|
|
|
# Update scene info
|
|
self.scene_info_container.clear()
|
|
with self.scene_info_container:
|
|
self._create_scene_info()
|
|
|
|
# Add time advancement to chat
|
|
with self.chat_container:
|
|
with ui.chat_message(name='System', sent=False).classes('w-full'):
|
|
ui.label(f'⏰ Time advanced by {hours} hour(s). Characters update their plans...').classes(
|
|
'text-sm italic text-gray-600')
|
|
|
|
def _clear_chat(self):
|
|
"""Clear chat history"""
|
|
self.chat_container.clear()
|
|
with self.chat_container:
|
|
with ui.chat_message(name='System', sent=False).classes('w-full'):
|
|
ui.label('Chat history cleared. Ready for new interactions!').classes('text-sm')
|
|
ui.notify('Chat cleared', type='info')
|