Skip to content
Open
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
31 changes: 26 additions & 5 deletions chatbot/llm_models/llm_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@
llm_retry_number = int(os.getenv('LLM_RETRY_NUMBER'))


def get_custom_model(company_bot):
"""Return company_bot.other_params['custom_model'] if set, else None."""
if not company_bot:
return None
other_params = company_bot.get('other_params') if isinstance(company_bot, dict) else getattr(
company_bot, 'other_params', None
)
if isinstance(other_params, str):
other_params = json.loads(other_params)
return other_params.get('custom_model') if other_params else None
Comment on lines +29 to +38

@coderabbitai coderabbitai Bot Aug 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle malformed other_params before parsing it.

json.loads at Line 37 can raise json.JSONDecodeError. A valid JSON list, string, or number can then reach Line 38 without a .get() method. The Bedrock and Responses API handlers call this helper before their try blocks. A malformed optional setting can therefore abort the request instead of using the normal error path.

Catch parse errors, require an object, and return None or a controlled configuration error.

Proposed fix
 def get_custom_model(company_bot):
     """Return company_bot.other_params['custom_model'] if set, else None."""
     if not company_bot:
         return None
     other_params = company_bot.get('other_params') if isinstance(company_bot, dict) else getattr(
         company_bot, 'other_params', None
     )
     if isinstance(other_params, str):
-        other_params = json.loads(other_params)
-    return other_params.get('custom_model') if other_params else None
+        try:
+            other_params = json.loads(other_params)
+        except json.JSONDecodeError:
+            return None
+    if not isinstance(other_params, dict):
+        return None
+    custom_model = other_params.get('custom_model')
+    if not isinstance(custom_model, str):
+        return None
+    return custom_model.strip() or None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_custom_model(company_bot):
"""Return company_bot.other_params['custom_model'] if set, else None."""
if not company_bot:
return None
other_params = company_bot.get('other_params') if isinstance(company_bot, dict) else getattr(
company_bot, 'other_params', None
)
if isinstance(other_params, str):
other_params = json.loads(other_params)
return other_params.get('custom_model') if other_params else None
def get_custom_model(company_bot):
"""Return company_bot.other_params['custom_model'] if set, else None."""
if not company_bot:
return None
other_params = company_bot.get('other_params') if isinstance(company_bot, dict) else getattr(
company_bot, 'other_params', None
)
if isinstance(other_params, str):
try:
other_params = json.loads(other_params)
except json.JSONDecodeError:
return None
if not isinstance(other_params, dict):
return None
custom_model = other_params.get('custom_model')
if not isinstance(custom_model, str):
return None
return custom_model.strip() or None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@chatbot/llm_models/llm_script.py` around lines 29 - 38, Update
get_custom_model to safely handle malformed or non-object other_params: catch
JSON decoding failures, validate that parsed values are mappings before calling
.get(), and return None (or the established controlled configuration error) for
invalid optional settings so callers remain on their normal error path.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's okay. Don't want to touch old code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VishnuKrishnathu, acknowledged. I will leave this change out of PR #44.

The malformed other_params case remains a follow-up risk in the existing code. If you later want to address it, I can help create a separate issue.

🐇︵‍💫

You are interacting with an AI system.



def handle_llama_model(
messages, max_token, model_name=None, is_json_format=True, temperature=None, top_p=None, seed=None, n=None,
stream=False, url_to_use=None
Expand Down Expand Up @@ -101,7 +113,10 @@ def handle_openai_model(
else:
client = OpenAI(api_key=client_api_key)

if model_name:
custom_model = get_custom_model(company_bot)
if custom_model:
model_to_use = custom_model
elif model_name:
Comment on lines +116 to +119

@coderabbitai coderabbitai Bot Aug 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 5 \
  'custom_model|LLMProvider|company_bot\.llm_model|handle_openai_model|handle_bedrock_model|handle_openai_response_api' \
  --glob '*.py' .

Repository: ELEVATE-Project/mitra-service

Length of output: 50388


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== llm_script outline =="
ast-grep outline chatbot/llm_models/llm_script.py --view expanded | sed -n '1,220p'

echo
echo "== llm_script relevant lines =="
sed -n '1,180p' chatbot/llm_models/llm_script.py | cat -n
sed -n '240,280p' chatbot/llm_models/llm_script.py | cat -n
sed -n '620,660p' chatbot/llm_models/llm_script.py | cat -n

echo
echo "== focused provider/model usages in llm_script.py =="
rg -n -C 3 'def get_custom_model|get_custom_model|LLMProvider|model_to_use|model_name|provider|model_id|modelId|invoke_model|chat completions|OpenAI' chatbot/llm_models/llm_script.py

Repository: ELEVATE-Project/mitra-service

Length of output: 24350


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== LLMModel enum and references =="
rg -n "class LLMModel|LLMModel\." chatbot/models chatbot --glob '*.py' | sed -n '1,240p'

echo
echo "== get_custom_model read-only semantic probe =="
python3 - <<'PY'
class CompanyBotLike:
    def __init__(self, other_params=None):
        self.other_params = other_params

def get_custom_model(company_bot):
    other_params = company_bot.get('other_params') if isinstance(company_bot, dict) else getattr(
        company_bot, 'other_params', None
    )
    if isinstance(other_params, str):
        import json
        other_params = json.loads(other_params)
    return other_params.get('custom_model') if other_params else None

samples = [
    {'other_params': {'custom_model': 'anthropic.claude-sonnet-4-20250514-v1:0'}},
    {'other_params': '{"custom_model":"anthropic.claude-sonnet-4-20250514-v1:0"}'},
    type('Obj', (), {'other_params': {'custom_model': 'gpt-4.1-mini'}}),
]
for company_bot in samples:
    model_name = 'gsm8k-template'
    company_bot_llm_model = 'llama3-1-8b'
    custom_model = get_custom_model(company_bot)
    open_ai_model_to_use = custom_model or model_name or company_bot_llm_model
    bedrock_model_id = custom_model or model_name or None
    print({
        'input_type': type(company_bot).__name__,
        'provider_agnostic_model_value': custom_model,
        'fallback_to_caller_model_name': model_name,
        'open_ai_openai_model': open_ai_model_to_use,
        'bedrock_model_id': bedrock_model_id,
    })
PY

Repository: ELEVATE-Project/mitra-service

Length of output: 2080


Validate the resolved model against the selected provider.

get_custom_model can return a Bedrock model ID, but handle_openai_model and handle_openai_response_api pass it directly as model to OpenAI. handle_bedrock_model has the same provider-neutral override behavior. Check other_params['custom_model'] and company_bot.llm_model before these handlers assign model/modelId.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@chatbot/llm_models/llm_script.py` around lines 116 - 119, Validate the custom
model selected in the model-resolution flow before assigning it to
provider-specific handlers. In the logic around get_custom_model,
handle_openai_model, handle_openai_response_api, and handle_bedrock_model, check
other_params['custom_model'] and company_bot.llm_model so only a model
compatible with the selected provider is passed as model or modelId; otherwise
continue using the provider-appropriate configured model.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's okay. Don't want to touch old code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VishnuKrishnathu, acknowledged. I will leave this provider-validation change out of PR #44.

🐇︵︵

You are interacting with an AI system.

model_to_use = model_name
elif company_bot:
model_to_use = company_bot.llm_model
Expand All @@ -121,7 +136,7 @@ def handle_openai_model(
LLMModel.GPT5_2,
}
if max_token:
if company_bot.llm_model in token_limit_models:
if model_to_use in token_limit_models:
request_data["max_completion_tokens"] = max_token
else:
request_data["max_tokens"]= max_token
Expand All @@ -135,7 +150,7 @@ def handle_openai_model(
request_data["tools"]= tools
if tool_choice:
request_data["tool_choice"]= tool_choice
if top_p is not None and company_bot.llm_model not in token_limit_models:
if top_p is not None and model_to_use not in token_limit_models:
request_data['top_p'] = top_p
print("request_data: ", request_data)
response = client.chat.completions.create(**request_data)
Expand Down Expand Up @@ -243,7 +258,10 @@ def handle_bedrock_model(
config=boto_config
)
print("aws_key used: ", aws_key if aws_key else AWS_KEY)
if model_name:
custom_model = get_custom_model(company_bot)
if custom_model:
model_id = custom_model
elif model_name:
Comment on lines +261 to +264

@coderabbitai coderabbitai Bot Aug 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 5 \
  'model_pricing|llama3-3-70b|claude-haiku-4-5-20251001-v1:0|claude-sonnet-4-5-20250929-v1:0' \
  --glob '*.py' \
  --glob '*.json' \
  --glob '*.yaml' \
  --glob '*.yml' .

Repository: ELEVATE-Project/mitra-service

Length of output: 34328


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "Relevant llm_script sections:"
sed -n '250,350p' chatbot/llm_models/llm_script.py
echo "---"
rg -n "get_pricing_from_company_bot|handle_bedrock_model|model_pricing|LLAMA_3_3_70B_INSTRUCT|CLAUDE" chatbot/observability chatbot --glob '*.py' | head -200

Repository: ELEVATE-Project/mitra-service

Length of output: 26416


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "Pricing exact-miss fallback paths:"
sed -n '179,222p' chatbot/llm_models/llm_script.py
echo "--- guest script duplicate"
sed -n '332,370p' chatbot/scripts/guest_discussion/onetime_script.py
echo "--- get_custom_model references"
rg -n "def get_custom_model|get_custom_model\\(" chatbot --glob '*.py'
echo "--- model_pricing keys in tracked data files"
rg -n -C 3 "model_pricing|llama3-3-70b|us\.anthropic\.claude-haiku-4-5|us\.anthropic\.claude-sonnet-4-5|claude-haiku-4-5|claude-sonnet-4-5" \
  --glob '*.py' --glob '*.json' --glob '*.yaml' --glob '*.yml' --glob '*.md' --glob '*.txt' . | head -250

Repository: ELEVATE-Project/mitra-service

Length of output: 32109


Do not fall back to Llama pricing for unmatched Bedrock models.

get_pricing_from_company_bot uses pricing_data.get(model_id) and then looks up pricing_data.get('llama3-3-70b'). New Claude IDs such as us.anthropic.claude-haiku-4-5-20251001-v1:0 and us.anthropic.claude-sonnet-4-5-20250929-v1:0 can therefore log incorrect costs if exact pricing entries are missing.

Add exact pricing keys for each supported model, or return None on exact miss.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@chatbot/llm_models/llm_script.py` around lines 261 - 264, Update
get_pricing_from_company_bot and its pricing data so supported Bedrock Claude
model IDs, including the newly listed Haiku and Sonnet IDs, have exact pricing
entries; when pricing_data.get(model_id) misses, return None instead of falling
back to the llama3-3-70b entry. Preserve existing exact-match pricing behavior
for all other models.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's okay. Don't want to touch old code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VishnuKrishnathu, acknowledged. I will keep this existing pricing fallback behavior unchanged in this PR.

🐇

You are interacting with an AI system.

model_id = model_name
else:
model_id = 'meta.llama3-1-8b-instruct-v1:0'
Expand Down Expand Up @@ -621,7 +639,10 @@ def handle_openai_response_api(

client = OpenAI(api_key=client_api_key)

if model_name:
custom_model = get_custom_model(company_bot)
if custom_model:
model_to_use = custom_model
elif model_name:
model_to_use = model_name
elif company_bot:
model_to_use = company_bot.llm_model
Expand Down
3 changes: 2 additions & 1 deletion chatbot/models/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ class LLMModel(models.TextChoices):
GPT5_2 = 'gpt-5.2', _('GPT_5_2')
GPT5_2_PRO = 'gpt-5.2-pro', _('GPT_5_2_PRO')
GPT5_MINI = 'gpt-5-mini', _('GPT_5_MINI')

CLAUDE_HAIKU_4_5 = 'us.anthropic.claude-haiku-4-5-20251001-v1:0', _('CLAUDE_HAIKU_4_5')
CLAUDE_SONNET_4_5 = 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', _('CLAUDE_SONNET_4_5')

class EntityStatus(models.TextChoices):
"""
Expand Down
23 changes: 1 addition & 22 deletions chatbot/models/story_models.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import io
import os
import base64
from django.db import models
from django.core.validators import MinLengthValidator
from chatbot.models import Profile, TagChoices, StoryLanguageChoices, StorySourceChoices, MediaTypeChoices, \
StoryStatusChoices, Company, TagSourceChoices
from pillow_heif import register_heif_opener
from django.core.files.base import ContentFile
from PIL import Image, UnidentifiedImageError
import requests

from chatbot.services.storage import StorageFactory

S3_BASE_URL = os.getenv('S3_MEDIA_URL')
register_heif_opener()
Expand Down Expand Up @@ -116,30 +112,17 @@ def get_public_url(self):

def save(self, *args, **kwargs):
try:
if self.file_url:
if self.file_url.startswith("s3://"):
storage_handler = StorageFactory.get_storage_handler()
response_content = storage_handler.get_file_from_store(self.file_url)
self.base64_str = base64.b64encode(response_content).decode('utf-8')
print("Encoded base64 from file_url")
else:
response = requests.get(self.file_url)
response.raise_for_status()
self.base64_str = base64.b64encode(response.content).decode('utf-8')
print("Encoded base64 from file_url")

if not self.file:
super().save(*args, **kwargs)
return
self.file.seek(0)
file_ext = os.path.splitext(self.file.name)[1].lower()
print("file_ext:", file_ext)
print("File name:", self.file.name)
print("File size:", self.file.size)

# Convert HEIC/HEIF to JPEG
if file_ext in ['.heic', '.heif']:
try:
self.file.seek(0)
image = Image.open(self.file)
converted_io = io.BytesIO()
image.save(converted_io, format='JPEG')
Expand All @@ -156,10 +139,6 @@ def save(self, *args, **kwargs):
except Exception as e:
print("Unexpected error during HEIF conversion:", str(e))

# Reset pointer before base64 encoding
self.file.seek(0)
self.base64_str = base64.b64encode(self.file.read()).decode('utf-8')

except Exception as e:
print("Error during save():", str(e))

Expand Down
6 changes: 3 additions & 3 deletions chatbot/utils/shikshalokam_story_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,6 @@ def get_html_from_template(story, profile, flow, auth=False, language=None):
return html_content

def update_story_pdf(access_token, session, flow, is_edit_story=False):

try:
chatsession = ChatSession.objects.values("language").get(session=session)

Expand All @@ -431,13 +430,15 @@ def update_story_pdf(access_token, session, flow, is_edit_story=False):

if story and story.content and story.formatted_content:
update_story_content(story)

profile = story.author
print("profile: ", profile)
print("story: ", story.title)
print("story format: ", story.formatted_content)
Comment on lines 435 to 437

@coderabbitai coderabbitai Bot Aug 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove user data from PDF-flow debug output.

These print calls write profile data and user-generated story content to stdout during every PDF update. This conflicts with the production log cleanup objective and can expose sensitive data. Remove the calls. Use redacted structured logging only when diagnostics are required.

Proposed fix
-        print("profile: ", profile)
-        print("story: ", story.title)
-        print("story format: ", story.formatted_content)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
print("profile: ", profile)
print("story: ", story.title)
print("story format: ", story.formatted_content)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@chatbot/utils/shikshalokam_story_utils.py` around lines 435 - 437, Remove the
three debug print calls for profile, story title, and story.formatted_content
from the PDF update flow. Do not emit user or story data to stdout; retain
existing behavior and use only redacted structured logging if diagnostics are
required elsewhere.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's okay. Don't want to touch old code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VishnuKrishnathu, acknowledged. I will leave the existing PDF-flow debug output unchanged in this PR.

🐇︵︵

You are interacting with an AI system.

language = chatsession.get("language", StoryLanguageChoices.ENGLISH)
flow_obj = Flow.objects.filter(flow_route=flow).first()
has_pdf_template = flow_obj and PDFTemplates.objects.filter(flow=flow_obj).exists()

if has_pdf_template:
html_content = get_html_from_template(
story=story, profile=profile, flow=flow,
Expand All @@ -447,7 +448,7 @@ def update_story_pdf(access_token, session, flow, is_edit_story=False):
html_content = get_story_html(story=story, profile=profile, flow=flow)

pdf_generated = generate_pdf_with_gotenberg(html_content)
# print("pdf_generated: ", pdf_generated)

pdf_file_name = story.title
if not pdf_file_name or pdf_file_name == '':
pdf_file_name = 'Improvement_story'
Expand Down Expand Up @@ -514,7 +515,6 @@ def update_story_pdf(access_token, session, flow, is_edit_story=False):
conversation = get_stored_conversation(company_chats=company_chats)
chat_history = get_stored_chathistory(company_chats=company_chats)


tasks_payload = []

task_id_from_session = None
Expand Down
9 changes: 1 addition & 8 deletions chatbot/views/story_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from chatbot.models import Story, StoryMedia, SessionFlowName
from chatbot.models.base_models import Flow
from chatbot.models.enums import CreateStoryChoices
from chatbot.models.media_models import ProfileMedia
from chatbot.serializer.profile_serializer import ProfileMediaSerializer
from chatbot.serializer.story_serializer import StoryCreateSerializer, StoryRetrieveSerializer, StoryMediaRetrieveSerializer, StoryFullSerializer
Expand All @@ -9,7 +8,6 @@
from chatbot.utils.story_utils.base.story_update_utils import extract_update_data, get_or_create_translation, update_translation_fields, sync_to_main_story
from chatbot.utils.story_utils.base.translation_mixins import LanguageDetectionMixin
from chatbot.utils.story_utils.story_utils import create_story_object, generate_story
from django.contrib.auth import PermissionDenied
from rest_framework import generics, status
from rest_framework.decorators import api_view
from rest_framework.response import Response
Expand Down Expand Up @@ -239,7 +237,6 @@ def create(self, request, *args, **kwargs):
"""
Handle POST requests (create).
"""
print("Creating")
session_value = request.data.get('session')
access_token = request.data.get('access_token')
flow = request.data.get('flow')
Expand All @@ -249,18 +246,14 @@ def create(self, request, *args, **kwargs):
file_url = "https://" + file_url[len("s3://"):]
request.data["file_url"] = file_url

print("session_value: ", session_value)
print("flow: ", flow)
print("access_token: ", access_token)
try:
response = super().create(request, *args, **kwargs)
print("response: ", response)
print("response status_code: ", response.status_code)

if response.status_code == status.HTTP_201_CREATED and flow != SessionFlowName.Reflection:
update_story_pdf(
access_token=access_token, session=session_value, flow=flow
)

return response

except Exception as e:
Expand Down
3 changes: 1 addition & 2 deletions shikshalokam_mohini/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
from datetime import timedelta
import sentry_sdk
from dotenv import load_dotenv
from socket import gethostbyname
from socket import gethostname

load_dotenv()

Expand Down Expand Up @@ -370,6 +368,7 @@ def load_secrets():
**STORAGES["staticfiles"]["OPTIONS"],
"location": "static",
}

else:
raise ValueError(
f"Unsupported STORAGE_CLOUD_PROVIDER: {STORAGE_CLOUD_PROVIDER}. "
Expand Down