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
8 changes: 4 additions & 4 deletions chatbot/admin/company_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down
43 changes: 38 additions & 5 deletions chatbot/consumers/async_base_consumer.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
12 changes: 10 additions & 2 deletions chatbot/consumers/async_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions chatbot/models/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class LLMProvider(models.TextChoices):
BEDROCK = 'bedrock', _('BEDROCK')
OPENAI = 'openai', _('OPENAI')
ANTHROPIC = 'anthropic', _('ANTHROPIC')
OPENROUTER = 'openrouter', _('Openrouter')


class ThemeType(models.TextChoices):
Expand Down
3 changes: 2 additions & 1 deletion chatbot/serializer/profile_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
49 changes: 25 additions & 24 deletions chatbot/services/core/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,48 @@ 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)

dynamic_context = PromptBuilder._render_dynamic_context(company_bot)
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
Expand Down
18 changes: 16 additions & 2 deletions chatbot/services/response_handlers/base_response_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down Expand Up @@ -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,
):
Expand Down Expand Up @@ -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()):
Expand Down
2 changes: 2 additions & 0 deletions chatbot/utils/audio_provider_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'(?<!\w)[*_]+(?!\w)', '', text)
# "1: 30" is read as a time by TTS engines — replace colon with comma
text = re.sub(r'(?<!\d)(\d+)\s*:\s*(\d+)(?!\d)', r'\1, \2', text)
# Collapse 3+ consecutive newlines → 2
text = re.sub(r'\n{3,}', '\n\n', text)
# Collapse multiple spaces/tabs → single space
Expand Down
2 changes: 1 addition & 1 deletion chatbot/utils/media_preview/media_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ def create_docx_from_args(

if arguments.get('duration'):
doc.add_heading(lang_constants.get('timeline_label', 'Timeline'), level=2)
doc.add_paragraph(f"{lang_constants.get('duration_prefix', 'Duration')}: {arguments['duration']}")
doc.add_paragraph(arguments['duration'])

action_plan = arguments.get('action_plan') or []
if action_plan:
Expand Down