```
#!/bin/python
import os
import json
import subprocess
import threading
from datetime import datetime
import ollama
import time
import sys
import signal
from prompt_toolkit import PromptSession
from prompt_toolkit.history import InMemoryHistory

# Configuration
ALARM_SOUND_FILE = os.path.expanduser("~/Music/166467_rocket_race.mp3")
PERSISTENT_STORAGE_FILE = "alarm_chat_history.json"
SCHEDULE_FILE = "user_schedule.json"
OLLAMA_MODEL = "gemma3:1b"
SUMMARY_INTERVAL = 60  # 10 minutes in seconds

# Escape sequences
ESCAPE_SEQUENCES = {
    "stop_alarm": "<<STOP_ALARM>>",
    "snooze": "<<SNOOZE>>",
    "add_to_schedule": "<<ADD_TO_SCHEDULE>>",
    "remove_from_schedule": "<<REMOVE_FROM_SCHEDULE>>",
}

class AlarmChatApp:
    def __init__(self):
        # Initialize attributes
        self.alarm_active = False
        self.mpv_process = None
        self.tts_process = None
        self.chat_history = []
        self.current_conversation = None
        self.last_summary_time = time.time()
        self.tts_available = self.check_tts_available()
        self.user_personality = {"description": "No personality data available."}

        # Verify requirements and load stored data
        self.verify_system_requirements()
        self.load_persistent_data()
        self.load_schedule()

        # Setup signal handlers
        signal.signal(signal.SIGINT, self.signal_handler)
        signal.signal(signal.SIGTERM, self.signal_handler)
        signal.signal(signal.SIGHUP, self.signal_handler)
        signal.signal(signal.SIGQUIT, self.signal_handler)
        signal.signal(signal.SIGTRAP, self.signal_handler)
        signal.signal(signal.SIGSYS, self.signal_handler)

    def check_tts_available(self):
        """Check if spd-say is available"""
        try:
            subprocess.run(['spd-say', '--version'],
                           stdout=subprocess.DEVNULL,
                           stderr=subprocess.DEVNULL)
            return True
        except (FileNotFoundError, subprocess.SubprocessError):
            print("Warning: spd-say not found. Falling back to text-only mode.")
            return False

    def load_persistent_data(self):
        """Load chat history from file"""
        try:
            with open(PERSISTENT_STORAGE_FILE, 'r') as f:
                self.chat_history = json.load(f)
        except (FileNotFoundError, json.JSONDecodeError) as e:
            print(f"Warning: Could not load chat history - {str(e)}")
            self.chat_history = []

    def load_schedule(self):
        """Load the user schedule from file"""
        try:
            with open(SCHEDULE_FILE, 'r') as f:
                self.user_schedule = json.load(f)
        except (FileNotFoundError, json.JSONDecodeError) as e:
            print(f"Warning: Could not load schedule - {str(e)}")
            self.user_schedule = {
                "wakeup_times": [],
                "preferred_wakeup_questions": [],
                "emergency_contacts": []
            }

    def save_schedule(self):
        """Save the updated user schedule"""
        try:
            with open(SCHEDULE_FILE, 'w') as f:
                json.dump(self.user_schedule, f)
        except IOError as e:
            print(f"Error saving schedule: {str(e)}")

    def verify_system_requirements(self):
        """Check required components"""
        try:
            subprocess.run(['ollama', '--version'],
                           stdout=subprocess.DEVNULL,
                           stderr=subprocess.DEVNULL)
        except FileNotFoundError:
            print("Error: Ollama is required but not found. Please install Ollama.")
            sys.exit(1)
        if not self.verify_model():
            print(f"Error: Model '{OLLAMA_MODEL}' is not available.")
            print(f"Please install it with: ollama pull {OLLAMA_MODEL}")
            sys.exit(1)

    def verify_model(self):
        """Check if the required model is available"""
        try:
            models = ollama.list()
            return any(m['model'] == OLLAMA_MODEL for m in models.get('models', []))
        except Exception as e:
            print(f"Error checking for model: {str(e)}")
            return False

    def text_to_speech(self, text):
        """Convert text to speech using spd-say"""
        if not self.tts_available or not text.strip():
            return
        if self.tts_process and self.tts_process.poll() is None:
            self.tts_process.terminate()
        try:
            self.tts_process = subprocess.Popen(
                ['spd-say', '-w', text],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL
            )
        except Exception as e:
            print(f"Error in TTS: {str(e)}")
            self.tts_available = False

    def generate_hint_prompt(self):
        """
        Create a short hint from past conversation history.
        """
        hints = []
        for conv in self.chat_history:
            timestamp = conv.get("timestamp", "unknown time")
            if not conv.get("woke_up", False):
                hints.append(f"On {timestamp}, the attempt was unsuccessful.")
            else:
                hints.append(f"On {timestamp}, you woke up successfully.")
        if hints:
            return "Past conversation summary: " + " ".join(hints)
        else:
            return "No past conversation history available."

    def update_personality_data(self):
        """
        Call the LLM to generate a personality summary from past user messages.
        This function builds a prompt based on recent conversation history.
        """
        try:
            if not self.chat_history:
                return "No conversation history available for personality analysis."

            # Use some of the latest conversations by selecting a subset
            recent_text = ""
            for conv in self.chat_history[-3:]:
                for msg in conv.get("messages", []):
                    if "user" in msg:
                        recent_text += msg["user"] + "\n"

            prompt_text = (
                "Based on the following user messages, summarize the user's personality, "
                "including any apparent likes, dislikes, strengths, and weaknesses:\n\n"
                f"{recent_text}\n"
                "Provide a concise summary."
            )
            messages = [
                {"role": "system", "content": "You are a helpful assistant that analyzes personality."},
                {"role": "user", "content": prompt_text}
            ]
            response = ollama.chat(
                model=OLLAMA_MODEL,
                messages=messages,
                options={"temperature": 0.3}
            )
            personality_summary = response["message"]["content"]
            return personality_summary.strip()
        except Exception as e:
            print(f"Error generating personality data: {str(e)}")
            return "Personality data not available."

    def generate_personality_text(self):
        """
        Obtain personality summary dynamically from the LLM.
        """
        self.user_personality["description"] = self.update_personality_data()
        return f"Personality: {self.user_personality['description']}"

    def generate_system_prompt(self):
        """Generate the system prompt for the LLM including dynamic hints and personality info"""
        try:
            current_time = datetime.now()
            weekday = current_time.strftime("%A")
            time_str = current_time.strftime("%H:%M")

            today_schedule = next(
                (day for day in self.user_schedule["wakeup_times"]
                 if day["weekday"].lower() == weekday.lower()), None)

            schedule_info = f"Today is {weekday}. "
            if today_schedule:
                schedule_info += f"Scheduled wake-up time: {today_schedule['time']}. "
                if today_schedule.get("important_events"):
                    schedule_info += "Important events: " + ", ".join(today_schedule["important_events"])
            else:
                schedule_info += "No specific wake-up time scheduled for today."

            hint_prompt = self.generate_hint_prompt()
            personality_text = self.generate_personality_text()

            return f"""You are an intelligent alarm clock helping the user wake up. Your primary goal is to determine if the user is truly awake and alert.
Talk like a police officer interrogating a suspect or a military officer addressing a subordinate.

Current time: {time_str}
{schedule_info}

Previous attempts hint: {hint_prompt}

User personality details: {personality_text}

Special commands available:
{ESCAPE_SEQUENCES["stop_alarm"]} - Stop the alarm (only when absolutely sure the user is awake)
{ESCAPE_SEQUENCES["snooze"]} - Snooze for 5 minutes (if the user seems groggy)
{ESCAPE_SEQUENCES["add_to_schedule"]} - Modify schedule: add a new wakeup time
{ESCAPE_SEQUENCES["remove_from_schedule"]} - Modify schedule: remove an existing wakeup time

Assessment criteria for wakefulness:
1. Responses should be coherent and specific.
2. Must demonstrate awareness of time/surroundings.
3. Should indicate a clear intention to start the day.
4. If user written nothing or too few for long time, then snoose.
   User write short because it is sleepy or already fallen asleep.

Begin by asking a specific question that requires a thoughtful answer.
"""
        except Exception as e:
            print(f"Error generating system prompt: {str(e)}")
            return ("You are an alarm clock assistant. Determine if the user is awake by "
                    "asking thoughtful questions. Use <<STOP_ALARM>> when convinced they're awake, "
                    "or <<SNOOZE>> if more time is needed.")

    def generate_periodic_summary(self):
        """Generate a periodic summary using LLM"""
        try:
            if not self.current_conversation:
                return "<<PERIODIC_SUMMARY>>\nNo active conversation\n"

            # Prepare conversation history
            messages = self.current_conversation.get("messages", [])
            conversation_text = ""
            for msg in messages:
                if "user" in msg:
                    conversation_text += f"User: {msg['user']}\n"
                elif "assistant" in msg:
                    conversation_text += f"Assistant: {msg['assistant']}\n"

            # Create LLM prompt
            prompt = f"""Analyze this alarm clock conversation and determine if the user is fully awake. Consider:
- Response coherence and specificity
- Awareness of time/context
- Clear intention to start the day
- If user written nothing or too few for long time, then snoose.
  User write short because it is sleepy or already fallen asleep.

Conversation history:
{messages}

Provide a concise summary ending with either {ESCAPE_SEQUENCES['stop_alarm']} (if awake) or {ESCAPE_SEQUENCES['snooze']} (if not)."""

            # Get LLM response
            response = ollama.chat(
                model=OLLAMA_MODEL,
                messages=[{"role": "system", "content": prompt}],
                options={"temperature": 0.2}
            )
            llm_summary = response['message']['content'].strip()

            # Ensure proper command formatting
            summary = f"<<PERIODIC_SUMMARY>>\n{llm_summary}"
            return summary
        except Exception as e:
            print(f"Error generating summary: {str(e)}")
            return "<<PERIODIC_SUMMARY>>\nError generating summary\n"

    def check_wakefulness(self):
        """Analyze conversation to determine if user is awake"""
        try:
            if not self.current_conversation or len(self.current_conversation["messages"]) < 4:
                return False
            last_responses = [msg["user"] for msg in self.current_conversation["messages"]
                              if isinstance(msg, dict) and "user" in msg][-2:]
            return all(len(response.split()) > 5 for response in last_responses)
        except Exception as e:
            print(f"Error checking wakefulness: {str(e)}")
            return False

    def chat_with_llm(self, user_input=None):
        """Send input to LLM and process response"""
        try:
            messages = [{"role": "system", "content": self.generate_system_prompt()}]
            if user_input:
                messages.append({"role": "user", "content": user_input})
            response = ollama.chat(
                model=OLLAMA_MODEL,
                messages=messages,
                options={"temperature": 0.7}
            )
            llm_response = response['message']['content']
            action_taken = None
            for cmd, seq in ESCAPE_SEQUENCES.items():
                if seq in llm_response:
                    llm_response = llm_response.replace(seq, "").strip()
                    action_taken = cmd
                    self.handle_escape_sequence(cmd)
            if llm_response.strip():
                self.text_to_speech(llm_response)
            return llm_response, action_taken
        except Exception as e:
            print(f"Error communicating with LLM: {str(e)}")
            return "I'm having trouble understanding. Can you repeat that?", None

    def handle_escape_sequence(self, cmd):
        """Handle detected escape sequences"""
        try:
            print(f"\n[System executing command: {cmd}]")
            if cmd == "stop_alarm":
                self.stop_alarm()
                if self.current_conversation:
                    self.current_conversation["woke_up"] = True
            elif cmd == "snooze":
                self.stop_alarm()
                print("Snoozing for 5 minutes...")
                threading.Timer(300, self.play_alarm).start()
            elif cmd == "add_to_schedule":
                self.automated_schedule_update(context, add=True)
            elif cmd == "remove_from_schedule":
                self.automated_schedule_update(context, add=False)
        except Exception as e:
            print(f"Error handling command {cmd}: {str(e)}")

    def automated_schedule_update(self, context, add=True):
        """Parse LLM response for schedule updates"""
        try:
            # Example context format: "Friday,07:00,Meeting;Gym"
            parts = context.split(',')
            if len(parts) < 2:
                raise ValueError("Invalid schedule format")

            weekday = parts[0].strip()
            time_str = parts[1].strip()
            events = parts[2].split(';') if len(parts) > 2 else []

            if add:
                new_entry = {"weekday": weekday, "time": time_str, "important_events": events}
                self.user_schedule["wakeup_times"].append(new_entry)
                print(f"Added schedule: {new_entry}")
            else:
                self.user_schedule["wakeup_times"] = [
                    entry for entry in self.user_schedule["wakeup_times"]
                    if not (entry["weekday"].lower() == weekday.lower()
                            and entry["time"] == time_str)
                ]
                print(f"Removed schedules matching: {weekday} {time_str}")

            self.save_schedule()

        except Exception as e:
            print(f"Error processing schedule update: {str(e)}")

    def play_alarm(self):
        """Play alarm sound in a loop using mpv"""
        if self.alarm_active:  # Prevent multiple concurrent alarms
            return
        if not os.path.exists(ALARM_SOUND_FILE):
            print("Warning: Alarm sound file not found. Continuing without audio.")
            self.alarm_active = True
            return
        try:
            self.alarm_active = True
            self.mpv_process = subprocess.Popen(
                ['mpv', '--no-video', ALARM_SOUND_FILE],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                preexec_fn=os.setsid
            )
        except Exception as e:
            print(f"Error starting alarm sound: {str(e)}")
            self.alarm_active = True

    def stop_alarm(self, deactivate=True):
        """Stop the alarm sound and clean up the process"""
        if not self.alarm_active:
            return
        if deactivate:
            self.alarm_active = False
        if self.mpv_process:
            try:
                os.killpg(os.getpgid(self.mpv_process.pid), signal.SIGTERM)
                self.mpv_process.wait(timeout=2)
            except ProcessLookupError:
                pass
            except subprocess.TimeoutExpired:
                try:
                    os.killpg(os.getpgid(self.mpv_process.pid), signal.SIGKILL)
                except ProcessLookupError:
                    pass
            except Exception as e:
                print(f"Error stopping alarm process: {str(e)}")
            finally:
                self.mpv_process = None


    def signal_handler(self, signum, frame):
        """Handle interrupt signals"""
        print(f"\nReceived signal {signum}, doing nothing")
        # Optionally call cleanup() and exit if desired.
        # self.cleanup()
        # sys.exit(1)

    def cleanup(self):
        """Clean up all resources before exit"""
        self.stop_alarm()
        if self.tts_process and self.tts_process.poll() is None:
            self.tts_process.terminate()
            self.tts_process = None
        if self.current_conversation and not self.current_conversation.get("woke_up", False):
            self.current_conversation["woke_up"] = False
            self.chat_history.append(self.current_conversation)
            self.save_persistent_data()

    def save_persistent_data(self):
        """Save persistent chat history to file (last 10 conversations)"""
        try:
            with open(PERSISTENT_STORAGE_FILE, 'w') as f:
                json.dump(self.chat_history[-10:], f)
        except IOError as e:
            print(f"Error saving chat history: {str(e)}")

    def run_summary_loop(self):
        """Run the periodic summary loop in a separate thread"""
        while self.alarm_active:
            try:
                # Wait until next scheduled summary time
                time_since_last = time.time() - self.last_summary_time
                if time_since_last < SUMMARY_INTERVAL:
                    time.sleep(SUMMARY_INTERVAL - time_since_last)
                    continue

                # Generate and process summary
                summary = self.generate_periodic_summary()
                print(f"\n[Periodic Summary @ {datetime.now().strftime('%H:%M:%S')}]\n{summary}")

                # Check for escape sequences while keeping alarm active
                for cmd, seq in ESCAPE_SEQUENCES.items():
                    if seq in summary:
                        self.handle_escape_sequence(cmd)

                self.last_summary_time = time.time()

            except Exception as e:
                print(f"Error in summary loop: {str(e)}")
                time.sleep(10)  # Prevent tight error loop

    def run(self):
        """Main chat loop using prompt_toolkit"""
        print("\n=== Alarm Chat App ===")
        print("The alarm will sound until you demonstrate you're fully awake.")
        print("Type 'quit' to exit (but the alarm will keep ringing!)\n")
        try:
            self.current_conversation = {
                "start_time": time.time(),
                "timestamp": datetime.now().isoformat(),
                "messages": [],
                "woke_up": False
            }
            self.play_alarm()
            session = PromptSession(history=InMemoryHistory())
            summary_thread = threading.Thread(target=self.run_summary_loop)
            summary_thread.start()
            try:
                initial_msg, _ = self.chat_with_llm()
                print(f"Assistant: {initial_msg}")
                self.current_conversation["messages"].append({"assistant": initial_msg})
                self.last_summary_time = time.time()
            except Exception as e:
                print(f"Error starting conversation: {str(e)}")
                initial_msg = "Are you awake? Please respond with more than just 'yes'."
                print(f"Assistant: {initial_msg}")
                self.current_conversation["messages"].append({"assistant": initial_msg})
            while self.alarm_active:
                human_time = datetime.fromtimestamp(time.time()).strftime('%H:%M:%S')
                try:
                    user_input = session.prompt("You: ")
                    if user_input.strip().lower() == 'quit':
                        break
                    self.current_conversation["messages"].append({"user": human_time + " " + user_input})
                    llm_response, action_taken = self.chat_with_llm(user_input)
                    if llm_response:
                        print(f"Assistant: {llm_response}")
                        self.current_conversation["messages"].append({"assistant": human_time + " " + llm_response})
                    if action_taken == "stop_alarm":
                        break
                except KeyboardInterrupt:
                    print("\nInterrupt received")
                    break
                except Exception as e:
                    print(f"Error in main loop: {str(e)}")
                    time.sleep(1)
        except Exception as e:
            print(f"Fatal error: {str(e)}")
        finally:
            self.cleanup()
            if self.alarm_active:
                print("\nWarning: Alarm was not properly stopped!")
            else:
                print("\nAlarm stopped. Have a productive day!")

if __name__ == "__main__":
    app = AlarmChatApp()
    app.run()
```
For some reason, this code runs summary only once.