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
4 changes: 2 additions & 2 deletions chatbot/celery_tasks/flow_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


@shared_task
def get_flow_response(channel_name, session_id, profile_id, route, bot_type, bot_route):
def get_flow_response(channel_name, session_id, profile_id, route, bot_type, bot_route, access_token=None):
"""Common bot task"""
print(f"bot_type is {bot_type} and bot_route is {bot_route}")
bot_strategy = BotServiceFactory.create_bot_service(
Expand All @@ -17,5 +17,5 @@ def get_flow_response(channel_name, session_id, profile_id, route, bot_type, bot
orchestrator = ChatOrchestrator(bot_strategy=bot_strategy)
return orchestrator.process_chat_request(
channel_name=channel_name, session_id=session_id, profile_id=profile_id,
language=route
language=route, access_token=access_token
)
30 changes: 18 additions & 12 deletions chatbot/celery_tasks/handle_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,30 @@ def _translate_chips(extra_content, voice_provider, route):

def translate_and_send_message(
accumulated_message, current_channel_name, current_step_number, finish_reason, route, company_bot,
extra_content=None
extra_content=None, is_bot_vernacular_message=False
):

if route != 'en' and accumulated_message and accumulated_message!= '':
# target_language_code = get_language_code_from_route(route)
logger.info(f"target_language_code date: %s", route)
voice_provider = Voice.objects.filter(
company_bot=company_bot, type=VoiceType.TextToText, language=route
).first()
logger.info("target_language_code: %s", route)

response = text_translate_provider(
voice_provider=voice_provider, message_body=accumulated_message, target_language=route,
source_language='en'
)
if response.get('status') == 200:
translated_messages = response.get('content')
else:
if is_bot_vernacular_message:
# Message is already in the target language — skip translation entirely.
translated_messages = accumulated_message
voice_provider = None
else:
voice_provider = Voice.objects.filter(
company_bot=company_bot, type=VoiceType.TextToText, language=route
).first()

response = text_translate_provider(
voice_provider=voice_provider, message_body=accumulated_message, target_language=route,
source_language='en'
)
if response.get('status') == 200:
translated_messages = response.get('content')
else:
translated_messages = accumulated_message

extra_content = _translate_chips(extra_content, voice_provider, route)

Expand Down
3 changes: 2 additions & 1 deletion chatbot/consumers/async_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ async def receive(self, text_data):
self.bot_route = text_data_json.get('bot_route')
self.flow_name = text_data_json.get('flow_name')
self.ip_address = text_data_json.get('address')
self.access_token = text_data_json.get('access_token')

profile = await self.get_profile(self.profile_id)
logger.info(
Expand Down Expand Up @@ -138,7 +139,7 @@ async def receive(self, text_data):
# Start the Celery task but don't wait for it
get_flow_response.delay(
self.channel_name, self.session_id, self.profile_id, self.route,
'common', self.bot_route
'common', self.bot_route, access_token=self.access_token
)

except Exception as e:
Expand Down
3 changes: 2 additions & 1 deletion chatbot/services/core/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def __init__(self, bot_strategy):
self.prompt_builder = PromptBuilder()
self.message_handler = MessageHandler()

def process_chat_request(self, channel_name, session_id, profile_id, language):
def process_chat_request(self, channel_name, session_id, profile_id, language, access_token=None):
"""Main processing method"""
try:
# Get session data
Expand Down Expand Up @@ -90,6 +90,7 @@ def process_chat_request(self, channel_name, session_id, profile_id, language):
'profile_id': profile_id,
'temp_messages': temp_messages,
'intro_mssg': intro_mssg,
'access_token': access_token,
}

# Add strategy-specific parameters
Expand Down
32 changes: 27 additions & 5 deletions chatbot/services/response_handlers/base_response_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from chatbot.celery_tasks.handle_message import translate_and_send_message
from chatbot.llm_models.llm_gateway import build_gateway_params, call_llm_gateway, call_llm_gateway_stream
from chatbot.models import ChatSession, ChatStatus, CompanyBotTypeChoices
from chatbot.models.bot_vernacular_model import BotVernacular
from chatbot.models.company_models import CompanyStateMachine
from chatbot.models.enums import OperationTypeChoices, PreProcessOutputMode
from chatbot.services.postprocessing.postprocessing_service import PostprocessingService
Expand All @@ -28,6 +29,16 @@ def __init__(self):
self._gateway_handled_tools = {'web_search'}
self._metadata_tools = {'respond_to_user'}

def get_error_message(self, company_bot, language):
"""Return (message, is_vernacular) — is_vernacular=True means message is already in target language."""
try:
vernacular = BotVernacular.objects.filter(company_bot=company_bot, language=language).first()
if vernacular and vernacular.error_message:
return vernacular.error_message, True
except Exception as e:
logger.error("Failed to fetch BotVernacular for language=%s: %s", language, e)
return self.default_error_message, False
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def _is_response_too_short(self, response):
"""
Check if response is too short (less than 3 words).
Expand Down Expand Up @@ -161,6 +172,9 @@ def handle_response(self, **kwargs):
if isinstance(extra_content, dict) and '_usage_cost' in extra_content:
kwargs['_usage_cost'] = extra_content.pop('_usage_cost')

if isinstance(extra_content, dict) and extra_content.pop('_is_vernacular_error', False):
kwargs['is_bot_vernacular_message'] = True

# Store extra_content if present for later use
if extra_content:
kwargs['llm_extra_content'] = extra_content
Expand All @@ -183,7 +197,9 @@ def handle_response(self, **kwargs):
}
}
else:
response = self.default_error_message
response, is_vernacular = self.get_error_message(company_bot, kwargs.get('language'))
if is_vernacular:
kwargs['is_bot_vernacular_message'] = True

if is_function_call and response is None:
response = early_return
Expand Down Expand Up @@ -323,6 +339,7 @@ def get_llm_response(self, **kwargs):
result = self._handle_gateway_response(
system_prompt=system_prompt, messages=message_to_send, company_bot=company_bot,
session_id=session_id, profile_id=profile_id, tools=tools, channel_name=channel_name,
language=kwargs.get('language'),
)

if result is None or (isinstance(result, tuple) and result[0] is None):
Expand All @@ -335,6 +352,7 @@ def get_llm_response(self, **kwargs):

def _handle_gateway_response(
self, system_prompt, messages, company_bot, session_id, profile_id, tools=None, channel_name=None,
language=None,
):
import json as _json

Expand Down Expand Up @@ -422,7 +440,8 @@ def _handle_gateway_response(
continue
if not response_data:
logger.info('[tool_loop] respond_to_user still empty after retry — sending default error')
return self.default_error_message, None, None
err_msg, is_vernacular = self.get_error_message(company_bot, language)
return err_msg, {'_is_vernacular_error': is_vernacular} if is_vernacular else None, None
return self._with_turn_usage(response_data, extra, finish, turn_usage)
# Web search held back for KB fallback. Only retry if LLM gave NO response at all —
# a non-empty answer is a deliberate choice (and streaming already sent those tokens).
Expand Down Expand Up @@ -488,7 +507,8 @@ def _handle_gateway_response(
append_to_last = True

logger.error('[tool_loop] max tool iterations reached')
return self.default_error_message, None, 'stop'
err_msg, is_vernacular = self.get_error_message(company_bot, language)
return err_msg, {'_is_vernacular_error': is_vernacular} if is_vernacular else None, 'stop'

def _execute_tool(self, tool_name, arguments, company_bot):
"""Execute a tool call and return (result_text_for_llm, retrieved_chunks)."""
Expand Down Expand Up @@ -1026,7 +1046,8 @@ def save_message(self, session_id, profile_id, message, chunks,
other_params=other_params
)

def translate_message(self, message, channel_name, step_number, language, company_bot, extra_content=None):
def translate_message(self, message, channel_name, step_number, language, company_bot, extra_content=None,
is_bot_vernacular_message=False):
"""Translate and send message"""
return translate_and_send_message(
accumulated_message=message,
Expand All @@ -1035,7 +1056,8 @@ def translate_message(self, message, channel_name, step_number, language, compan
finish_reason="stop",
route=language,
company_bot=company_bot,
extra_content=extra_content
extra_content=extra_content,
is_bot_vernacular_message=is_bot_vernacular_message,
)

def get_chat_status(self, state_machine, company_bot):
Expand Down
38 changes: 26 additions & 12 deletions chatbot/services/response_handlers/common_handler.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from chatbot.models import ChatStatus, CompanyChat, CompanyBotTypeChoices, LLMProvider, BotVernacular, Voice, VoiceType
from chatbot.models import ChatStatus, CompanyChat, CompanyBotTypeChoices, LLMProvider, Voice, VoiceType
from chatbot.models.company_models import CompanyStateMachine
from chatbot.services.response_handlers.base_response_handler import BaseResponseHandler
from chatbot.utils.shiksha_chaupal.date_utils import handle_date_prompt
Expand Down Expand Up @@ -52,8 +52,9 @@ def _handle_event_state(self, chat_session, state_machine, **kwargs):
other_info=other_info
)
print("DATE RES: ", bot_question)
is_vernacular_error = False
if bot_question is None:
bot_question = self.default_error_message
bot_question, is_vernacular_error = self.get_error_message(company_bot, language)

if bot_question == '':
return {
Expand All @@ -67,7 +68,7 @@ def _handle_event_state(self, chat_session, state_machine, **kwargs):
else:
translated_message = self.translate_message(
message=bot_question, channel_name=channel_name, step_number=chat_session.current_step,
language=language, company_bot=company_bot
language=language, company_bot=company_bot, is_bot_vernacular_message=is_vernacular_error
)

stage = state_machine.name if state_machine else None
Expand Down Expand Up @@ -104,7 +105,8 @@ def _send_db_question(self, bot_question, chat_session, chunks, **kwargs):
)
except CompanyStateMachine.DoesNotExist:
logger.error(f"State machine not found for step {chat_session.current_step}")
return self.default_error_message
err_msg, _ = self.get_error_message(company_bot, language)
return err_msg

chat_status = self.get_chat_status(
state_machine=state_machine, company_bot=company_bot
Expand Down Expand Up @@ -322,13 +324,12 @@ def process_response(self, response, chat_session, chunks, streaming_completed=F
forward_kwargs['skip_next_stage_preprocessing'] = kwargs.get('skip_next_stage_preprocessing', False)

if kwargs.get('use_error_message', False) and not is_function_call:
bot_vernacular = BotVernacular.objects.filter(company_bot=company_bot, language=language).first()
error_message = bot_vernacular.error_message if (
bot_vernacular and bot_vernacular.error_message) else "Please try again!"
error_message, is_vernacular = self.get_error_message(company_bot, language)
logger.info(f"Using error message: {error_message}")
print(f"DEBUG: Using error message: {error_message}")
expected_output_response = error_message
response = error_message
kwargs['is_bot_vernacular_message'] = is_vernacular

# Handle function calls for STATE_MACHINE bots
if is_function_call and company_bot and company_bot.bot_type == CompanyBotTypeChoices.STATE_MACHINE:
Expand Down Expand Up @@ -711,7 +712,8 @@ def _handle_regular_response(self, response, chat_session, company_bot,

translated_message = self.translate_message(
message=response, channel_name=channel_name, step_number=current_step,
language=language, company_bot=company_bot, extra_content=extra_content
language=language, company_bot=company_bot, extra_content=extra_content,
is_bot_vernacular_message=kwargs.get('is_bot_vernacular_message', False),
)

other_params = {}
Expand Down Expand Up @@ -816,7 +818,7 @@ def _handle_profile_tool_response(self, response, chat_session, chunks, **kwargs

profile_id = kwargs.get('profile_id')
if profile_id:
self._save_submitted_user_context(profile_id, arguments)
self._save_submitted_user_context(profile_id, arguments, access_token=kwargs.get('access_token'))

llm_extra_content = kwargs.get('llm_extra_content') or {}
llm_extra_content['profile'] = arguments
Expand All @@ -831,7 +833,7 @@ def _handle_profile_tool_response(self, response, chat_session, chunks, **kwargs
**kwargs
)

def _save_submitted_user_context(self, profile_id, arguments):
def _save_submitted_user_context(self, profile_id, arguments, access_token=None):
"""Persist submit_user_context arguments to Profile and ProfileAddress."""
from chatbot.models.profile_models import Profile
from chatbot.models.geo_models import ProfileAddress
Expand Down Expand Up @@ -872,6 +874,17 @@ def _save_submitted_user_context(self, profile_id, arguments):
address.save(update_fields=addr_fields)
logger.info(f'[submit_user_context] {"created" if created else "updated"} ProfileAddress for profile id={profile_id} fields={addr_fields}')

if access_token:
from chatbot.utils.elevate.profile_utils import update_elevate_profile
update_elevate_profile(
access_token=access_token,
name=arguments.get('name'),
role=arguments.get('role'),
school_name=arguments.get('school_name'),
district=district,
state=state,
)

except Exception as e:
logger.error(f'[submit_user_context] failed to save profile context: {e}', exc_info=True)

Expand Down Expand Up @@ -1060,7 +1073,7 @@ def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwarg
download['file_name'] = display_filename

if not download:
bot_message = self.default_error_message
bot_message, _ = self.get_error_message(company_bot, language)
extra_content_to_send = {}
else:
bot_message = arguments.get('bot_message', f"Your file '{filename}' is ready to download.")
Expand Down Expand Up @@ -1098,4 +1111,5 @@ def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwarg
return bot_message
else:
logger.warning(f"Unknown function call: {function_name}")
return self.default_error_message
err_msg, _ = self.get_error_message(company_bot, language)
return err_msg
55 changes: 55 additions & 0 deletions chatbot/utils/audio_provider_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re

from chatbot.models import VoiceProvider, LanguageMapping, Voice, VoiceType, CompanyBot
from chatbot.translate.ai4Bharat.speech_to_text import transcribe_ai4bharat_multiple_chunks
from chatbot.translate.ai4Bharat.text_to_speech import ai4bharat_text_speech
Expand All @@ -18,6 +20,57 @@
logger = logging.getLogger('django')


def strip_markdown_for_tts(text: str) -> str:
"""Strip markdown formatting so TTS engines receive clean natural-language text."""
if text is None:
return ""
if not text:
return text
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Fenced code blocks — remove entirely (before anything else to avoid inner matches)
text = re.sub(r'```[\s\S]*?```', '', text)
# Inline code — remove
text = re.sub(r'`[^`\n]+`', '', text)
# Images first — must precede links or [alt](url) gets consumed leaving a stray !
text = re.sub(r'!\[([^\]]*)\]\([^)]*\)', r'\1', text)
# Links — keep display text, drop URL
text = re.sub(r'\[([^\]]*)\]\([^)]*\)', r'\1', text)
# Horizontal rules: standalone line of 3+ hyphens / underscores / asterisks
text = re.sub(r'^\s*[-_*]{3,}\s*$', '', text, flags=re.MULTILINE)
# Multiple consecutive hyphens used as em/en dash
text = re.sub(r'-{2,}', ' ', text)
# Table separator rows (|---|:---|)
text = re.sub(r'^\|[\s\-|:]+\|$', '', text, flags=re.MULTILINE)
# Table cell pipes → space
text = re.sub(r'\|', ' ', text)
# Heading markers (# / ## / ### etc.)
text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE)
# Blockquote markers
text = re.sub(r'^>\s*', '', text, flags=re.MULTILINE)
# HTML line breaks → period so TTS pauses between items (LLM uses <br> inside table cells)
text = re.sub(r'\s*<br\s*/?>\s*', '. ', text, flags=re.IGNORECASE)
# Any remaining HTML tags → remove
text = re.sub(r'<[^>]+>', '', text)
# Unicode bullet character • → remove (period from <br> already provides the pause)
text = re.sub(r'•\s*', '', text)
# Clean up double periods that arise when text before <br> already ended with punctuation
text = re.sub(r'([.!?])\s*\.\s*', r'\1 ', text)
# Bullet list markers BEFORE bold/italic — prevents * bullet + **bold** from being misread as nested *{1,3}
text = re.sub(r'^[ \t]*[-*]\s+', '', text, flags=re.MULTILINE)
# Bold + italic with asterisks: ***text*** / **text** / *text* (single-line, non-nested)
text = re.sub(r'\*{1,3}([^\n*]*?)\*{1,3}', r'\1', text)
# Bold + italic with underscores: __text__ / _text_ (single-line, non-nested)
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)
# Collapse 3+ consecutive newlines → 2
text = re.sub(r'\n{3,}', '\n\n', text)
# Collapse multiple spaces/tabs → single space
text = re.sub(r'[ \t]{2,}', ' ', text)

return text.strip()


def get_voice_provider(company_bot, voice_type, source_language=None, target_language=None):
"""Return appropriate Voice provider preferring non-English language."""

Expand All @@ -44,6 +97,8 @@ def get_voice_provider(company_bot, voice_type, source_language=None, target_lan


def text_speech_provider(company_bot, text, source_language):
text = strip_markdown_for_tts(text)
logger.info("TTS strip text: %s", text)
voice_provider = get_voice_provider(
company_bot=company_bot, voice_type=VoiceType.TextToSpeech, source_language=source_language
)
Expand Down
Loading