From bb89d3c39101de2d5fa1d86b80bfb8887ee85bdb Mon Sep 17 00:00:00 2001 From: kunalpratapsingh Date: Thu, 4 Jun 2026 00:25:03 +0530 Subject: [PATCH 01/12] Feedback update --- chatbot/admin/company_admin.py | 4 +- chatbot/celery_tasks/title_tasks.py | 126 ++++++++++++++++++ chatbot/consumers/async_base_consumer.py | 15 +-- ...e_bot_historicalflow_title_bot_and_more.py | 34 +++++ chatbot/models/chat_models.py | 96 +------------ chatbot/models/company_models.py | 8 ++ .../response_handlers/common_handler.py | 8 ++ chatbot/utils/media_preview/media_creation.py | 32 ++++- shikshalokam_mohini/celery_config.py | 3 +- 9 files changed, 221 insertions(+), 105 deletions(-) create mode 100644 chatbot/celery_tasks/title_tasks.py create mode 100644 chatbot/migrations/0085_flow_title_bot_historicalflow_title_bot_and_more.py diff --git a/chatbot/admin/company_admin.py b/chatbot/admin/company_admin.py index e902df8..4aea919 100644 --- a/chatbot/admin/company_admin.py +++ b/chatbot/admin/company_admin.py @@ -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', { diff --git a/chatbot/celery_tasks/title_tasks.py b/chatbot/celery_tasks/title_tasks.py new file mode 100644 index 0000000..94d05ab --- /dev/null +++ b/chatbot/celery_tasks/title_tasks.py @@ -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') + + session.save_title(output_title) + logger.info("Title saved for session %s: %s", session_id, output_title) \ No newline at end of file diff --git a/chatbot/consumers/async_base_consumer.py b/chatbot/consumers/async_base_consumer.py index 6a33fb3..1629205 100644 --- a/chatbot/consumers/async_base_consumer.py +++ b/chatbot/consumers/async_base_consumer.py @@ -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): diff --git a/chatbot/migrations/0085_flow_title_bot_historicalflow_title_bot_and_more.py b/chatbot/migrations/0085_flow_title_bot_historicalflow_title_bot_and_more.py new file mode 100644 index 0000000..3d832ec --- /dev/null +++ b/chatbot/migrations/0085_flow_title_bot_historicalflow_title_bot_and_more.py @@ -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), + ), + ] diff --git a/chatbot/models/chat_models.py b/chatbot/models/chat_models.py index 3d494d0..67bb133 100644 --- a/chatbot/models/chat_models.py +++ b/chatbot/models/chat_models.py @@ -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) @@ -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']) \ No newline at end of file diff --git a/chatbot/models/company_models.py b/chatbot/models/company_models.py index 5de9e4f..f989826 100644 --- a/chatbot/models/company_models.py +++ b/chatbot/models/company_models.py @@ -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.", diff --git a/chatbot/services/response_handlers/common_handler.py b/chatbot/services/response_handlers/common_handler.py index 73b4ef0..37c8257 100644 --- a/chatbot/services/response_handlers/common_handler.py +++ b/chatbot/services/response_handlers/common_handler.py @@ -6,6 +6,7 @@ from chatbot.celery_tasks.handle_message import translate_and_send_message import logging import json +import os from json_repair import repair_json logger = logging.getLogger('django') @@ -853,6 +854,7 @@ def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwarg company_bot_id=company_bot.id, session_id=session_id, sources=finalized_sources, + flow_name=flow_name, ) pdf_url = pdf_result.get('media_url') if pdf_result.get('success') else None @@ -872,6 +874,7 @@ def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwarg docx_result = create_docx_from_args( arguments=arguments, company_bot_id=company_bot.id, session_id=session_id, sources=finalized_sources, + flow_name=flow_name, ) pdf_url = pdf_result.get('media_url') if pdf_result.get('success') else None docx_url = docx_result.get('media_url') if docx_result.get('success') else None @@ -879,11 +882,16 @@ def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwarg logger.info(f'[download_file] pdf_url={pdf_url} docx_url={docx_url}') + _raw_filename = pdf_result.get('file_name') or docx_result.get('file_name') + display_filename = os.path.splitext(_raw_filename)[0] if _raw_filename else None + download = {} if pdf_url: download['pdf_url'] = pdf_url if docx_url: download['docx_url'] = docx_url + if display_filename: + download['file_name'] = display_filename if not download: bot_message = self.default_error_message diff --git a/chatbot/utils/media_preview/media_creation.py b/chatbot/utils/media_preview/media_creation.py index 8a56f7f..89d7e28 100644 --- a/chatbot/utils/media_preview/media_creation.py +++ b/chatbot/utils/media_preview/media_creation.py @@ -269,6 +269,34 @@ def sanitize_filename(filename: str, extension: str = '.pdf') -> str: return f"download{ext}" +def _get_flow_title(flow_name: str, fallback: str = 'Document') -> str: + """Return the title stored in PDFTemplates.constants_json['title'] for the given flow_route.""" + try: + from chatbot.models.company_models import Flow, PDFTemplates + if not flow_name: + logger.info('[_get_flow_title] flow_name is None — using fallback') + return fallback + flow = Flow.objects.filter(flow_route=flow_name).first() + if not flow: + logger.info(f'[_get_flow_title] no Flow found for flow_route={flow_name!r} — using fallback') + return fallback + pdf_template = PDFTemplates.objects.filter(flow=flow).first() + if not pdf_template: + logger.info(f'[_get_flow_title] no PDFTemplates found for flow={flow} — using fallback') + return fallback + title = (pdf_template.constants_json or {}).get('doc_title') + if title: + logger.info(f'[_get_flow_title] using constants_json doc_title={title!r}') + return title + # Fall back to template_name if no title key in constants_json + if pdf_template.template_name: + logger.info(f'[_get_flow_title] no title in constants_json, using template_name={pdf_template.template_name!r}') + return pdf_template.template_name + except Exception as e: + logger.error(f'[_get_flow_title] error for flow_name={flow_name!r}: {e}') + return fallback + + def render_template_to_pdf( *, flow_name: str, @@ -349,6 +377,7 @@ def create_docx_from_args( company_bot_id: int, session_id: str, sources: list = None, + flow_name: str = None, ) -> dict: """ Generate a DOCX file directly from download_file tool call arguments (no template model). @@ -361,11 +390,12 @@ def create_docx_from_args( try: is_mip = bool(arguments.get('goal') or arguments.get('action_plan')) safe_filename = sanitize_filename(arguments.get('filename', 'download.docx'), '.docx') + title_text = _get_flow_title(flow_name, fallback=arguments.get('title', 'Document')) doc = docx.Document() if is_mip: - doc.add_heading('School Improvement Plan', level=1) + doc.add_heading(title_text, level=1) if arguments.get('goal'): doc.add_heading('Goal', level=2) diff --git a/shikshalokam_mohini/celery_config.py b/shikshalokam_mohini/celery_config.py index 03c1a5f..724ed58 100644 --- a/shikshalokam_mohini/celery_config.py +++ b/shikshalokam_mohini/celery_config.py @@ -30,5 +30,6 @@ 'chatbot.celery_tasks.knowledge_service.media_tasks', 'chatbot.celery_tasks.flow_tasks', 'chatbot.celery_tasks.free_flow_tasks', - 'chatbot.celery_tasks.post_processing_tasks' + 'chatbot.celery_tasks.post_processing_tasks', + 'chatbot.celery_tasks.title_tasks' ]) From 6df84a1f95bdd3325dbe63e45d3958088b8710cd Mon Sep 17 00:00:00 2001 From: kunalpratapsingh Date: Thu, 4 Jun 2026 01:54:24 +0530 Subject: [PATCH 02/12] Added profile bot changes --- .../response_handlers/common_handler.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/chatbot/services/response_handlers/common_handler.py b/chatbot/services/response_handlers/common_handler.py index 37c8257..183577d 100644 --- a/chatbot/services/response_handlers/common_handler.py +++ b/chatbot/services/response_handlers/common_handler.py @@ -223,12 +223,15 @@ def process_response(self, response, chat_session, chunks, streaming_completed=F # Route non-state-machine function calls to the appropriate handler. _freeflow_action_tools = {'download_file'} _json_message_tools = {'process_user_input', 'respond_to_user'} + _profile_tools = {'submit_user_context'} if isinstance(response, dict) and 'function_call' in response: fc_name = response.get('function_call', {}).get('name', '') if fc_name in _freeflow_action_tools: return self._handle_freeflow_function_call(response, chat_session, chunks, **kwargs) elif fc_name in _json_message_tools: return self._handle_json_tool_response(response, chat_session, chunks, **kwargs) + elif fc_name in _profile_tools: + return self._handle_profile_tool_response(response, chat_session, chunks, **kwargs) retry_attempt = kwargs.get('retry_attempt', 0) print(f"DEBUG: Current retry attempt: {retry_attempt}") @@ -796,6 +799,70 @@ def _handle_json_tool_response(self, response, chat_session, chunks, **kwargs): **kwargs ) + def _handle_profile_tool_response(self, response, chat_session, chunks, **kwargs): + """Handle submit_user_context — persist extracted context to Profile/ProfileAddress and inject into extra_content.""" + arguments = response['function_call'].get('arguments', {}) + if isinstance(arguments, str): + try: + arguments = json.loads(arguments) + except json.JSONDecodeError: + arguments = repair_json(arguments, return_objects=True) + + profile_id = kwargs.get('profile_id') + if profile_id: + self._save_submitted_user_context(profile_id, arguments) + + llm_extra_content = kwargs.get('llm_extra_content') or {} + llm_extra_content['profile'] = arguments + llm_extra_content['profile_extracted'] = True + kwargs['llm_extra_content'] = llm_extra_content + + return self._handle_regular_response( + response='', + chat_session=chat_session, + chunks=chunks, + current_step=chat_session.current_step, + **kwargs + ) + + def _save_submitted_user_context(self, profile_id, arguments): + """Persist submit_user_context arguments to Profile and ProfileAddress.""" + from chatbot.models.profile_models import Profile + from chatbot.models.geo_models import ProfileAddress + try: + profile = Profile.objects.filter(id=profile_id).first() + if not profile: + logger.info(f'[submit_user_context] profile not found for id={profile_id}') + return + + update_fields = [] + if arguments.get('role'): + profile.designation = arguments['role'] + update_fields.append('designation') + if arguments.get('school_name'): + profile.org_associated = arguments['school_name'] + update_fields.append('org_associated') + if update_fields: + profile.save(update_fields=update_fields) + logger.info(f'[submit_user_context] updated Profile id={profile_id} fields={update_fields}') + + district = arguments.get('district') + state = arguments.get('state') + if district or state: + address, created = ProfileAddress.objects.get_or_create(profile=profile) + addr_fields = [] + if district: + address.district = district + addr_fields.append('district') + if state: + address.state = state + addr_fields.append('state') + 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}') + + except Exception as e: + logger.error(f'[submit_user_context] failed to save profile context: {e}', exc_info=True) + def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwargs): """Handle function calls for FREE_FLOW bots (like download_file)""" From 51779ef0d6eeb9aefec4ff53d586bdd59fdff5ab Mon Sep 17 00:00:00 2001 From: kunalpratapsingh Date: Thu, 4 Jun 2026 02:01:38 +0530 Subject: [PATCH 03/12] profile admin fix --- chatbot/models/geo_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatbot/models/geo_models.py b/chatbot/models/geo_models.py index 97f1814..ad4e7d8 100644 --- a/chatbot/models/geo_models.py +++ b/chatbot/models/geo_models.py @@ -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) From cd588e1609ce4550de6f194d1c976c8f5aa3900a Mon Sep 17 00:00:00 2001 From: kunalpratapsingh Date: Thu, 4 Jun 2026 02:44:10 +0530 Subject: [PATCH 04/12] profile save name --- chatbot/services/response_handlers/common_handler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chatbot/services/response_handlers/common_handler.py b/chatbot/services/response_handlers/common_handler.py index 183577d..42690a5 100644 --- a/chatbot/services/response_handlers/common_handler.py +++ b/chatbot/services/response_handlers/common_handler.py @@ -836,6 +836,9 @@ def _save_submitted_user_context(self, profile_id, arguments): return update_fields = [] + if arguments.get('name'): + profile.first_name = arguments['name'] + update_fields.append('first_name') if arguments.get('role'): profile.designation = arguments['role'] update_fields.append('designation') From d772ce912300bacb7ffe045b86147984cab3dd86 Mon Sep 17 00:00:00 2001 From: kunalpratapsingh Date: Fri, 5 Jun 2026 19:23:25 +0530 Subject: [PATCH 05/12] Profile API + TNC patch api --- chatbot/urls.py | 2 + chatbot/views/api_views.py | 133 +++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/chatbot/urls.py b/chatbot/urls.py index 257aedb..e71423e 100644 --- a/chatbot/urls.py +++ b/chatbot/urls.py @@ -41,6 +41,8 @@ urlpatterns = [ path('api/profile/', api_views.post_profile), + path('api/get-profile/', api_views.get_profile_view, name='get-profile'), + path('api/accept-tnc/', api_views.accept_tnc_view, name='accept-tnc'), path('api/user_profile/', ProfileListCreateView.as_view(), name='profile-list-create'), path('api/generate-session/', api_views.generate_session_id, name='generate_session_id'), diff --git a/chatbot/views/api_views.py b/chatbot/views/api_views.py index cc6657b..adf4944 100644 --- a/chatbot/views/api_views.py +++ b/chatbot/views/api_views.py @@ -185,3 +185,136 @@ def logout(request): }, status=500) +@api_view(['GET']) +def get_profile_view(request): + try: + profile_id = request.query_params.get('profile_id') + email = request.query_params.get('email') + company_slug = request.query_params.get('company_slug') + + if not profile_id and not email: + return Response({ + 'status': 'error', + 'message': 'profile_id or email is required' + }, status=400) + + if profile_id: + profile = Profile.objects.get(pk=profile_id) + else: + if company_slug: + company = Company.objects.get(slug=company_slug) + else: + company = Company.objects.order_by('id').first() + if not company: + return Response({ + 'status': 'error', + 'message': 'No company found' + }, status=404) + profile = Profile.objects.get(email=email, company=company) + + is_tnc_accepted = bool( + profile.other_params and profile.other_params.get('is_tnc_accepted', False) + ) + + has_address = ProfileAddress.objects.filter( + profile=profile, + state__isnull=False, + district__isnull=False, + ).exclude(state='').exclude(district='').exists() + + is_profile_complete = bool( + profile.first_name and + profile.designation and + profile.org_associated and + has_address + ) + + return Response({ + 'id': profile.id, + 'first_name': profile.first_name, + 'last_name': profile.last_name, + 'email': profile.email, + 'phone': profile.phone, + 'designation': profile.designation, + 'org_associated': profile.org_associated, + 'gender': profile.gender, + 'location': profile.location, + 'preferred_route': profile.preferred_route, + 'is_tnc_accepted': is_tnc_accepted, + 'is_profile_complete': is_profile_complete, + }, status=200) + + except Profile.DoesNotExist: + return Response({ + 'status': 'error', + 'message': 'Profile not found' + }, status=404) + + except Company.DoesNotExist: + return Response({ + 'status': 'error', + 'message': 'Company not found' + }, status=404) + + except Exception as e: + traceback.print_exc() + return Response({ + 'status': 'error', + 'message': str(e) + }, status=500) + + +@api_view(['PATCH']) +def accept_tnc_view(request): + try: + profile_id = request.data.get('profile_id') + email = request.data.get('email') + company_slug = request.data.get('company_slug') + + if not profile_id and not email: + return Response({ + 'status': 'error', + 'message': 'profile_id or email is required' + }, status=400) + + if profile_id: + profile = Profile.objects.get(pk=profile_id) + else: + if company_slug: + company = Company.objects.get(slug=company_slug) + else: + company = Company.objects.order_by('id').first() + if not company: + return Response({ + 'status': 'error', + 'message': 'No company found' + }, status=404) + profile = Profile.objects.get(email=email, company=company) + other_params = profile.other_params or {} + other_params['is_tnc_accepted'] = True + profile.other_params = other_params + profile.save(update_fields=['other_params', 'updated_at']) + + return Response({ + 'status': 'ok', + 'is_tnc_accepted': True, + }, status=200) + + except Profile.DoesNotExist: + return Response({ + 'status': 'error', + 'message': 'Profile not found' + }, status=404) + + except Company.DoesNotExist: + return Response({ + 'status': 'error', + 'message': 'Company not found' + }, status=404) + + except Exception as e: + traceback.print_exc() + return Response({ + 'status': 'error', + 'message': str(e) + }, status=500) From ab6625aaf7cfaf02a1fd0c41111ed176e7dc90b8 Mon Sep 17 00:00:00 2001 From: kunalpratapsingh Date: Tue, 9 Jun 2026 12:03:26 +0530 Subject: [PATCH 06/12] saathi backend work --- shikshalokam_mohini/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shikshalokam_mohini/settings.py b/shikshalokam_mohini/settings.py index 63e8a37..1c80441 100644 --- a/shikshalokam_mohini/settings.py +++ b/shikshalokam_mohini/settings.py @@ -27,7 +27,7 @@ def load_secrets(): paths_to_try = [ - '/home/ubuntu/shikshalokam-mohini-service/config/secrets.json', + '/home/ubuntu/saathi-backend/config/secrets.json', os.path.join(CODE_BASE_DIR, "config/secrets.json"), os.path.join(os.getcwd(), "config/secrets.json") ] From 0b6ad5503d57834e66bb7222d16e9ef08a038489 Mon Sep 17 00:00:00 2001 From: kunalpratapsingh Date: Tue, 9 Jun 2026 14:51:32 +0530 Subject: [PATCH 07/12] PDF translation issue update --- chatbot/celery_tasks/handle_message.py | 29 ++++++++ chatbot/consumers/async_consumer.py | 4 +- .../response_handlers/common_handler.py | 72 ++++++++++++++++++- chatbot/utils/media_preview/media_creation.py | 49 ++++++++++--- 4 files changed, 139 insertions(+), 15 deletions(-) diff --git a/chatbot/celery_tasks/handle_message.py b/chatbot/celery_tasks/handle_message.py index fa9766b..e92bbfc 100644 --- a/chatbot/celery_tasks/handle_message.py +++ b/chatbot/celery_tasks/handle_message.py @@ -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 @@ -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, { diff --git a/chatbot/consumers/async_consumer.py b/chatbot/consumers/async_consumer.py index 6a83ae6..d0ef859 100644 --- a/chatbot/consumers/async_consumer.py +++ b/chatbot/consumers/async_consumer.py @@ -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( diff --git a/chatbot/services/response_handlers/common_handler.py b/chatbot/services/response_handlers/common_handler.py index 42690a5..49a36b6 100644 --- a/chatbot/services/response_handlers/common_handler.py +++ b/chatbot/services/response_handlers/common_handler.py @@ -1,9 +1,10 @@ -from chatbot.models import ChatStatus, CompanyChat, CompanyBotTypeChoices, LLMProvider, BotVernacular +from chatbot.models import ChatStatus, CompanyChat, CompanyBotTypeChoices, LLMProvider, BotVernacular, 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 from chatbot.celery_tasks.common_chat_tasks import save_in_company_db from chatbot.celery_tasks.handle_message import translate_and_send_message +from chatbot.utils.audio_provider_utils import text_translate_provider import logging import json import os @@ -866,6 +867,67 @@ def _save_submitted_user_context(self, profile_id, arguments): except Exception as e: logger.error(f'[submit_user_context] failed to save profile context: {e}', exc_info=True) + def _translate_download_arguments(self, arguments: dict, language: str, company_bot) -> dict: + if language == 'en': + return arguments + + voice_provider = Voice.objects.filter( + company_bot=company_bot, type=VoiceType.TextToText, language=language + ).first() + if not voice_provider: + logger.info('[_translate_download_arguments] no TextToText voice provider for language=%s — skipping translation', language) + return arguments + + def _translate(text, context=''): + try: + resp = text_translate_provider( + voice_provider=voice_provider, message_body=text, + target_language=language, source_language='en' + ) + if resp.get('status') == 200: + return resp.get('content') or text + logger.error( + '[_translate_download_arguments] translation failed%s status=%s — falling back to original', + f' ({context})' if context else '', resp.get('status') + ) + except Exception as e: + logger.error( + '[_translate_download_arguments] translation exception%s: %s — falling back to original', + f' ({context})' if context else '', e, exc_info=True + ) + return text + + SKIP_KEYS = {'filename'} + translated = {} + + for key, value in arguments.items(): + if key in SKIP_KEYS: + translated[key] = value + elif isinstance(value, str): + translated[key] = _translate(value, context=key) + elif isinstance(value, list): + translated_list = [] + for i, item in enumerate(value): + if isinstance(item, str): + translated_list.append(_translate(item, context=f'{key}[{i}]')) + elif isinstance(item, dict): + translated_item = {} + for k, v in item.items(): + if isinstance(v, str): + translated_item[k] = _translate(v, context=f'{key}[{i}].{k}') + else: + translated_item[k] = v + translated_list.append(translated_item) + else: + translated_list.append(item) + translated[key] = translated_list + elif isinstance(value, (int, float)): + translated[key] = value + else: + translated[key] = value + + return translated + def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwargs): """Handle function calls for FREE_FLOW bots (like download_file)""" @@ -912,12 +974,16 @@ def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwarg logger.info(f'[download_file] flow_name={flow_name!r} filename={filename!r} sources={len(finalized_sources)}') + # Translate LLM-generated content fields to the user's language + arguments = self._translate_download_arguments(arguments, language, company_bot) + pdf_result = render_template_to_pdf( flow_name=flow_name, arguments=arguments, company_bot_id=company_bot.id, session_id=session_id, sources=finalized_sources, + language=language, ) docx_result = create_docx_from_args( arguments=arguments, @@ -925,6 +991,7 @@ def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwarg session_id=session_id, sources=finalized_sources, flow_name=flow_name, + language=language, ) pdf_url = pdf_result.get('media_url') if pdf_result.get('success') else None @@ -940,11 +1007,12 @@ def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwarg pdf_result = render_template_to_pdf( flow_name=flow_name, arguments=arguments, company_bot_id=company_bot.id, session_id=session_id, sources=finalized_sources, + language=language, ) docx_result = create_docx_from_args( arguments=arguments, company_bot_id=company_bot.id, session_id=session_id, sources=finalized_sources, - flow_name=flow_name, + flow_name=flow_name, language=language, ) pdf_url = pdf_result.get('media_url') if pdf_result.get('success') else None docx_url = docx_result.get('media_url') if docx_result.get('success') else None diff --git a/chatbot/utils/media_preview/media_creation.py b/chatbot/utils/media_preview/media_creation.py index 89d7e28..ef046c9 100644 --- a/chatbot/utils/media_preview/media_creation.py +++ b/chatbot/utils/media_preview/media_creation.py @@ -304,6 +304,7 @@ def render_template_to_pdf( company_bot_id: int, session_id: str, sources: list = None, + language: str = 'en', ) -> dict: """ Look up the PDFTemplate for the flow (by flow_route), render it with Jinja2, @@ -339,9 +340,18 @@ def render_template_to_pdf( chat_session = ChatSession.objects.filter(session=session_id).first() profile = chat_session.profile if chat_session else None + _all_constants = pdf_template.constants_json or {} + _lang_constants = _all_constants.get(language) or _all_constants.get('en') or {} + _template_constants = dict(_lang_constants) + for k, v in list(_lang_constants.items()): + if k.endswith('_label'): + _template_constants.setdefault(k[:-6], v) + elif k.endswith('_prefix'): + _template_constants.setdefault(k[:-7], v) context = { 'args': arguments, - 'constants': pdf_template.constants_json or {}, + 'constants': _template_constants, + 'language': language, 'profile': profile, 'sources': sources or arguments.get('sources') or [], } @@ -378,6 +388,7 @@ def create_docx_from_args( session_id: str, sources: list = None, flow_name: str = None, + language: str = 'en', ) -> dict: """ Generate a DOCX file directly from download_file tool call arguments (no template model). @@ -387,10 +398,26 @@ def create_docx_from_args( import docx from docx.enum.text import WD_ALIGN_PARAGRAPH + lang_constants = {} + try: + from chatbot.models.company_models import Flow, PDFTemplates + if flow_name: + _flow = Flow.objects.filter(flow_route=flow_name).first() + if _flow: + _pdf_template = PDFTemplates.objects.filter(flow=_flow).first() + if _pdf_template and _pdf_template.constants_json: + lang_constants = ( + _pdf_template.constants_json.get(language) + or _pdf_template.constants_json.get('en') + or {} + ) + except Exception as _e: + logger.error(f'[create_docx_from_args] failed to load lang_constants: {_e}') + try: is_mip = bool(arguments.get('goal') or arguments.get('action_plan')) safe_filename = sanitize_filename(arguments.get('filename', 'download.docx'), '.docx') - title_text = _get_flow_title(flow_name, fallback=arguments.get('title', 'Document')) + title_text = lang_constants.get('doc_title') or _get_flow_title(flow_name, fallback=arguments.get('title', 'Document')) doc = docx.Document() @@ -398,27 +425,27 @@ def create_docx_from_args( doc.add_heading(title_text, level=1) if arguments.get('goal'): - doc.add_heading('Goal', level=2) + doc.add_heading(lang_constants.get('goal_label', 'Goal'), level=2) doc.add_paragraph(arguments['goal']) if arguments.get('objective'): - doc.add_heading('Objective', level=2) + doc.add_heading(lang_constants.get('objective_label', 'Objective'), level=2) doc.add_paragraph(arguments['objective']) if arguments.get('duration'): - doc.add_heading('Timeline', level=2) - doc.add_paragraph(f"Duration: {arguments['duration']}") + doc.add_heading(lang_constants.get('timeline_label', 'Timeline'), level=2) + doc.add_paragraph(f"{lang_constants.get('duration_prefix', 'Duration')}: {arguments['duration']}") action_plan = arguments.get('action_plan') or [] if action_plan: from docx.shared import Inches - doc.add_heading('Action plan', level=2) + doc.add_heading(lang_constants.get('action_plan_label', 'Action plan'), level=2) table = doc.add_table(rows=1, cols=3) table.style = 'Table Grid' header_cells = table.rows[0].cells header_cells[0].text = '#' - header_cells[1].text = 'Action' - header_cells[2].text = 'Week' + header_cells[1].text = lang_constants.get('action_col_label', 'Action') + header_cells[2].text = lang_constants.get('week_label', 'Week') table.columns[0].width = Inches(0.4) table.columns[1].width = Inches(5.0) table.columns[2].width = Inches(1.1) @@ -430,7 +457,7 @@ def create_docx_from_args( success_indicators = arguments.get('success_indicators') or [] if success_indicators: - doc.add_heading('Success indicators', level=2) + doc.add_heading(lang_constants.get('success_indicators_label', 'Success indicators'), level=2) for i, indicator in enumerate(success_indicators): doc.add_paragraph(f"{i + 1}. {indicator}") @@ -444,7 +471,7 @@ def create_docx_from_args( resolved_sources = sources or arguments.get('sources') or [] if resolved_sources: - doc.add_heading('References', level=2) + doc.add_heading(lang_constants.get('references_label', 'References'), level=2) for src in resolved_sources: src_title = src.get('title', '') src_url = src.get('url', '') From ff0a6999babd2d11846b0d161fa54570a940264f Mon Sep 17 00:00:00 2001 From: kunalpratapsingh Date: Tue, 9 Jun 2026 15:37:26 +0530 Subject: [PATCH 08/12] PDF translation issue update --- .../response_handlers/common_handler.py | 24 +++++++++++++++++-- chatbot/utils/media_preview/media_creation.py | 16 ++++++------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/chatbot/services/response_handlers/common_handler.py b/chatbot/services/response_handlers/common_handler.py index 49a36b6..fba1e60 100644 --- a/chatbot/services/response_handlers/common_handler.py +++ b/chatbot/services/response_handlers/common_handler.py @@ -977,6 +977,25 @@ def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwarg # Translate LLM-generated content fields to the user's language arguments = self._translate_download_arguments(arguments, language, company_bot) + filename_base = os.path.splitext(filename)[0] + translated_filename = filename_base + if language != 'en': + try: + filename_voice_provider = Voice.objects.filter( + company_bot=company_bot, type=VoiceType.TextToText, language=language + ).first() + if filename_voice_provider: + filename_translation_response = text_translate_provider( + voice_provider=filename_voice_provider, message_body=filename_base, + target_language=language, source_language='en' + ) + if filename_translation_response.get('status') == 200: + translated_filename = filename_translation_response.get('content') or filename_base + else: + logger.error('[download_file] filename translation failed status=%s — using original', filename_translation_response.get('status')) + except Exception as e: + logger.error('[download_file] filename translation exception: %s — using original', e) + pdf_result = render_template_to_pdf( flow_name=flow_name, arguments=arguments, @@ -984,6 +1003,7 @@ def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwarg session_id=session_id, sources=finalized_sources, language=language, + display_filename=translated_filename, ) docx_result = create_docx_from_args( arguments=arguments, @@ -1007,7 +1027,7 @@ def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwarg pdf_result = render_template_to_pdf( flow_name=flow_name, arguments=arguments, company_bot_id=company_bot.id, session_id=session_id, sources=finalized_sources, - language=language, + language=language, display_filename=translated_filename, ) docx_result = create_docx_from_args( arguments=arguments, @@ -1021,7 +1041,7 @@ def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwarg logger.info(f'[download_file] pdf_url={pdf_url} docx_url={docx_url}') _raw_filename = pdf_result.get('file_name') or docx_result.get('file_name') - display_filename = os.path.splitext(_raw_filename)[0] if _raw_filename else None + display_filename = translated_filename if _raw_filename else None download = {} if pdf_url: diff --git a/chatbot/utils/media_preview/media_creation.py b/chatbot/utils/media_preview/media_creation.py index ef046c9..66c8c3d 100644 --- a/chatbot/utils/media_preview/media_creation.py +++ b/chatbot/utils/media_preview/media_creation.py @@ -305,6 +305,7 @@ def render_template_to_pdf( session_id: str, sources: list = None, language: str = 'en', + display_filename: str = None, ) -> dict: """ Look up the PDFTemplate for the flow (by flow_route), render it with Jinja2, @@ -342,15 +343,14 @@ def render_template_to_pdf( _all_constants = pdf_template.constants_json or {} _lang_constants = _all_constants.get(language) or _all_constants.get('en') or {} - _template_constants = dict(_lang_constants) - for k, v in list(_lang_constants.items()): - if k.endswith('_label'): - _template_constants.setdefault(k[:-6], v) - elif k.endswith('_prefix'): - _template_constants.setdefault(k[:-7], v) + + template_args = dict(arguments) + if display_filename: + template_args['filename'] = display_filename + context = { - 'args': arguments, - 'constants': _template_constants, + 'args': template_args, + 'constants': _lang_constants, 'language': language, 'profile': profile, 'sources': sources or arguments.get('sources') or [], From 7ed7633b92cc7b81bf90d05539791a7447d7fe9e Mon Sep 17 00:00:00 2001 From: kunalpratapsingh Date: Tue, 9 Jun 2026 16:41:34 +0530 Subject: [PATCH 09/12] Onboarding issue, new logic, now once interacted with bot sets bool to true --- .../services/response_handlers/common_handler.py | 9 ++++++--- chatbot/views/api_views.py | 14 +++----------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/chatbot/services/response_handlers/common_handler.py b/chatbot/services/response_handlers/common_handler.py index fba1e60..21bcfdd 100644 --- a/chatbot/services/response_handlers/common_handler.py +++ b/chatbot/services/response_handlers/common_handler.py @@ -846,9 +846,12 @@ def _save_submitted_user_context(self, profile_id, arguments): if arguments.get('school_name'): profile.org_associated = arguments['school_name'] update_fields.append('org_associated') - if update_fields: - profile.save(update_fields=update_fields) - logger.info(f'[submit_user_context] updated Profile id={profile_id} fields={update_fields}') + other_params = profile.other_params or {} + other_params['is_onboarding_completed'] = True + profile.other_params = other_params + update_fields.append('other_params') + profile.save(update_fields=update_fields) + logger.info(f'[submit_user_context] updated Profile id={profile_id} fields={update_fields}') district = arguments.get('district') state = arguments.get('state') diff --git a/chatbot/views/api_views.py b/chatbot/views/api_views.py index adf4944..7858b8a 100644 --- a/chatbot/views/api_views.py +++ b/chatbot/views/api_views.py @@ -216,18 +216,10 @@ def get_profile_view(request): profile.other_params and profile.other_params.get('is_tnc_accepted', False) ) - has_address = ProfileAddress.objects.filter( - profile=profile, - state__isnull=False, - district__isnull=False, - ).exclude(state='').exclude(district='').exists() - - is_profile_complete = bool( - profile.first_name and - profile.designation and - profile.org_associated and - has_address + is_onboarding_completed = bool( + profile.other_params and profile.other_params.get('is_onboarding_completed', False) ) + is_profile_complete = is_onboarding_completed return Response({ 'id': profile.id, From 70195561f07553d7bd983eea58e4ed788bbc5d59 Mon Sep 17 00:00:00 2001 From: kunalpratapsingh Date: Tue, 9 Jun 2026 17:17:27 +0530 Subject: [PATCH 10/12] fixed issue where elevate profile api replaced the profile booleans --- chatbot/utils/elevate/profile_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/chatbot/utils/elevate/profile_utils.py b/chatbot/utils/elevate/profile_utils.py index d4a5e6d..af4c1d5 100644 --- a/chatbot/utils/elevate/profile_utils.py +++ b/chatbot/utils/elevate/profile_utils.py @@ -72,11 +72,14 @@ def handle_elevate_profile(access_token): 'latest_flow_used': SessionFlowName.LoginMiStory, 'location': user_data.get('location'), 'designation': designation_value, - 'other_params': {'elevate_profile_details': user_data}, 'source': 'elevate', 'preferred_route': language, } ) + existing_other_params = profile.other_params or {} + existing_other_params['elevate_profile_details'] = user_data + profile.other_params = existing_other_params + profile.save(update_fields=['other_params']) state = user_data.get('state', {}) district = user_data.get('district', {}) From 1d81d6f7deafd88c8dd8fabc3f1afe37635d113b Mon Sep 17 00:00:00 2001 From: kunalpratapsingh Date: Wed, 10 Jun 2026 11:20:44 +0530 Subject: [PATCH 11/12] saving sources and download in chats to preserver for chat history --- chatbot/services/response_handlers/common_handler.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/chatbot/services/response_handlers/common_handler.py b/chatbot/services/response_handlers/common_handler.py index 21bcfdd..b76021c 100644 --- a/chatbot/services/response_handlers/common_handler.py +++ b/chatbot/services/response_handlers/common_handler.py @@ -719,6 +719,11 @@ def _handle_regular_response(self, response, chat_session, company_bot, other_params['reason'] = reason print(f"DEBUG: Adding reason to other_params: {reason}") + if extra_content: + public_extra = {k: v for k, v in extra_content.items() if not k.startswith('_')} + if public_extra: + other_params['extra_content'] = public_extra + stage = state_machine.name if state_machine else None if response and str(response).strip(): message_to_save = response @@ -1087,12 +1092,7 @@ def _handle_freeflow_function_call(self, response, chat_session, chunks, **kwarg status=ChatStatus.IN_PROGRESS, translated_message=translated_message, stage=None, - other_params={ - 'function_call': function_name, - 'arguments': arguments, - 'pdf_url': pdf_url, - 'docx_url': docx_url, - }, + other_params={'extra_content': extra_content_to_send} if extra_content_to_send else None, ) return bot_message From e307ab50218874fd775482d09f1dec5a5c79299a Mon Sep 17 00:00:00 2001 From: kunalpratapsingh Date: Wed, 10 Jun 2026 15:10:29 +0530 Subject: [PATCH 12/12] Updated at ordering feature added --- chatbot/views/drf_views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/chatbot/views/drf_views.py b/chatbot/views/drf_views.py index f920a24..83817fa 100644 --- a/chatbot/views/drf_views.py +++ b/chatbot/views/drf_views.py @@ -1,5 +1,6 @@ import django_filters from rest_framework import generics +from rest_framework.filters import OrderingFilter from rest_framework.response import Response from rest_framework import status from chatbot.filter.drf_filter import ChatSessionProfileFilter @@ -65,8 +66,10 @@ class ProfileRetrieveUpdateDestroyView(generics.RetrieveUpdateAPIView): class ChatSessionListCreateView(generics.ListCreateAPIView): queryset = ChatSession.objects.all() serializer_class = ChatSessionSerializer - filter_backends = [django_filters.rest_framework.DjangoFilterBackend, ChatSessionProfileFilter] + filter_backends = [django_filters.rest_framework.DjangoFilterBackend, ChatSessionProfileFilter, OrderingFilter] filterset_fields = ['session', 'project_id', 'user_id', 'profile', 'session_type'] + ordering_fields = ['updated_at', 'created_at', 'id'] + ordering = ['-updated_at'] class ChatSessionRetrieveUpdateDestroyView(generics.RetrieveUpdateAPIView):