Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
14 changes: 12 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,16 @@ 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')
return custom if custom else company_bot.llm_model
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

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+):\s+(\d+)', r'\1, \2', text)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
# 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