This commit is contained in:
2025-09-01 06:43:11 +02:00
parent bde3fc0df9
commit 45eb2b8bc5
38 changed files with 3424 additions and 915 deletions

View File

@@ -1,129 +1,111 @@
import os
from typing import Optional
from nicegui import ui
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
scene_manager = None # Will hold SceneManager instance
selected_character: Optional[str] = None
memory_viewer = None
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
backend: LLMBackend = {'base_url': os.environ['BACKEND_BASE_URL'],
'api_token': os.environ['BACKEND_API_TOKEN'],
'model': os.environ['BACKEND_MODEL']}
loaded_characters = load_characters()
self.backend = backend
# Initialize mock scene manager (will be replaced with real one)
await self._initialize_scene()
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 ui.header().classes('bg-gradient-to-r from-purple-600 to-indigo-600 text-white'):
ui.label('🎭 Living Agents').classes('text-2xl font-bold')
ui.label('Multi-Agent Roleplay with Stanford Memory Architecture').classes(
'text-sm opacity-90')
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')
self.classes('w-full')
with self:
# 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')
# Main container with three columns
with ui.row().classes('w-full p-4 gap-4'):
# 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')
# Left Panel - Scene Control & Characters
with ui.column().classes('w-1/4 gap-4'):
# 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')
# Scene Information Card
# 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('📍 Scene Control').classes('text-lg font-bold mb-2')
self.scene_info_container = ui.column().classes('w-full gap-2')
with self.scene_info_container:
self._create_scene_info()
ui.separator()
# Time controls
with ui.row().classes('w-full gap-2 mt-2'):
ui.button('⏰ +1 Hour', on_click=lambda: self._advance_time(1)).classes('flex-1')
ui.button('📅 +1 Day', on_click=lambda: self._advance_time(24)).classes('flex-1')
# Characters List
with ui.card().classes('w-full'):
ui.label('👥 Characters').classes('text-lg font-bold mb-2')
# Character cards
with ui.column().classes('w-full gap-2'):
# Alice
with ui.card().classes('w-full p-3 cursor-pointer hover:bg-gray-50').on(
'click', lambda: self._select_character('Alice')):
with ui.row().classes('items-center gap-2'):
ui.icon('person', size='sm').classes('text-purple-500')
with ui.column().classes('flex-1'):
ui.label('Alice').classes('font-semibold')
ui.label('Literature Student, 23').classes(
'text-xs text-gray-500')
with ui.row().classes('gap-1 mt-1'):
ui.badge('📚 10 memories', color='purple').classes('text-xs')
ui.badge('💭 0 reflections', color='indigo').classes('text-xs')
# Bob
with ui.card().classes('w-full p-3 cursor-pointer hover:bg-gray-50').on(
'click', lambda: self._select_character('Bob')):
with ui.row().classes('items-center gap-2'):
ui.icon('person', size='sm').classes('text-blue-500')
with ui.column().classes('flex-1'):
ui.label('Bob').classes('font-semibold')
ui.label('Software Developer, 28').classes(
'text-xs text-gray-500')
with ui.row().classes('gap-1 mt-1'):
ui.badge('📚 8 memories', color='purple').classes('text-xs')
ui.badge('💭 0 reflections', color='indigo').classes('text-xs')
# Emma
with ui.card().classes('w-full p-3 cursor-pointer hover:bg-gray-50').on(
'click', lambda: self._select_character('Emma')):
with ui.row().classes('items-center gap-2'):
ui.icon('person', size='sm').classes('text-pink-500')
with ui.column().classes('flex-1'):
ui.label('Emma').classes('font-semibold')
ui.label('Barista & Artist, 25').classes(
'text-xs text-gray-500')
with ui.row().classes('gap-1 mt-1'):
ui.badge('📚 7 memories', color='purple').classes('text-xs')
ui.badge('💭 0 reflections', color='indigo').classes('text-xs')
# Character Summary - moved here to be under Characters
with ui.card().classes('w-full'):
ui.label('📝 Character Summary').classes('text-lg font-bold mb-2')
self.character_summary = ui.column().classes('w-full')
with self.character_summary:
ui.label('Select a character to see their summary').classes(
'text-sm text-gray-500 italic')
# Middle Panel - Interaction & Chat
with ui.column().classes('w-1/2 gap-4'):
# Interaction Controls
with ui.card().classes('w-full'):
ui.label('💬 Interactions').classes('text-lg font-bold mb-2')
ui.label('💬 Interactions').classes('text-lg font-bold mb-1')
# Character-to-User interaction
with ui.column().classes('w-full gap-2'):
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-2'):
with ui.row().classes('w-full gap-1'):
self.user_input = ui.input(
placeholder='Say something to the selected character...'
).classes('flex-1')
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')
'icon=send color=primary').bind_enabled_from(self, 'is_generating',
backward=lambda v: not v)
ui.separator()
@@ -131,153 +113,101 @@ class MainPage(AsyncElement):
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')
self.char2_select = ui.select(
['Alice', 'Bob', 'Emma'],
label='Character 2',
value='Bob'
).classes('flex-1')
self.interaction_context = ui.input(
placeholder='Context for interaction...'
).classes('w-full')
ui.button(
'Make them interact',
on_click=self._character_interaction
).props('icon=forum color=secondary').classes('w-full')
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')
# Chat History
with ui.card().classes('w-full flex-1'):
with ui.row().classes('w-full items-center mb-2'):
ui.label('🗨️ Conversation History').classes('text-lg font-bold')
ui.space()
ui.button(icon='delete', on_click=self._clear_chat).props('flat round size=sm')
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)
# Scrollable chat container
with ui.scroll_area().classes('w-full h-96 border rounded p-2'):
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')
# move outside of page generation
# ui.timer(0.5, self.setup_characters, once=True)
# Right Panel - Memory Stream
with ui.column().classes('w-1/4 gap-4'):
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)
# Memory Stream Viewer
with ui.card().classes('w-full flex-1'):
with ui.row().classes('w-full items-center mb-2'):
ui.label('🧠 Memory Stream').classes('text-lg font-bold')
ui.space()
# Memory type filter
self.memory_filter = ui.select(
['all', 'observation', 'reflection', 'plan'],
value='all',
on_change=self._update_memory_view
).props('dense outlined').classes('w-24')
# Scrollable memory list
with ui.scroll_area().classes('w-full h-96 border rounded p-2'):
self.memory_viewer = ui.column().classes('w-full gap-2')
with self.memory_viewer:
ui.label('Select a character to view memories').classes('text-sm text-gray-500 italic')
# Footer with stats
with ui.footer().classes('bg-gray-100 text-gray-600 text-sm'):
with ui.row().classes('w-full justify-center items-center gap-4'):
ui.label('🎯 Stanford Memory Architecture')
ui.label('|')
self.stats_label = ui.label('Total Memories: 0 | Reflections: 0')
ui.label('|')
ui.label('⚡ Powered by Custom LLM Connector')
def _create_scene_info(self):
"""Create scene information display"""
with ui.row().classes('w-full justify-between'):
ui.label('Location:').classes('text-sm font-semibold')
ui.label('Cozy Coffee Shop').classes('text-sm')
with ui.row().classes('w-full justify-between'):
ui.label('Time:').classes('text-sm font-semibold')
ui.label('2:30 PM').classes('text-sm')
with ui.row().classes('w-full justify-between'):
ui.label('Atmosphere:').classes('text-sm font-semibold')
ui.label('Quiet and peaceful').classes('text-sm')
async def _initialize_scene(self):
"""Initialize the scene with mock data (will be replaced with real SceneManager)"""
# This will be replaced with actual SceneManager initialization
ui.notify('🎬 Scene initialized with 3 characters', type='positive')
async def _select_character(self, character_name: str):
async def _select_character(self, character: Character):
"""Select a character and update UI"""
self.selected_character = character_name
ui.notify(f'Selected: {character_name}', type='info')
self.selected_character = character
self._character_view.refresh() # type: ignore
# Update character summary
self.character_summary.clear()
with self.character_summary:
ui.label(f'{character_name}').classes('font-bold text-lg')
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)
if character_name == 'Alice':
ui.label('Age: 23').classes('text-sm')
ui.label('Occupation: Graduate student').classes('text-sm')
ui.label('Personality: Introverted, observant, loves mystery novels').classes('text-sm mt-2')
ui.label('Current Goal: Finish thesis chapter').classes('text-sm text-blue-600 mt-2')
elif character_name == 'Bob':
ui.label('Age: 28').classes('text-sm')
ui.label('Occupation: Senior Developer').classes('text-sm')
ui.label('Personality: Confident, helpful, technical').classes('text-sm mt-2')
ui.label('Current Goal: Launch new feature').classes('text-sm text-blue-600 mt-2')
elif character_name == 'Emma':
ui.label('Age: 25').classes('text-sm')
ui.label('Occupation: Barista & Art Student').classes('text-sm')
ui.label('Personality: Energetic, social, creative').classes('text-sm mt-2')
ui.label('Current Goal: Organize art show').classes('text-sm text-blue-600 mt-2')
# Update memory viewer
await self._update_memory_view()
async def _update_memory_view(self):
@ui.refreshable
async def _memory_view(self):
"""Update the memory stream viewer"""
if not self.selected_character:
return
self.memory_viewer.clear()
with self.memory_viewer:
# Mock memories for demonstration
memories = [
('observation', 'Arrived at the coffee shop', 8, '10:00 AM'),
('observation', 'Ordered my usual latte', 3, '10:05 AM'),
('observation', 'Saw a familiar face by the window', 6, '10:30 AM'),
('reflection', 'I seem to come here when I need to focus', 7, '11:00 AM'),
('plan', 'Work on thesis for 2 hours', 5, '11:30 AM'),
]
filter_type = self.memory_filter.value
for mem_type, description, importance, time in memories:
if filter_type == 'all' or filter_type == mem_type:
with ui.card().classes('w-full p-2'):
with ui.row().classes('w-full items-start gap-2'):
# Memory type icon
if mem_type == 'observation':
ui.icon('visibility', size='xs').classes('text-blue-500 mt-1')
elif mem_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')
with ui.column().classes('flex-1'):
ui.label(description).classes('text-sm')
with ui.row().classes('gap-2 mt-1'):
ui.badge(f'{importance}', color='orange').classes('text-xs')
ui.label(time).classes('text-xs text-gray-500')
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"""
@@ -298,7 +228,7 @@ class MainPage(AsyncElement):
# Mock response (will be replaced with actual agent response)
with self.chat_container:
with ui.chat_message(name=self.selected_character, sent=False).classes('w-full'):
with ui.chat_message(name=self.selected_character.name, sent=False).classes('w-full'):
spinner = ui.spinner('dots')
# Simulate thinking
@@ -314,11 +244,17 @@ class MainPage(AsyncElement):
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')
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')
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')
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"""
@@ -356,7 +292,8 @@ class MainPage(AsyncElement):
# 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')
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"""