Skip to content
4 changes: 2 additions & 2 deletions chatbot/admin/company_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,14 +517,14 @@ class FlowAdmin(SimpleHistoryAdmin):
search_fields = ('flow_name', 'flow_route', 'bot__name')
date_hierarchy = 'created_at'
ordering = ('-created_at',)
raw_id_fields = ('bot', 'story_bot', 'parent_flow', 'image_config', 'story_validation_bot')
raw_id_fields = ('bot', 'title_bot', 'story_bot', 'parent_flow', 'image_config', 'story_validation_bot')

fieldsets = (
('Basic Information', {
'fields': ('flow_name', 'flow_route', 'languages')
}),
('Bot Configuration', {
'fields': ('bot', 'story_bot', 'story_validation_bot'),
'fields': ('bot', 'title_bot', 'story_bot', 'story_validation_bot'),
'description': 'Configure the bots associated with this flow.'
}),
('Flow Settings', {
Expand Down
29 changes: 29 additions & 0 deletions chatbot/celery_tasks/handle_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,33 @@
logger = logging.getLogger('django')


def _translate_chips(extra_content, voice_provider, route):
if not extra_content or not voice_provider:
return extra_content
chips = extra_content.get('quick_reply_chips')
if not chips:
return extra_content
translated = []
for chip in chips:
if not isinstance(chip, str):
translated.append(chip)
continue
try:
resp = text_translate_provider(
voice_provider=voice_provider, message_body=chip,
target_language=route, source_language='en'
)
if resp.get('status') == 200:
translated.append(resp.get('content') or chip)
else:
logger.error('[_translate_chips] chip translation failed status=%s — using original', resp.get('status'))
translated.append(chip)
except Exception as e:
logger.error('[_translate_chips] chip translation exception: %s — using original', e)
translated.append(chip)
return {**extra_content, 'quick_reply_chips': translated}


def translate_and_send_message(
accumulated_message, current_channel_name, current_step_number, finish_reason, route, company_bot,
extra_content=None
Expand All @@ -30,6 +57,8 @@ def translate_and_send_message(
else:
translated_messages = accumulated_message

extra_content = _translate_chips(extra_content, voice_provider, route)

async_to_sync(channel_layer.send)(
current_channel_name,
{
Expand Down
126 changes: 126 additions & 0 deletions chatbot/celery_tasks/title_tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from celery import shared_task
from chatbot.models import ChatSession, CompanyChat, Voice, VoiceType
from chatbot.models.company_models import Flow
from chatbot.llm_models.llm_gateway import call_llm_gateway, build_gateway_params
from chatbot.utils.chat_utils import get_guided_chat
from chatbot.utils.audio_provider_utils import text_translate_provider
import json_repair
import logging

logger = logging.getLogger('django')


def _track_usage(session_id, response):
try:
usage = response.get('usage', {}) or {}
cost = response.get('cost', {}) or {}
usage_cost = {
'input_tokens': usage.get('input_tokens', 0) or 0,
'output_tokens': usage.get('output_tokens', 0) or 0,
'total_tokens': usage.get('total_tokens', 0) or 0,
'cost_usd': cost.get('computed_usd', 0) or 0,
}
if not any(usage_cost.values()):
return
session = ChatSession.objects.get(session=session_id)
other_params = session.other_params or {}
totals = other_params.get('usage', {})
logger.info("[usage] title call session %s before update: %s | this call: %s", session_id, totals, usage_cost)
totals['total_input_tokens'] = totals.get('total_input_tokens', 0) + usage_cost['input_tokens']
totals['total_output_tokens'] = totals.get('total_output_tokens', 0) + usage_cost['output_tokens']
totals['total_tokens'] = totals.get('total_tokens', 0) + usage_cost['total_tokens']
totals['total_cost_usd'] = round(totals.get('total_cost_usd', 0) + usage_cost['cost_usd'], 6)
other_params['usage'] = totals
session.other_params = other_params
session.save(update_fields=['other_params'])
logger.info("[usage] title call session %s after update: %s", session_id, totals)
except Exception as e:
logger.error("[usage] failed to track title usage for session %s: %s", session_id, e)


@shared_task
def generate_session_title(session_id, language='en'):
session = ChatSession.objects.filter(session=session_id).first()
if not session or session.title:
return

flow = Flow.objects.filter(bot=session.company_bot).first()
if not flow or not flow.title_bot:
logger.info("No title bot configured for session %s", session_id)
return

company_bot = flow.title_bot
logger.info("Generating title for session %s using bot %s", session_id, company_bot.id)

company_chats = (
CompanyChat.objects
.select_related('sender', 'receiver')
.filter(session=session_id)
.order_by('created_at')
.values("receiver", "receiver__id", "translated_message", "message", "status", "created_at")
)
messages = get_guided_chat(company_bot=company_bot, company_chats=company_chats)

tools = company_bot.tool_context
if tools and isinstance(tools, str):
tools = json_repair.repair_json(tools, return_objects=True)

tool_choice = None
if isinstance(tools, dict):
tool_choice = tools.get('tool_choice', 'auto')
tools = tools.get('tools') or tools.get('tool')
elif isinstance(tools, list):
tool_choice = 'auto'

system_msg = {'role': 'system', 'content': company_bot.context}
response = call_llm_gateway(
messages=[system_msg] + list(messages),
provider=company_bot.provider,
model=company_bot.llm_model,
params=build_gateway_params(company_bot),
tools=tools or None,
tool_choice=tool_choice,
)

if not response:
logger.error("LLM gateway returned no response for title generation, session %s", session_id)
return

_track_usage(session_id, response)

try:
import json as _json
choice = response.get('choices', [{}])[0]
message = choice.get('message', {})
tool_calls = message.get('tool_calls') or []
title_tc = next(
(tc for tc in tool_calls if tc.get('function', {}).get('name') == 'generate_title'),
None,
)
if not title_tc:
logger.error("generate_title tool call missing in response for session %s", session_id)
return
raw_args = title_tc.get('function', {}).get('arguments', '{}')
arguments = _json.loads(raw_args) if isinstance(raw_args, str) else raw_args
output_title = arguments.get('title')
except Exception as e:
logger.error("Error extracting title for session %s: %s", session_id, e)
return

if not output_title:
logger.error("No title value in generate_title tool call for session %s", session_id)
return

if language != 'en':
voice_provider = Voice.objects.filter(
company_bot=company_bot, type=VoiceType.TextToText, language=language
).first()
translated = text_translate_provider(
voice_provider=voice_provider, message_body=output_title, target_language=language,
source_language='en'
)
if translated.get('status') == 200:
output_title = translated.get('content')
Comment thread
KUNALTEMPEST marked this conversation as resolved.

session.save_title(output_title)
logger.info("Title saved for session %s: %s", session_id, output_title)
15 changes: 5 additions & 10 deletions chatbot/consumers/async_base_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,11 @@ async def chat_message(self, event):

@database_sync_to_async
def save_chat_session(self, session_id):
chat_session = ChatSession.objects.filter(session=session_id)
if chat_session.exists():
c = chat_session[0]
else:
c = ChatSession(session=session_id)

if hasattr(self, 'route'):
c.save_title(self.route)
else:
c.save_title()
from chatbot.celery_tasks.title_tasks import generate_session_title
session = ChatSession.objects.filter(session=session_id).first()
if session and not session.title:
language = getattr(self, 'route', 'en') or 'en'
generate_session_title.delay(session_id, language)

@database_sync_to_async
def determine_company_chat_status(self, session_id, profile_id, route, is_disconnected=False):
Expand Down
4 changes: 2 additions & 2 deletions chatbot/consumers/async_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,9 @@ def translate_message(self, message):
if not chat_session:
return message

state_machine = CompanyStateMachine.objects.get(
state_machine = CompanyStateMachine.objects.filter(
company_bot=self.company_bot, step=chat_session.current_step
)
).first()

if state_machine and state_machine.text_conversion_type == TextConversionType.TRANSLITERATE:
transliterate_voice_provider = Voice.objects.filter(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 5.2 on 2026-06-03 17:10

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('chatbot', '0084_companybot_enable_web_search_and_more'),
]

operations = [
migrations.AddField(
model_name='flow',
name='title_bot',
field=models.ForeignKey(blank=True, help_text='Optional bot for session title generation.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='title_flows', to='chatbot.companybot'),
),
migrations.AddField(
model_name='historicalflow',
name='title_bot',
field=models.ForeignKey(blank=True, db_constraint=False, help_text='Optional bot for session title generation.', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='chatbot.companybot'),
),
migrations.AlterField(
model_name='mediaimage',
name='media_type',
field=models.CharField(blank=True, choices=[('application/pdf', 'PDF'), ('text/plain', 'TXT'), ('text/csv', 'CSV'), ('image/jpeg', 'JPEG'), ('image/png', 'PNG'), ('image/svg+xml', 'SVG'), ('image/webp', 'WEBP'), ('image/heif', 'HEIF'), ('image/heic', 'HEIC'), ('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'XLSX'), ('application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'DOCX')], max_length=100, null=True),
),
migrations.AlterField(
model_name='storymedia',
name='media_type',
field=models.CharField(blank=True, choices=[('application/pdf', 'PDF'), ('text/plain', 'TXT'), ('text/csv', 'CSV'), ('image/jpeg', 'JPEG'), ('image/png', 'PNG'), ('image/svg+xml', 'SVG'), ('image/webp', 'WEBP'), ('image/heif', 'HEIF'), ('image/heic', 'HEIC'), ('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'XLSX'), ('application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'DOCX')], max_length=100, null=True),
),
]
96 changes: 5 additions & 91 deletions chatbot/models/chat_models.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
from django.db import models
from chatbot.models import CompanyChat, Profile, CompanyBot, ChatStatus, LLMModel, Voice, VoiceType, LLMProvider, \
ChatType, StoryLanguageChoices
from chatbot.llm_models.llm_script import handle_bedrock_model, handle_openai_model
from chatbot.utils.audio_provider_utils import text_translate_provider
import json_repair

from chatbot.utils.chat_utils import get_guided_chat
from chatbot.models import Profile, CompanyBot, ChatStatus, StoryLanguageChoices


class ChatSession(models.Model):
"""
Represents an active chat session between a user profile and a company bot.
Stores session metadata, conversation state, and handles title generation using LLMs.
"""

session = models.CharField(max_length=255, unique=True)
Expand All @@ -32,86 +25,7 @@ class ChatSession(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def save_title(self, language='en'):
company_chats = CompanyChat.objects.select_related('sender', 'receiver').filter(session=self.session).order_by('created_at').values("receiver", "receiver__id", "translated_message", "message", "status", "created_at")
if self.profile:
company_bot = CompanyBot.objects.filter(company=self.profile.company, route='/mohini_title').first()
else:
company_bot = CompanyBot.objects.filter(route='/mohini_title').first()

if not company_bot:
return

messages = get_guided_chat(
company_bot=company_bot, company_chats=company_chats
)
prompt = self._get_prompt(company_bot=company_bot)

json_output = self._handle_llm_model(
prompt=prompt, messages=messages, company_bot=company_bot
)
try:
if isinstance(json_output, str):
json_output = json_repair.repair_json(json_output, return_objects=True)
output_title = json_output.get('title')
except Exception as e:
print("Error: ", e)
output_title = 'MI Story'
if language != 'en':
voice_provider = Voice.objects.filter(
company_bot=company_bot, type=VoiceType.TextToText, language=language
).first()

response = text_translate_provider(
voice_provider=voice_provider, message_body=output_title, target_language=language,
source_language='en'
)
if response.get('status') == 200:
output_title = response.get('content')

self.title = output_title
self.save()

def _get_prompt(self, company_bot):
prompt = company_bot.context
if company_bot.provider == LLMProvider.BEDROCK_CONVERSE:
return [{'text': prompt}]
elif company_bot.provider == LLMProvider.OPENAI:
return [
{
'role': 'system',
'content': prompt
},
]

def _handle_llm_model(self, prompt, messages, company_bot):
response_json = None
if company_bot.provider == LLMProvider.BEDROCK_CONVERSE:
tool = company_bot.tool_context
if tool and isinstance(tool, str):
tool = json_repair.repair_json(tool, return_objects=True)
response_json = handle_bedrock_model(
system_prompt=prompt, messages=messages, model_name=company_bot.llm_model,
temperature=company_bot.bot_temperature, max_token=company_bot.max_token,
tools=tool, company_bot=company_bot
)
elif company_bot.provider == LLMProvider.OPENAI:
response_json = handle_openai_model(
system_prompt=prompt, messages=messages, model_name=company_bot.llm_model,
temperature=company_bot.bot_temperature, max_token=company_bot.max_token
)

if response_json and isinstance(response_json, dict):
if response_json.get('parameters'):
response_json = response_json.get('parameters')
elif response_json.get('input'):
response_json = response_json.get('input')
return response_json

def _parse_response(self, response):
response_str = str(response.content, encoding="utf-8")
response_json = json_repair.repair_json(response_str, return_objects=True)
response_content = response_json['choices'][0]['message']['content']
cleaned_content = (response_content.replace('\n', '').replace('\t', '').replace('\r', '')
.replace('\\n', '').replace('\\t', '').replace('\\r', ''))
return json_repair.repair_json(cleaned_content, return_objects=True)
def save_title(self, title):
if not self.title:
self.title = title
self.save(update_fields=['title'])
Comment thread
KUNALTEMPEST marked this conversation as resolved.
8 changes: 8 additions & 0 deletions chatbot/models/company_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,14 @@ class Flow(models.Model):
related_name='story_validation_flows',
help_text="Optional secondary bot for story-related functionality."
)
title_bot = models.ForeignKey(
CompanyBot,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='title_flows',
help_text="Optional bot for session title generation."
)
websocket_url = models.CharField(
max_length=500,
help_text="WebSocket path for real-time communication (e.g., ws/common). Do not include protocol or host.",
Expand Down
2 changes: 1 addition & 1 deletion chatbot/models/geo_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ class ProfileAddress(models.Model):
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
return self.profile.first_name
return self.profile.first_name or str(self.profile.email)
Comment thread
KUNALTEMPEST marked this conversation as resolved.
Loading