diff --git a/chatbot/admin/company_admin.py b/chatbot/admin/company_admin.py index 4aea919..3060e62 100644 --- a/chatbot/admin/company_admin.py +++ b/chatbot/admin/company_admin.py @@ -294,7 +294,7 @@ def changelist_view(self, request, extra_context=None): @admin.register(CompanyChat) class CompanyChatAdmin(ExportAllFieldsMixin, admin.ModelAdmin): - list_display = ('session', 'sender', 'receiver', 'message', 'translated_message', 'created_at', 'stage') + list_display = ('session', 'sender', 'receiver', 'message', 'translated_message', 'created_at', 'stage', 'status') list_filter = ( CustomAdvanceDateFilter, ProfileCompanyChatFilter, @@ -356,7 +356,7 @@ def get_list_filter(self, request): class ChatSessionAdmin(ExportAllFieldsMixin, admin.ModelAdmin): list_display = ( 'session', 'get_first_name', 'session_status', 'session_type', 'current_question', 'total_steps', - 'created_at' + 'created_at', 'updated_at' ) list_filter = ( 'session', @@ -403,8 +403,8 @@ def get_list_display(self, request): user_email = request.user.email profile = Profile.objects.filter(email=user_email) if not user.is_superuser and len(profile) > 0 and profile[0].profile_type == ProfileType.MODERATOR: - return 'session', 'get_first_name', 'current_question', 'total_steps', 'session_status', 'created_at' - return 'session', 'get_first_name', 'current_question', 'total_steps', 'session_status', 'created_at' + return 'session', 'get_first_name', 'current_question', 'total_steps', 'session_status', 'created_at', 'updated_at' + return 'session', 'get_first_name', 'current_question', 'total_steps', 'session_status', 'created_at', 'updated_at' def get_first_name(self, obj): return obj.profile.first_name if obj.profile else None diff --git a/chatbot/consumers/async_base_consumer.py b/chatbot/consumers/async_base_consumer.py index 1629205..65711a5 100644 --- a/chatbot/consumers/async_base_consumer.py +++ b/chatbot/consumers/async_base_consumer.py @@ -1,7 +1,8 @@ import json from channels.generic.websocket import AsyncWebsocketConsumer from channels.db import database_sync_to_async -from chatbot.models import ChatSession, CompanyChat, ChatStatus, Profile, CompanyBot +from django.db.models import Max +from chatbot.models import ChatSession, CompanyChat, ChatStatus, Profile, CompanyBot, CompanyBotTypeChoices from chatbot.models.company_models import CompanyStateMachine import logging import traceback @@ -67,19 +68,34 @@ def determine_company_chat_status(self, session_id, profile_id, route, is_discon else: company_bot = CompanyBot.objects.get(route=route) + existing_chats = CompanyChat.objects.filter(session=session_id) + + if company_bot.bot_type == CompanyBotTypeChoices.SIMPLE: + if existing_chats.exclude(sender_id=1).count() == 0: + return ChatStatus.STARTED + if is_disconnected: + return ChatStatus.PAUSED + last_chat = existing_chats.last() + if last_chat and last_chat.status == ChatStatus.PAUSED: + return ChatStatus.RESUME + return ChatStatus.IN_PROGRESS + state_machine = CompanyStateMachine.objects.filter( company_bot=company_bot, step=chat_session.current_step ).first() - existing_chats = CompanyChat.objects.filter(session=session_id) + max_step = CompanyStateMachine.objects.filter( + company_bot=company_bot + ).aggregate(Max('step'))['step__max'] + is_last_step = state_machine and state_machine.step == max_step - if existing_chats.count() == 0: + if existing_chats.exclude(sender_id=1).count() == 0: return ChatStatus.STARTED - elif state_machine and state_machine.name != 'APPRECIATION' and is_disconnected: + elif state_machine and not is_last_step and is_disconnected: return ChatStatus.PAUSED elif existing_chats.exists(): last_chat = existing_chats.last() - if last_chat.status == ChatStatus.PAUSED: + if last_chat and last_chat.status == ChatStatus.PAUSED: return ChatStatus.RESUME elif chat_session and chat_session.session_status == ChatStatus.COMPLETED: return ChatStatus.COMPLETED @@ -106,8 +122,25 @@ def update_last_chat_status(self, chat_status): if existing_chat.status != ChatStatus.COMPLETED: existing_chat.status = chat_status existing_chat.save() + + chat_session = ChatSession.objects.filter(session=self.session_id).first() + if chat_session and chat_session.session_status != ChatStatus.COMPLETED: + chat_session.session_status = chat_status + chat_session.save(update_fields=['session_status', 'updated_at']) except Exception as e: logger.info('Error in update_last_chat_status: %s', e, exc_info=True) async def update_last_chat_status_async(self, chat_status): await self.update_last_chat_status(chat_status) + + @database_sync_to_async + def update_session_status(self, chat_status): + if not hasattr(self, 'session_id') or not self.session_id: + return + try: + chat_session = ChatSession.objects.filter(session=self.session_id).first() + if chat_session and chat_session.session_status != ChatStatus.COMPLETED: + chat_session.session_status = chat_status + chat_session.save(update_fields=['session_status', 'updated_at']) + except Exception as e: + logger.info('Error in update_session_status: %s', e, exc_info=True) diff --git a/chatbot/consumers/async_consumer.py b/chatbot/consumers/async_consumer.py index 113409a..d03241f 100644 --- a/chatbot/consumers/async_consumer.py +++ b/chatbot/consumers/async_consumer.py @@ -66,9 +66,15 @@ async def receive(self, text_data): self.company_bot = await self.get_company_bot(profile, self.bot_route) # Create chat session asynchronously - await self.create_chat_session( + _, session_created = await self.create_chat_session( self.session_id, profile, self.company_bot, self.ip_address, user_id ) + + if not session_created: + company_chat_status = await self.determine_company_chat_status_async( + session_id=self.session_id, profile_id=self.profile_id, route=self.bot_route + ) + await self.update_session_status(chat_status=company_chat_status) else: # Validate that user is authenticated before processing messages if not self.session_id or not self.bot_route: @@ -129,6 +135,8 @@ async def receive(self, text_data): translated_message=translated_message, audio_base64=text_data_json.get('asr_audio'), stage=current_stage ) + if company_chat_status == ChatStatus.IN_PROGRESS: + await self.update_session_status(chat_status=company_chat_status) logger.info( f"channel_name: %s, session_id: %s, profile_id: %s, route: %s", @@ -230,7 +238,7 @@ def create_chat_session(self, session_id, profile, company_bot, ip_address, user cs.save(update_fields=["other_params"]) - return cs + return cs, cs_created @database_sync_to_async def translate_message(self, message): diff --git a/chatbot/models/enums.py b/chatbot/models/enums.py index 6872359..1acd67f 100644 --- a/chatbot/models/enums.py +++ b/chatbot/models/enums.py @@ -43,6 +43,7 @@ class LLMProvider(models.TextChoices): BEDROCK = 'bedrock', _('BEDROCK') OPENAI = 'openai', _('OPENAI') ANTHROPIC = 'anthropic', _('ANTHROPIC') + OPENROUTER = 'openrouter', _('Openrouter') class ThemeType(models.TextChoices): diff --git a/chatbot/serializer/profile_serializer.py b/chatbot/serializer/profile_serializer.py index 664230c..82aca04 100644 --- a/chatbot/serializer/profile_serializer.py +++ b/chatbot/serializer/profile_serializer.py @@ -32,7 +32,8 @@ class ProfileSerializer(serializers.ModelSerializer): class Meta: model = Profile - exclude = ['password', ] + fields = '__all__' + extra_kwargs = {'password': {'write_only': True, 'required': False}} def list(self, request, *args, **kwargs): print("GET request received") diff --git a/chatbot/services/core/prompt_builder.py b/chatbot/services/core/prompt_builder.py index 572d5db..de9b8d0 100644 --- a/chatbot/services/core/prompt_builder.py +++ b/chatbot/services/core/prompt_builder.py @@ -12,15 +12,19 @@ class PromptBuilder: @staticmethod def build_system_prompt(company_bot, state_machine=None, profile=None): - system_parts = [company_bot.context.strip()] + address = profile.profile_address.all().first() if profile else None + context_data = {"profile": profile, "address": address} + + system_parts = [PromptBuilder._render_template(company_bot.context, context_data)] if state_machine and state_machine.context: - system_parts.append(state_machine.context.strip()) + system_parts.append(PromptBuilder._render_template(state_machine.context, context_data)) if state_machine and state_machine.completion_criteria: - system_parts.append(f"Completion Criteria:\n{state_machine.completion_criteria.strip()}") + rendered = PromptBuilder._render_template(state_machine.completion_criteria, context_data) + system_parts.append(f"Completion Criteria:\n{rendered}") - rendered_tag_context = PromptBuilder._render_tag_context(company_bot, profile) + rendered_tag_context = PromptBuilder._render_tag_context(company_bot, context_data) if rendered_tag_context: system_parts.append(rendered_tag_context) @@ -28,31 +32,28 @@ def build_system_prompt(company_bot, state_machine=None, profile=None): if dynamic_context: system_parts.append(dynamic_context) - return "\n\n".join(system_parts) + final_prompt = "\n\n".join(system_parts) + return final_prompt @staticmethod - def _render_tag_context(company_bot, profile): - tag_context = company_bot.tag_context - if not tag_context or not tag_context.strip(): - return "" - + def _render_template(text, context_data): + if not text or not text.strip(): + return text or "" try: - address = None - if profile: - address = profile.profile_address.all().first() - - context_data = { - "profile": profile, - "address": address, - } - - return Template(tag_context).render(context_data).strip() + return Template(text).render(context_data).strip() except UndefinedError as e: - logger.warning("tag_context template variable missing: %s", e) - return tag_context.strip() + logger.info("template variable missing: %s", e) + return text.strip() except Exception as e: - logger.error("Failed to render tag_context: %s", e, exc_info=True) - return tag_context.strip() + logger.error("Failed to render template: %s", e, exc_info=True) + return text.strip() + + @staticmethod + def _render_tag_context(company_bot, context_data): + tag_context = company_bot.tag_context + if not tag_context or not tag_context.strip(): + return "" + return PromptBuilder._render_template(tag_context, context_data) @staticmethod diff --git a/chatbot/services/response_handlers/base_response_handler.py b/chatbot/services/response_handlers/base_response_handler.py index cdc8c7b..7632188 100644 --- a/chatbot/services/response_handlers/base_response_handler.py +++ b/chatbot/services/response_handlers/base_response_handler.py @@ -545,7 +545,7 @@ def _call_gateway_non_stream( params.pop('web_search_options', None) print(f'[non_stream] use_web_search={use_web_search} bot.enable_web_search={getattr(company_bot, "enable_web_search", "N/A")} web_search_in_params={"web_search_options" in params}') data = call_llm_gateway( - messages=gateway_messages, provider=company_bot.provider, model=company_bot.llm_model, + messages=gateway_messages, provider=company_bot.provider, model=self._get_effective_model(company_bot), params=params, tools=tools, tool_choice=tool_choice, ) logger.info(f"[gateway] raw response: {data}") @@ -657,7 +657,7 @@ def _handle_gateway_stream( citation_chunks = [] finish_chunk = None for delta_content, tool_use_delta, chunk_finish_reason, chunk_citations, chunk_finish_data in call_llm_gateway_stream( - messages=gateway_messages, provider=company_bot.provider, model=company_bot.llm_model, + messages=gateway_messages, provider=company_bot.provider, model=self._get_effective_model(company_bot), params=stream_params, tools=tools, tool_choice=tool_choice, cache_policy=cache_policy, metadata=metadata, ): @@ -842,6 +842,20 @@ def _parse_if_string(self, value, fallback): except Exception: return fallback + def _get_effective_model(self, company_bot): + """Return the model name to use for gateway calls. + + If other_params contains a 'custom_model' key, that value takes precedence + over the llm_model enum field — useful for OpenRouter or any provider that + uses model IDs not listed in LLMModel. + """ + custom = (company_bot.other_params or {}).get('custom_model') + if isinstance(custom, str): + custom = custom.strip() + if custom: + return custom + return company_bot.llm_model + def _with_turn_usage(self, response_data, extra, finish, turn_usage): """Return a (response, extra, finish) tuple with turn_usage injected into extra.""" if any(turn_usage.values()): diff --git a/chatbot/utils/audio_provider_utils.py b/chatbot/utils/audio_provider_utils.py index d0ff354..8dadf1d 100644 --- a/chatbot/utils/audio_provider_utils.py +++ b/chatbot/utils/audio_provider_utils.py @@ -63,6 +63,8 @@ def strip_markdown_for_tts(text: str) -> str: text = re.sub(r'_{1,2}([^\n_]*?)_{1,2}', r'\1', text) # Remaining lone asterisks / underscores not attached to word characters text = re.sub(r'(?