Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 106 additions & 78 deletions handlers/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,16 @@ def _create_or_update_user(self, user) -> bool:
)
return is_admin

async def _send_error_response(self, update: Update, username: str, lang: str) -> None:
async def _send_error_response(self, update: Update, username: str, lang: str, loading_msg: Optional[Any] = None) -> None:
error_message = self.persona.get_error_message(username=username or "user", lang=lang)
formatted_error = format_error_response(error_message)
await update.message.reply_html(formatted_error)
if loading_msg:
try:
await loading_msg.edit_text(formatted_error, parse_mode="HTML")
except Exception:
await update.message.reply_html(formatted_error)
else:
await update.message.reply_html(formatted_error)

async def chat_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
user = update.effective_user
Expand Down Expand Up @@ -150,85 +156,99 @@ async def chat_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE)
self.memory.save_user_message(user.id, query)
self.context_manager.apply_sliding_window(user.id)

message_context = {}
if FEATURES.get("emotion_detection", False) and self.nlp:
message_context = self.nlp.get_message_context(query, user.id)
logger.debug(f"Message context for user {user.id}: {message_context}")

affection_delta = 0
if message_context:
affection_delta = self._calculate_affection_delta(user.id, message_context)

# === STEP 3: Get current mood and calculate new mood ===
mood_manager = MoodManager()

current_mood_data = self.db.get_user_mood(user.id)
user_info = self.db.get_user_relationship_info(user.id)

new_mood_state = mood_manager.calculate_mood(
current_mood=current_mood_data["mood"],
current_intensity=current_mood_data["intensity"],
affection_delta=affection_delta,
emotion_context=message_context,
relationship_level=user_info["relationship_level"],
last_mood_change=current_mood_data["last_change"]
)

# === STEP 4: Apply mood modifier to affection delta ===
mood_modifier = mood_manager.get_affection_modifier(
new_mood_state.mood,
affection_delta
)
modified_affection_delta = int(affection_delta * mood_modifier)

logger.info(
f"[MOOD] User {user.id}: {current_mood_data['mood']} → {new_mood_state.mood} "
f"(intensity: {new_mood_state.intensity}) | "
f"Affection: {affection_delta} → {modified_affection_delta} (×{mood_modifier:.1f})"
)

# === STEP 5: Update affection with mood-modified delta ===
if modified_affection_delta != 0:
self.db.update_affection(user.id, modified_affection_delta)

# === STEP 6: Update mood in database ===
updated_history = mood_manager.add_to_mood_history(
current_mood_data["history"],
new_mood_state
)
self.db.update_user_mood(
user.id,
new_mood_state.mood,
new_mood_state.intensity,
updated_history
)

# === STEP 7: Increment interaction count (will recalculate level) ===
self.db.increment_interaction_count(user.id)
# Send initial loading message
phrase = "Alya is thinking" if lang == "en" else "Alya lagi mikir"
loading_msg = await update.message.reply_text(f"<blockquote><b>💭 {phrase}...</b></blockquote>", parse_mode="HTML")

from utils.telegram_helpers import start_loading_animation
loading_task = start_loading_animation(loading_msg, phrase)

# Prepare context with LATEST relationship level and MOOD
user_context = await self._prepare_conversation_context(
user, query, lang, message_context, new_mood_state, mood_manager
)
response = await self.gemini.generate_response(
user_id=user.id,
username=user.first_name or "user",
message=user_context["enhanced_query"],
context=user_context["system_prompt"],
relationship_level=user_context["relationship_level"],
is_admin=user.id in ADMIN_IDS or self.db.is_admin(user.id),
lang=lang,
retry_count=3,
is_media_analysis=False,
media_context=None
)
try:
message_context = {}
if FEATURES.get("emotion_detection", False) and self.nlp:
message_context = self.nlp.get_message_context(query, user.id)
logger.debug(f"Message context for user {user.id}: {message_context}")

affection_delta = 0
if message_context:
affection_delta = self._calculate_affection_delta(user.id, message_context)

# === STEP 3: Get current mood and calculate new mood ===
mood_manager = MoodManager()

current_mood_data = self.db.get_user_mood(user.id)
user_info = self.db.get_user_relationship_info(user.id)

new_mood_state = mood_manager.calculate_mood(
current_mood=current_mood_data["mood"],
current_intensity=current_mood_data["intensity"],
affection_delta=affection_delta,
emotion_context=message_context,
relationship_level=user_info["relationship_level"],
last_mood_change=current_mood_data["last_change"]
)

# === STEP 4: Apply mood modifier to affection delta ===
mood_modifier = mood_manager.get_affection_modifier(
new_mood_state.mood,
affection_delta
)
modified_affection_delta = int(affection_delta * mood_modifier)

logger.info(
f"[MOOD] User {user.id}: {current_mood_data['mood']} → {new_mood_state.mood} "
f"(intensity: {new_mood_state.intensity}) | "
f"Affection: {affection_delta} → {modified_affection_delta} (×{mood_modifier:.1f})"
)

# === STEP 5: Update affection with mood-modified delta ===
if modified_affection_delta != 0:
self.db.update_affection(user.id, modified_affection_delta)

# === STEP 6: Update mood in database ===
updated_history = mood_manager.add_to_mood_history(
current_mood_data["history"],
new_mood_state
)
self.db.update_user_mood(
user.id,
new_mood_state.mood,
new_mood_state.intensity,
updated_history
)

# === STEP 7: Increment interaction count (will recalculate level) ===
self.db.increment_interaction_count(user.id)

# Prepare context with LATEST relationship level and MOOD
user_context = await self._prepare_conversation_context(
user, query, lang, message_context, new_mood_state, mood_manager
)
response = await self.gemini.generate_response(
user_id=user.id,
username=user.first_name or "user",
message=user_context["enhanced_query"],
context=user_context["system_prompt"],
relationship_level=user_context["relationship_level"],
is_admin=user.id in ADMIN_IDS or self.db.is_admin(user.id),
lang=lang,
retry_count=3,
is_media_analysis=False,
media_context=None
)
finally:
loading_task.cancel()
try:
await loading_task
except asyncio.CancelledError:
pass

if response:
logger.info(f"[RESPONSE_RECEIVED] Got response from Gemini, length={len(response)}")
await self._process_and_send_response(update, user, response, user_context["message_context"], lang)
await self._process_and_send_response(update, user, response, user_context["message_context"], lang, loading_msg=loading_msg)
else:
logger.warning(f"[RESPONSE_RECEIVED] Response is empty or None")
await self._send_error_response(update, user.first_name, lang)
await self._send_error_response(update, user.first_name, lang, loading_msg=loading_msg)
except Exception as e:
logger.error(f"Error in chat command: {e}", exc_info=True)
await self._send_error_response(update, user.first_name, lang)
Expand Down Expand Up @@ -489,7 +509,8 @@ async def _process_and_send_response(
user,
response: str,
message_context: Dict[str, Any],
lang: str
lang: str,
loading_msg: Optional[Any] = None
) -> None:
"""Clean, format, and send response to Telegram."""
try:
Expand All @@ -506,10 +527,17 @@ async def _process_and_send_response(
formatted_response = format_persona_response(response, use_html=True)
formatted_response = f"{formatted_response}\u200C"

await update.message.reply_html(formatted_response)
if loading_msg:
try:
await loading_msg.edit_text(formatted_response, parse_mode="HTML")
except Exception as e:
logger.error(f"Failed to edit message: {e}")
await update.message.reply_html(formatted_response)
else:
await update.message.reply_html(formatted_response)
except Exception as e:
logger.error(f"Error processing response: {e}", exc_info=True)
await self._send_error_response(update, user.first_name, lang)
await self._send_error_response(update, user.first_name, lang, loading_msg=loading_msg)

def _clean_and_append_russian_translation(self, response: str, lang: str = DEFAULT_LANGUAGE) -> str:
"""Extract and translate Russian expressions (both marked and unmarked)."""
Expand Down
62 changes: 48 additions & 14 deletions handlers/voice.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ async def handle_voice_message(self, update: Update, context: ContextTypes.DEFAU
lang_flag = {"en": "🇺🇸", "id": "🇮🇩", "jp": "🎌"}.get(detected_lang, "🌐")
await update.message.reply_html(f"🎤 <i>({lang_flag} {detected_lang.upper()}): {user_text}</i>")

import asyncio
phrase = "Alya is thinking" if db_user_dict.get('language_code', DEFAULT_LANGUAGE) == 'en' else "Alya lagi mikir"
loading_msg = await update.message.reply_text(f"<blockquote><b>💭 {phrase}...</b></blockquote>", parse_mode="HTML")

from utils.telegram_helpers import start_loading_animation
loading_task = start_loading_animation(loading_msg, phrase)

# 2. Memory & Relationship Updates
self.db_manager.save_message(user.id, "user", user_text)
if self.memory_manager:
Expand All @@ -148,36 +155,62 @@ async def handle_voice_message(self, update: Update, context: ContextTypes.DEFAU
# Clean format: [Role] Content
history_text = "\n".join([f"[{msg['role'].capitalize()}] {msg['content']}" for msg in history])

response = await self.gemini_client.generate_response(
user_id=user.id,
username=user.first_name or "user",
message=user_text,
context=self.persona_manager.get_chat_prompt(
username=user.first_name,
try:
response = await self.gemini_client.generate_response(
user_id=user.id,
username=user.first_name or "user",
message=user_text,
context=history_text,
context=self.persona_manager.get_chat_prompt(
username=user.first_name,
message=user_text,
context=history_text,
relationship_level=rel_level,
is_admin=is_admin,
lang=db_user_dict.get('language_code', DEFAULT_LANGUAGE)
),
relationship_level=rel_level,
is_admin=is_admin,
lang=db_user_dict.get('language_code', DEFAULT_LANGUAGE)
),
relationship_level=rel_level,
is_admin=is_admin,
lang=db_user_dict.get('language_code', DEFAULT_LANGUAGE)
)
)
finally:
loading_task.cancel()
try:
await loading_task
except asyncio.CancelledError:
pass

if not response:
await update.message.reply_html("❌ Gagal mendapatkan respon dari Alya...")
error_msg = "❌ Gagal mendapatkan respon dari Alya..."
try:
await loading_msg.edit_text(error_msg, parse_mode="HTML")
except Exception:
await update.message.reply_html(error_msg)
return

ui_text = format_persona_response(response, use_html=True) + "\u200C"
await update.message.reply_html(ui_text)
try:
await loading_msg.edit_text(ui_text, parse_mode="HTML")
except Exception:
await update.message.reply_html(ui_text)

voice_lang = self.db_manager.get_user_voice_language(user.id) if self.db_manager else "en"
source_lang = db_user_dict.get('language_code', DEFAULT_LANGUAGE)

# Extract dialogue and translate to voice_lang before sending to TTS
tts_text = await translate_response_for_voice(response, source_lang, voice_lang)

import asyncio
tts_phrase = "Alya is recording a voice note" if source_lang == 'en' else "Alya lagi ngerekam voice note"
tts_loading_msg = await update.message.reply_text(f"<blockquote><b>🎙️ {tts_phrase}...</b></blockquote>", parse_mode="HTML")

from utils.telegram_helpers import start_loading_animation
tts_loading_task = start_loading_animation(
tts_loading_msg,
tts_phrase,
frames=["🎙️", "🎶", "✨"],
interval=1.2
)

await dispatch_tts(
bot=context.bot,
chat_id=update.effective_chat.id,
Expand All @@ -186,6 +219,7 @@ async def handle_voice_message(self, update: Update, context: ContextTypes.DEFAU
response_text=tts_text,
voice_lang=voice_lang,
user_lang=source_lang,
loading_message_id=tts_loading_msg.message_id
)

# 5. Metadata Update
Expand Down
30 changes: 23 additions & 7 deletions utils/analyze.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import io
import logging
import asyncio
from pathlib import Path
from typing import Dict, Any, Optional, Union, BinaryIO

Expand Down Expand Up @@ -241,12 +242,15 @@ async def handle_analysis_command(update: Update, context: ContextTypes.DEFAULT_
await message.reply_html(analyze_response(lang))
return

# If no query specified for media, use default
lang = get_user_lang(user.id)
if not query and media_type != "text":
lang = get_user_lang(user.id)
query = "Analyze this for me, please." if lang == 'en' else "Tolong analisis ini."

await context.bot.send_chat_action(chat_id=update.effective_chat.id, action=ChatAction.TYPING)
phrase = "Alya is analyzing" if lang == 'en' else "Alya sedang menganalisis"
loading_msg = await message.reply_text(f"<blockquote><b>🔍 {phrase}...</b></blockquote>", parse_mode="HTML")

from utils.telegram_helpers import start_loading_animation
loading_task = start_loading_animation(loading_msg, phrase, frames=["🔍", "🧐", "✨"])

try:
result = await analyzer.analyze_media(
Expand All @@ -263,14 +267,26 @@ async def handle_analysis_command(update: Update, context: ContextTypes.DEFAULT_
lang=lang,
username=user.first_name
)

finally:
loading_task.cancel()
try:
await loading_task
except asyncio.CancelledError:
pass

try:
# Handle if result is a list (long message split)
if isinstance(formatted_result, list):
for part in formatted_result:
await loading_msg.edit_text(formatted_result[0], parse_mode='HTML', disable_web_page_preview=True)
for part in formatted_result[1:]:
await message.reply_html(part, disable_web_page_preview=True)
else:
await message.reply_html(formatted_result, disable_web_page_preview=True)
await loading_msg.edit_text(formatted_result, parse_mode='HTML', disable_web_page_preview=True)
except Exception as e:
logger.error(f"Failed to handle analysis command for user {user.id}: {e}", exc_info=True)
lang = get_user_lang(user.id)
await message.reply_html(get_system_error_response(lang))
error_response = get_system_error_response(lang)
try:
await loading_msg.edit_text(error_response, parse_mode='HTML')
except Exception:
await message.reply_html(error_response)
Loading
Loading