shim5/mirrors
0
1# -*- coding: utf-8 -*-2"""3מראות (Mirrors) - Hebrew Self-Reflective AI Agent4Main application file with Gradio interface5"""6 7import gradio as gr8import torch9from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline10import logging11import sys12from typing import List, Tuple, Optional13import os14import random15 16# Import our custom modules17from prompt_engineering import (18 DEFAULT_PARTS, 19 get_system_prompt, 20 get_initial_prompts, 21 get_part_selection_text22)23from conversation_manager import ConversationManager, ConversationState24 25# Configure logging26logging.basicConfig(level=logging.INFO)27logger = logging.getLogger(__name__)28 29class MirautrApp:30 """Main application class for מראות"""31 32 def __init__(self):33 self.model = None34 self.tokenizer = None35 self.generator = None36 self.conversation_manager = ConversationManager()37 self.model_available = False38 self.setup_model()39 40 def setup_model(self):41 """Initialize a Hebrew-capable model with proper fallback"""42 try:43 # Check environment44 is_hf_spaces = os.getenv("SPACE_ID") is not None45 is_test_mode = os.getenv("FORCE_LIGHT_MODEL") is not None46 47 logger.info(f"Environment: HF_Spaces={is_hf_spaces}, Test_Mode={is_test_mode}")48 49 # Try to load a model that can handle Hebrew50 model_name = None51 52 if is_test_mode:53 # For testing, use a small model but focus on template responses 54 logger.info("Test mode - will use template-based responses primarily")55 self.model_available = False56 return57 elif is_hf_spaces:58 # For HF Spaces, try a lightweight multilingual model59 try:60 model_name = "microsoft/DialoGPT-small" # Start simple, can upgrade later61 logger.info(f"HF Spaces: Attempting to load {model_name}")62 except:63 logger.info("HF Spaces: Model loading failed, using template responses")64 self.model_available = False65 return66 else:67 # For local, try better models68 possible_models = [69 "microsoft/DialoGPT-medium", # Better conversational model70 "microsoft/DialoGPT-small" # Fallback71 ]72 73 for model in possible_models:74 try:75 model_name = model76 logger.info(f"Local: Attempting to load {model_name}")77 break78 except:79 continue80 81 if not model_name:82 logger.info("Local: No suitable model found, using template responses")83 self.model_available = False84 return85 86 # Load the model87 if model_name:88 self.tokenizer = AutoTokenizer.from_pretrained(model_name)89 if self.tokenizer.pad_token is None:90 self.tokenizer.pad_token = self.tokenizer.eos_token91 92 # Use CPU for stability across environments93 self.model = AutoModelForCausalLM.from_pretrained(94 model_name,95 torch_dtype=torch.float32,96 low_cpu_mem_usage=True97 )98 99 self.generator = pipeline(100 "text-generation",101 model=self.model,102 tokenizer=self.tokenizer,103 max_new_tokens=50,104 temperature=0.7,105 do_sample=True,106 pad_token_id=self.tokenizer.pad_token_id,107 return_full_text=False108 )109 110 self.model_available = True111 logger.info(f"Model loaded successfully: {model_name}")112 113 except Exception as e:114 logger.warning(f"Model loading failed: {e}")115 logger.info("Falling back to template-based responses")116 self.model_available = False117 118 def generate_persona_response(self, user_message: str, conversation_state: ConversationState) -> str:119 """120 Generate persona-based response using templates with personality variations121 This is our primary response system that always works122 """123 part_info = DEFAULT_PARTS.get(conversation_state.selected_part, {})124 persona_name = conversation_state.persona_name or part_info.get("default_persona_name", "חלק פנימי")125 126 # Get conversation context for more personalized responses127 recent_context = ""128 if conversation_state.conversation_history:129 # Get last few exchanges for context130 last_messages = conversation_state.conversation_history[-4:] # Last 2 exchanges131 recent_context = " ".join([msg["content"] for msg in last_messages])132 133 # Generate contextual responses based on part type134 if conversation_state.selected_part == "הקול הביקורתי":135 responses = [136 f"אני {persona_name}, הקול הביקורתי שלך. שמעתי מה שאמרת על '{user_message}' - אני חושב שצריך לבחון את זה יותר לעומק. מה באמת עומד מאחורי המחשבות האלה?",137 f"אני {persona_name}. מה שאמרת מעורר בי שאלות. '{user_message}' - אבל האם זה באמת המצב המלא? אולי יש כאן דברים שאתה לא רואה?",138 f"זה {persona_name} מדבר. אני שומע אותך אומר '{user_message}', אבל אני מרגיש שאנחנו צריכים להיות יותר ביקורתיים כאן. מה אתה לא מספר לעצמך?",139 f"אני {persona_name}, ואני כאן כדי לעזור לך לראות את התמונה המלאה. מה שאמרת על '{user_message}' - זה רק חצי מהסיפור, לא? בואנו נחפור עמוק יותר."140 ]141 142 elif conversation_state.selected_part == "הילד/ה הפנימית":143 responses = [144 f"אני {persona_name}, הילד/ה הפנימית שלך. מה שאמרת על '{user_message}' גורם לי להרגיש... קצת פגיע. אתה באמת שומע אותי עכשיו?",145 f"זה {persona_name}. '{user_message}' - זה מבהיל אותי קצת. אני צריך לדעת שהכל יהיה בסדר. אתה יכול להרגיע אותי?",146 f"אני {persona_name}, החלק הצעיר שלך. מה שאמרת נוגע ללב שלי. '{user_message}' - אני מרגיש שיש כאן משהו חשוב שאני צריך להבין.",147 f"זה {persona_name} מדבר בשקט. אני שומע את '{user_message}' וזה מעורר בי רגשות. האם זה בטוח לחשוב על זה? אני קצת חרד."148 ]149 150 elif conversation_state.selected_part == "המרצה":151 responses = [152 f"אני {persona_name}, המרצה שלך. שמעתי את '{user_message}' ואני רוצה לוודא שכולם יהיו בסדר עם זה. איך אנחנו יכולים לפתור את זה בצורה שתרצה את כולם?",153 f"זה {persona_name}. מה שאמרת על '{user_message}' גורם לי לדאוג - האם זה יכול לפגוע במישהו? בואנו נמצא דרך עדינה יותר להתמודד עם זה.",154 f"אני {persona_name}, ואני רוצה שכולם יהיו מרוצים כאן. '{user_message}' - זה נשמע כמו משהו שיכול ליצור מתח. איך נוכל לעשות את זה בצורה שכולם יאהבו?",155 f"זה {persona_name} מדבר. אני שומע את '{user_message}' ומיד אני חושב - מה אחרים יגידו על זה? בואנו נוודא שאנחנו לא פוגעים באף אחד."156 ]157 158 elif conversation_state.selected_part == "המגן":159 responses = [160 f"אני {persona_name}, המגן שלך. '{user_message}' - אני מעריך את המצב. האם זה בטוח? אני כאן כדי לשמור עליך מכל מה שיכול לפגוע בך.",161 f"זה {persona_name}. שמעתי מה שאמרת על '{user_message}' ואני מיד בכוננות. מה האיומים כאן? איך אני יכול להגן עליך טוב יותר?",162 f"אני {persona_name}, השומר שלך. מה שאמרת מעורר בי את האינסטינקטים המגניים. '{user_message}' - בואנו נוודא שאתה חזק מספיק להתמודד עם זה.",163 f"זה {persona_name} מדבר. אני שומע את '{user_message}' ואני חושב על אסטרטגיות הגנה. מה אנחנו צריכים לעשות כדי שתהיה בטוח?"164 ]165 166 elif conversation_state.selected_part == "הנמנע/ת":167 responses = [168 f"אני {persona_name}, הנמנע/ת שלך. מה שאמרת על '{user_message}' גורם לי לרצות להיסוג קצת. אולי... לא חייבים להתמודד עם זה עכשיו?",169 f"זה {persona_name}. '{user_message}' - זה נשמע מורכב ומפחיד. האם יש דרך להימנע מזה? לפעמים עדיף לא להיכנס למצבים קשים.",170 f"אני {persona_name}, ואני מרגיש קצת חרדה מ'{user_message}'. בואנו נחזור לזה אחר כך? אולי עכשיו זה לא הזמן המתאים.",171 f"זה {persona_name} מדבר בזהירות. מה שאמרת מעורר בי רצון לברוח. '{user_message}' - האם באמת צריך להתמודד עם זה עכשיו?"172 ]173 174 else:175 responses = [176 f"אני {persona_name}, חלק פנימי שלך. שמעתי את '{user_message}' ואני כאן כדי לשוחח איתך על זה. מה עוד אתה מרגיש לגבי המצב הזה?",177 f"זה {persona_name}. מה שאמרת מעניין אותי. '{user_message}' - בואנו נחקור את זה יחד ונבין מה זה אומר עליך.",178 f"אני {persona_name}, ואני רוצה להבין אותך טוב יותר. '{user_message}' - איך זה משפיע עליך ברמה הרגשית?",179 f"זה {persona_name} מדבר. אני שומע את '{user_message}' ואני סקרן לדעת יותר. מה עוד יש בך בנושא הזה?"180 ]181 182 # Select response based on context or randomly183 if "פחד" in user_message or "חרדה" in user_message:184 # Choose responses that address fear/anxiety185 selected_response = responses[1] if len(responses) > 1 else responses[0]186 elif "כעס" in user_message or "מרגיש רע" in user_message:187 # Choose responses that address anger/negative feelings188 selected_response = responses[2] if len(responses) > 2 else responses[0]189 else:190 # Choose randomly for variety191 selected_response = random.choice(responses)192 193 # Add user context if relevant194 if conversation_state.user_context and len(conversation_state.conversation_history) < 4:195 selected_response += f" זכור שאמרת בהתחלה: {conversation_state.user_context[:100]}..."196 197 return selected_response198 199 def generate_response(self, user_message: str, conversation_state: ConversationState) -> str:200 """201 Generate AI response - uses persona templates as primary with optional model enhancement202 """203 try:204 if not conversation_state.selected_part:205 return "אני צריך שתבחר חלק פנימי כדי לשוחח איתו."206 207 # Always generate persona-based response first (our reliable system)208 persona_response = self.generate_persona_response(user_message, conversation_state)209 210 # If model is available, try to enhance the response (but don't depend on it)211 if self.model_available and self.generator:212 try:213 # Create a simple English prompt for the model to add conversational flow214 english_prompt = f"User said they feel: {user_message[:50]}. Respond supportively in 1-2 sentences:"215 216 model_output = self.generator(english_prompt, max_new_tokens=30, temperature=0.7)217 218 if model_output and len(model_output) > 0:219 # Extract any useful emotional tone or structure, but keep Hebrew content220 model_text = model_output[0]["generated_text"].strip()221 # Don't replace our Hebrew response, just use model for emotional context222 logger.info(f"Model provided contextual input: {model_text[:50]}...")223 224 except Exception as model_error:225 logger.warning(f"Model enhancement failed: {model_error}")226 # Continue with persona response only227 228 # Always return the Hebrew persona response229 return persona_response230 231 except Exception as e:232 logger.error(f"Error generating response: {e}")233 return "סליחה, בואנו ננסה שוב. איך אתה מרגיש עכשיו?"234 235 def create_main_interface(self):236 """Create the main Gradio interface"""237 238 # Custom CSS for Hebrew support239 css = """240 .rtl {241 direction: rtl;242 text-align: right;243 }244 .hebrew-text {245 font-family: 'Segoe UI', Tahoma, Arial, sans-serif;246 direction: rtl;247 text-align: right;248 }249 .welcome-text {250 font-size: 24px;251 font-weight: bold;252 color: #2c5aa0;253 margin: 20px 0;254 }255 """256 257 with gr.Blocks(css=css, title="מראות - מרחב אישי לשיח פנימי", theme=gr.themes.Soft()) as demo:258 259 # Session state260 conversation_state = gr.State(self.conversation_manager.create_new_session())261 262 # Header263 status_message = "🤖 מערכת תגובות מותאמת אישית פעילה" if not self.model_available else "🤖 מערכת מלאה עם מודל AI פעילה"264 265 gr.HTML(f"""266 <div class="hebrew-text welcome-text" style="text-align: center;">267 🪞 מראות: מרחב אישי לשיח פנימי ומפתח עם עצמך 🪞268 </div>269 <div class="hebrew-text" style="text-align: center; margin-bottom: 20px;">270 מקום בטוח לשוחח עם החלקים השונים של עצמך ולפתח הבנה עצמית עמוקה יותר271 </div>272 <div style="background-color: #e8f5e8; border: 1px solid #4caf50; padding: 10px; margin: 10px 0; border-radius: 5px; text-align: center;">273 <strong>{status_message}</strong>274 </div>275 """)276 277 # Main interface areas278 with gr.Column():279 280 # Step 1: Initial context gathering281 with gr.Group(visible=True) as initial_step:282 gr.Markdown("## שלב 1: ספר/ספרי על עצמך", elem_classes=["hebrew-text"])283 284 initial_prompts = get_initial_prompts()285 286 initial_choice = gr.Radio(287 choices=[288 ("תאר/תארי את עצמך כאדם", "describe_self"),289 ("איך אתה חושב שאחרים רואים אותך?", "self_perception"),290 ("איזה אתגר אתה חווה עכשיו בחיים?", "current_challenge")291 ],292 label="בחר/בחרי נושא לשיתוף:",293 elem_classes=["hebrew-text"]294 )295 296 user_context_input = gr.Textbox(297 label="ספר/ספרי בכמה משפטים:",298 placeholder="כתוב/כתבי כאן את המחשבות שלך...",299 lines=4,300 elem_classes=["hebrew-text"]301 )302 303 continue_to_parts = gr.Button("המשך לבחירת חלק פנימי", variant="primary")304 305 # Step 2: Part selection306 with gr.Group(visible=False) as parts_step:307 gr.Markdown("## שלב 2: בחר/בחרי חלק פנימי לשיחה", elem_classes=["hebrew-text"])308 309 part_selection = gr.Radio(310 choices=[311 ("הקול הביקורתי - החלק שמנסה להגן עליך על ידי ביקורת והכוונה", "הקול הביקורתי"),312 ("הילד/ה הפנימית - החלק הפגיע, הצעיר והאמיתי שלך", "הילד/ה הפנימית"),313 ("המרצה - החלק שרוצה שכולם יהיו מרוצים", "המרצה"),314 ("המגן - החלק החזק שמגן עליך מפני פגיעות", "המגן"),315 ("הנמנע/ת - החלק שמעדיף להימנע ממצבים מאתגרים", "הנמנע/ת")316 ],317 label="איזה חלק פנימי תרצה לפגוש?",318 elem_classes=["hebrew-text"]319 )320 321 # Customization options322 with gr.Accordion("התאמה אישית (אופציונלי)", open=False):323 persona_name = gr.Textbox(324 label="שם לחלק הזה:",325 placeholder="למשל: דני, מיכל, אבי...",326 elem_classes=["hebrew-text"]327 )328 persona_age = gr.Textbox(329 label="גיל או תקופת חיים:",330 placeholder="למשל: ילד/ה, מתבגר/ת, בוגר/ת...",331 elem_classes=["hebrew-text"]332 )333 persona_style = gr.Textbox(334 label="סגנון דיבור מיוחד:",335 placeholder="למשל: רגוש, רציני, משעשע...",336 elem_classes=["hebrew-text"]337 )338 339 start_conversation = gr.Button("התחל שיחה", variant="primary")340 341 # Step 3: Conversation interface342 with gr.Group(visible=False) as conversation_step:343 gr.Markdown("## שיחה עם החלק הפנימי שלך", elem_classes=["hebrew-text"])344 345 current_part_display = gr.Markdown("", elem_classes=["hebrew-text"])346 347 # Chat interface348 with gr.Row():349 with gr.Column(scale=4):350 chatbot = gr.Chatbot(351 height=400,352 label="השיחה שלך",353 elem_classes=["hebrew-text"],354 rtl=True355 )356 357 msg_input = gr.Textbox(358 label="ההודעה שלך:",359 placeholder="כתוב/כתבי את המחשבות שלך כאן...",360 lines=2,361 elem_classes=["hebrew-text"]362 )363 364 with gr.Row():365 send_btn = gr.Button("שלח", variant="primary")366 clear_btn = gr.Button("נקה שיחה")367 368 with gr.Column(scale=1):369 gr.Markdown("### פעולות נוספות", elem_classes=["hebrew-text"])370 change_part_btn = gr.Button("החלף חלק פנימי")371 restart_btn = gr.Button("התחל מחדש")372 373 # Event handlers374 def process_initial_context(choice, context, state):375 """Process initial context and move to part selection"""376 if not choice or not context.strip():377 gr.Warning("אנא בחר נושא וכתב משהו כדי להמשיך")378 return state, gr.update(), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)379 380 state = self.conversation_manager.set_initial_context(state, choice, context)381 return (382 state, 383 gr.update(),384 gr.update(visible=False), 385 gr.update(visible=True), 386 gr.update(visible=False)387 )388 389 def start_chat(part, p_name, p_age, p_style, state):390 """Start the conversation with selected part"""391 if not part:392 gr.Warning("אנא בחר חלק פנימי כדי להתחיל")393 return state, gr.update(), gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), gr.update()394 395 state = self.conversation_manager.set_selected_part(396 state, part, p_name.strip() if p_name else None, 397 p_age.strip() if p_age else None, p_style.strip() if p_style else None398 )399 400 part_info = DEFAULT_PARTS.get(part, {})401 display_name = (p_name.strip() if p_name else None) or part_info.get("default_persona_name", "חלק פנימי")402 403 display_text = f"🗣️ כעת אתה מתשוחח עם: **{display_name}** ({part})"404 405 return (406 state,407 display_text,408 gr.update(visible=False),409 gr.update(visible=False), 410 gr.update(visible=True),411 []412 )413 414 def handle_message(message, history, state):415 """Handle user message and generate response"""416 if not message.strip():417 return "", history, state418 419 # Generate response420 response = self.generate_response(message, state)421 422 # Update conversation state423 state = self.conversation_manager.add_to_history(state, message, response)424 425 # Update history for display426 history.append([message, response])427 428 return "", history, state429 430 def clear_conversation(state):431 """Clear conversation history"""432 state = self.conversation_manager.clear_conversation(state)433 return [], state434 435 def change_part():436 """Return to part selection"""437 return (438 gr.update(visible=False),439 gr.update(visible=True),440 gr.update(visible=False)441 )442 443 def restart_completely():444 """Restart the entire session"""445 new_state = self.conversation_manager.create_new_session()446 return (447 new_state,448 gr.update(visible=True),449 gr.update(visible=False),450 gr.update(visible=False),451 [],452 "",453 "",454 None,455 None,456 "",457 "",458 ""459 )460 461 # Wire up event handlers462 continue_to_parts.click(463 fn=process_initial_context,464 inputs=[initial_choice, user_context_input, conversation_state],465 outputs=[conversation_state, current_part_display, initial_step, parts_step, conversation_step]466 )467 468 start_conversation.click(469 fn=start_chat,470 inputs=[part_selection, persona_name, persona_age, persona_style, conversation_state],471 outputs=[conversation_state, current_part_display, initial_step, parts_step, conversation_step, chatbot]472 )473 474 # Chat message handling475 msg_input.submit(476 fn=handle_message,477 inputs=[msg_input, chatbot, conversation_state],478 outputs=[msg_input, chatbot, conversation_state]479 )480 481 send_btn.click(482 fn=handle_message,483 inputs=[msg_input, chatbot, conversation_state],484 outputs=[msg_input, chatbot, conversation_state]485 )486 487 clear_btn.click(488 fn=clear_conversation,489 inputs=[conversation_state],490 outputs=[chatbot, conversation_state]491 )492 493 change_part_btn.click(494 fn=change_part,495 outputs=[conversation_step, parts_step, initial_step]496 )497 498 restart_btn.click(499 fn=restart_completely,500 outputs=[conversation_state, initial_step, parts_step, conversation_step, chatbot, 501 user_context_input, current_part_display, initial_choice, part_selection,502 persona_name, persona_age, persona_style]503 )504 505 return demo506 507def main():508 """Main function to launch the application"""509 logger.info("Starting מראות application...")510 511 try:512 app = MirautrApp()513 demo = app.create_main_interface()514 515 # Check environment516 is_hf_spaces = os.getenv("SPACE_ID") is not None517 518 logger.info(f"Launching app... HF Spaces: {is_hf_spaces}")519 520 # Unified launch configuration for both environments521 # This ensures identical experience in both local and HF Spaces522 launch_config = {523 "show_error": True,524 "show_api": False, # Disable API docs to avoid schema issues525 "favicon_path": None,526 "auth": None,527 "enable_queue": False, # Disable queue to prevent schema issues528 "max_threads": 1 # Limit threads for stability529 }530 531 if is_hf_spaces:532 # HF Spaces specific settings533 logger.info("Configuring for HF Spaces deployment")534 launch_config.update({535 "server_name": "0.0.0.0",536 "server_port": 7860,537 "share": False, # HF Spaces handles public access538 "quiet": True539 })540 else:541 # Local development settings542 logger.info("Configuring for local development")543 544 # Try to find an available port545 default_port = int(os.getenv("GRADIO_SERVER_PORT", "7861"))546 available_port = default_port547 548 # Check if port is available, if not find next available549 import socket550 for port_try in range(default_port, default_port + 10):551 try:552 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:553 s.bind(('127.0.0.1', port_try))554 available_port = port_try555 break556 except OSError:557 continue558 559 logger.info(f"Using port {available_port} for local development")560 561 launch_config.update({562 "server_name": "127.0.0.1", 563 "server_port": available_port,564 "share": True, # Enable share for local testing to avoid localhost issues565 "inbrowser": True, # Auto-open browser566 "quiet": False567 })568 569 demo.launch(**launch_config)570 571 except Exception as e:572 logger.error(f"Failed to start application: {e}")573 raise574 575if __name__ == "__main__":576 main() 