Repository: dimagi/open-chat-studio
Pull request #1318: event bot for generating reminder messages
Author: login: snopoke
Created: 2025-03-12T16:13:31Z | Merged: 2025-03-20T12:30:25Z

Resolves #1195 

## Description
Introduces a new 'bot' which is used to generate the messages that get sent to users from reminders, events etc.

## User Impact
Improved reminder messages

### Docs and Changelog
TODO

Linked issue #1195: Tool based reminders sometimes causes the bot to reply to itself
Related [Slack thread](https://dimagi.slack.com/archives/C05PK63Q89H/p1739571399405219).
The [Jira ticket](https://dimagi.atlassian.net/browse/OCSBUGS-4)

The bug seems to occur with the dynamic reminders (i.e. the one-off tool based reminder). It results in a weird “meta conversation” loop where the LLM sends a reminder and then responds to itself, thanking itself for the reminder and continuing the exchange unnecessarily.

Linked issue comments: user: login: snopoke
created_at: 2025-02-21T14:18:45Z
body: Thoughts:

* don't use the experiment / pipeline bot, use a standalone prompt purpose built for generating these reminders
* pass the experiment description in the system prompt
* include participant data and last N messages of chat history for context and language hints
author_association: CONTRIBUTOR

Diff:
diff --git a/apps/api/tests/test_api.py b/apps/api/tests/test_api.py
index 8abf850357..c6d152527d 100644
--- a/apps/api/tests/test_api.py
+++ b/apps/api/tests/test_api.py
@@ -20,7 +20,7 @@
 from apps.utils.factories.channels import ExperimentChannelFactory
 from apps.utils.factories.experiment import ExperimentFactory, ParticipantFactory
 from apps.utils.factories.team import TeamWithUsersFactory
-from apps.utils.langchain import build_fake_llm_service
+from apps.utils.langchain import mock_llm
 from apps.utils.tests.clients import ApiTestClient
 
 
@@ -499,16 +499,13 @@ def _setup_participant_data(
 
 @pytest.mark.django_db()
 @override_settings(CELERY_TASK_ALWAYS_EAGER=True)
-@patch("apps.experiments.models.Experiment.get_llm_service")
 @patch("apps.chat.channels.CommCareConnectClient")
-def test_generate_bot_message_and_send(ConnectClient, get_llm_service, experiment):
+def test_generate_bot_message_and_send(ConnectClient, experiment):
     """
     Test that a bot message is generated and sent to a participant. If there isn't a session for the participant yet,
     we expect one to be created. The generated bot message should be saved as an AI message, but the prompt should not
     be saved.
     """
-    fake_llm_service = build_fake_llm_service(responses=["Time to take a break an brew some coffee"], token_counts=[0])
-    get_llm_service.return_value = fake_llm_service
     connect_client_mock = ConnectClient.return_value
 
     connect_id = uuid.uuid4().hex
@@ -534,33 +531,36 @@ def test_generate_bot_message_and_send(ConnectClient, get_llm_service, experimen
         "identifier": connect_id,
         "platform": ChannelPlatform.COMMCARE_CONNECT,
         "experiment": str(experiment.public_id),
-        "prompt_text": "Tell the user to take a break and make coffee",
+        "prompt_text": "Tell the user to take a break and make a beverege",
     }
     url = reverse("api:trigger_bot")
-    response = client.post(url, json.dumps(data), content_type="application/json")
+    with mock_llm(["Time to take a break and brew some coffee"], [0]):
+        response = client.post(url, json.dumps(data), content_type="application/json")
     assert response.status_code == 200
     connect_client_mock.send_message_to_user.assert_called()
     kwargs = connect_client_mock.send_message_to_user.call_args.kwargs
-    assert kwargs["message"] == "Time to take a break an brew some coffee"
+    assert kwargs["message"] == "Time to take a break and brew some coffee"
     session = ExperimentSession.objects.get(participant=participant_data.participant, experiment=experiment)
     assert session.chat.messages.count() == 1
     first_message = session.chat.messages.first()
     assert first_message.message_type == "ai"
-    assert first_message.content == "Time to take a break an brew some coffee"
+    assert first_message.content == "Time to take a break and brew some coffee"
 
     # Call it a second time to make sure the session is reused
-    response = client.post(url, json.dumps(data), content_type="application/json")
+    with mock_llm(["Time to take a break and brew some tea"], [0]):
+        response = client.post(url, json.dumps(data), content_type="application/json")
     assert response.status_code == 200
     session = ExperimentSession.objects.get(participant=participant_data.participant, experiment=experiment)
     assert session.chat.messages.count() == 2
     last_message = session.chat.messages.last()
     assert last_message.message_type == "ai"
-    assert last_message.content == "Time to take a break an brew some coffee"
+    assert last_message.content == "Time to take a break and brew some tea"
 
     # Call it a third time, but this time we want to start a new session
     first_session = session
     data["start_new_session"] = True
-    response = client.post(url, json.dumps(data), content_type="application/json")
+    with mock_llm(["Time to take a break an juice some fruit"], [0]):
+        response = client.post(url, json.dumps(data), content_type="application/json")
     assert response.status_code == 200
     first_session.refresh_from_db()
     assert first_session.status == SessionStatus.PENDING_REVIEW
@@ -572,4 +572,4 @@ def test_generate_bot_message_and_send(ConnectClient, get_llm_service, experimen
     assert new_session.chat.messages.count() == 1
     last_message = new_session.chat.messages.last()
     assert last_message.message_type == "ai"
-    assert last_message.content == "Time to take a break an brew some coffee"
+    assert last_message.content == "Time to take a break an juice some fruit"
diff --git a/apps/channels/tests/test_base_channel_behavior.py b/apps/channels/tests/test_base_channel_behavior.py
index 1bb1efa345..10b3941514 100644
--- a/apps/channels/tests/test_base_channel_behavior.py
+++ b/apps/channels/tests/test_base_channel_behavior.py
@@ -30,7 +30,7 @@
 from apps.utils.factories.channels import ExperimentChannelFactory
 from apps.utils.factories.experiment import ExperimentFactory, ExperimentSessionFactory
 from apps.utils.factories.team import MembershipFactory
-from apps.utils.langchain import build_fake_llm_service, mock_experiment_llm
+from apps.utils.langchain import build_fake_llm_service, mock_llm
 
 from .message_examples import telegram_messages
 
@@ -232,23 +232,21 @@ def test_reset_conversation_does_not_create_new_session(
 
 
 def _send_user_message_on_channel(channel_instance, user_message: str):
-    with mock_experiment_llm(channel_instance.experiment, responses=["OK"]):
+    with mock_llm(responses=["OK"]):
         channel_instance.new_user_message(user_message)
 
 
 @pytest.mark.django_db()
 @patch("apps.chat.channels.TelegramChannel.submit_input_to_llm", Mock())
 @patch("apps.chat.channels.TelegramChannel._get_bot_response", Mock())
-@patch("apps.chat.channels.TelegramChannel._generate_response_for_user")
+@patch("apps.chat.channels.TelegramChannel._send_seed_message")
 @patch("apps.chat.channels.TelegramChannel.send_text_to_user")
-def test_pre_conversation_flow(send_text_to_user_mock, generate_response_for_user):
+def test_pre_conversation_flow(send_text_to_user_mock, _send_seed_message):
     """This simulates an interaction between a user and the bot. The user initiated the conversation, so the
     user and bot must first go through the pre conversation flow. The following needs to happen:
     - The user must give consent
     - The user must indicate that they filled out the survey
     """
-    bot_response_to_seed_message = "Hi user"
-    generate_response_for_user.return_value = bot_response_to_seed_message
     experiment = ExperimentFactory(conversational_consent_enabled=True)
     channel = TelegramChannel(experiment, ExperimentChannelFactory(experiment=experiment))
     pre_survey = experiment.pre_survey
@@ -287,18 +285,20 @@ def _user_message(message: str):
     assert channel.experiment_session.status == SessionStatus.PENDING_PRE_SURVEY
     # The bot should be persistent about that survey. Let's make sure it sends it
     assert expected_survey_text in chat.messages.last().content
+
     # The user caves, and says they did fill it out
+    assert _send_seed_message.call_count == 0
     _user_message("1")
     # Check the status
     channel.experiment_session.refresh_from_db()
     assert channel.experiment_session.status == SessionStatus.ACTIVE
-    generate_response_for_user.assert_called()
-    assert send_text_to_user_mock.call_args[0][0] == bot_response_to_seed_message
+    _send_seed_message.assert_called()
 
 
 @pytest.mark.django_db()
 @patch("apps.chat.channels.TelegramChannel.send_text_to_user", Mock())
 @patch("apps.chat.bots.TopicBot", Mock())
+@patch("apps.chat.bots.EventBot.get_user_message", Mock(return_value="error"))
 def test_unsupported_message_type_creates_system_message():
     experiment = ExperimentFactory(conversational_consent_enabled=True)
     channel = TelegramChannel(experiment, ExperimentChannelFactory(experiment=experiment))
@@ -379,9 +379,9 @@ def test_voice_response_behaviour(
 )
 @patch("apps.chat.channels.TelegramChannel.send_text_to_user")
 @patch("apps.chat.channels.TelegramChannel._reply_voice_message")
-@patch("apps.chat.channels.TelegramChannel._generate_response_for_user")
+@patch("apps.chat.bots.EventBot.get_user_message")
 def test_failed_transcription_informs_the_user(
-    _generate_response_for_user,
+    _get_user_message,
     _reply_voice_message,
     send_text_to_user,
     voice_behaviour,
@@ -390,7 +390,7 @@ def test_failed_transcription_informs_the_user(
 ):
     """When we fail to transcribe the user's voice message, we should inform them"""
 
-    _generate_response_for_user.return_value = "Sorry, we could not transcribe your message"
+    _get_user_message.return_value = "Sorry, we could not transcribe your message"
     experiment = telegram_channel.experiment
     experiment.voice_response_behaviour = voice_behaviour
     experiment.save()
@@ -404,11 +404,11 @@ def test_failed_transcription_informs_the_user(
 
 
 @pytest.mark.django_db()
-@patch("apps.chat.channels.TelegramChannel._generate_response_for_user")
+@patch("apps.chat.bots.EventBot.get_user_message")
 @patch("apps.chat.channels.TelegramChannel.send_message_to_user")
 @patch("apps.chat.channels.TelegramChannel.is_message_type_supported")
 def test_any_failure_informs_users(
-    is_message_type_supported, send_message_to_user, _generate_response_for_user, telegram_channel, caplog
+    is_message_type_supported, send_message_to_user, _get_user_message, telegram_channel, caplog
 ):
     """
     Any failure should try and inform the user that something went wrong. The method that does the informing should
@@ -417,7 +417,7 @@ def test_any_failure_informs_users(
 
     is_message_type_supported.side_effect = Exception("Random error")
     # The generate response should fail, causing the default error message to be sent
-    _generate_response_for_user.side_effect = Exception("Generation error")
+    _get_user_message.side_effect = Exception("Generation error")
 
     with pytest.raises(Exception, match="Random error"):
         telegram_channel.new_user_message(telegram_messages.text_message())
diff --git a/apps/channels/tests/test_slack_channel.py b/apps/channels/tests/test_slack_channel.py
index c0e6571467..08ebb90e71 100644
--- a/apps/channels/tests/test_slack_channel.py
+++ b/apps/channels/tests/test_slack_channel.py
@@ -33,10 +33,10 @@ def test_handle_user_message(process_input, slack_channel):
 
 
 @pytest.mark.django_db()
-@patch("apps.chat.bots.TopicBot.process_input")
+@patch("apps.chat.bots.EventBot.get_user_message")
 @patch("apps.chat.channels.SlackChannel.messaging_service")
-def test_ad_hoc_bot_message(messaging_service, process_input, slack_channel):
-    process_input.return_value = "Hi"
+def test_ad_hoc_bot_message(messaging_service, get_user_message, slack_channel):
+    get_user_message.return_value = "Hi"
     session = SlackChannel.start_new_session(
         slack_channel.experiment,
         slack_channel,
diff --git a/apps/chat/bots.py b/apps/chat/bots.py
index cd379a17f6..c2e82de3eb 100644
--- a/apps/chat/bots.py
+++ b/apps/chat/bots.py
@@ -1,3 +1,4 @@
+import textwrap
 from typing import TYPE_CHECKING, Any
 
 from langchain.memory import ConversationBufferMemory
@@ -13,6 +14,8 @@
 from apps.events.tasks import enqueue_static_triggers
 from apps.experiments.models import Experiment, ExperimentRoute, ExperimentSession, SafetyLayer
 from apps.pipelines.nodes.base import PipelineState
+from apps.service_providers.llm_service.default_models import get_default_model
+from apps.service_providers.llm_service.prompt_context import PromptTemplateContext
 from apps.service_providers.llm_service.runnables import create_experiment_runnable
 
 if TYPE_CHECKING:
@@ -309,3 +312,70 @@ def process_input(self, user_input: str, save_input_to_history=True, attachments
 
     def get_ai_message_id(self) -> int:
         return self.ai_message_id
+
+
+class EventBot:
+    SYSTEM_PROMPT = textwrap.dedent(
+        """
+    Your role is to generate messages to send to users. These could be reminders
+    or prompts to help them complete their tasks. The text that you generate will be sent
+    to the user in a chat message.
+
+    You should generate the message in the language of the user.
+
+    This is the data we have about the user:
+    ```
+    {participant_data}
+    ```
+
+    The current date and time is: {current_datetime}
+    
+    Here are the most recent messages in the conversation:
+    ```
+    {conversation_history}
+    ```
+    """
+    )
+
+    def __init__(self, session: ExperimentSession, experiment: Experiment):
+        self.session = session
+        self.experiment = experiment or session.experiment_version
+
+    def get_user_message(self, event_prompt: str):
+        provider = self.llm_provider
+        if not provider:
+            raise Exception("No LLM provider found")
+
+        model = get_default_model(provider.type)
+
+        service = provider.get_llm_service()
+        llm = service.get_chat_model(model.name, 0.7)
+        response = llm.invoke(
+            [
+                {"role": "system", "content": self.system_prompt},
+                {"role": "user", "content": f"Generate the message for the user based on this text: {event_prompt}"},
+            ]
+        )
+        return response.content
+
+    @property
+    def llm_provider(self):
+        if self.experiment.llm_provider:
+            return self.experiment.llm_provider
+
+        # If no LLM provider is set, use the first one in the team
+        return self.experiment.team.llmprovider_set.first()
+
+    @property
+    def system_prompt(self):
+        context = PromptTemplateContext(self.session, None).get_context(["participant_data", "current_datetime"])
+        context["conversation_history"] = self.get_conversation_history()
+        return self.SYSTEM_PROMPT.format(**context)
+
+    def get_conversation_history(self):
+        messages = []
+        for message in self.session.chat.message_iterator(with_summaries=False):
+            messages.append(f"{message.role}: {message.content}")
+            if len(messages) > 10:
+                break
+        return "\n".join(reversed(messages))
diff --git a/apps/chat/channels.py b/apps/chat/channels.py
index 9a993f2e11..081dd05ce4 100644
--- a/apps/chat/channels.py
+++ b/apps/chat/channels.py
@@ -16,7 +16,7 @@
 from apps.channels import audio
 from apps.channels.clients.connect_client import CommCareConnectClient
 from apps.channels.models import ChannelPlatform, ExperimentChannel
-from apps.chat.bots import get_bot
+from apps.chat.bots import EventBot, get_bot
 from apps.chat.exceptions import (
     AudioSynthesizeException,
     ChannelException,
@@ -331,8 +331,12 @@ def start_conversation(self):
         self.experiment_session.update_status(SessionStatus.ACTIVE)
         # This is technically the start of the conversation
         if self.experiment.seed_message:
-            bot_response = self._generate_response_for_user(self.experiment.seed_message)
-            self.send_message_to_user(bot_response)
+            self._send_seed_message()
+
+    def _send_seed_message(self):
+        topic_bot = self.bot or get_bot(self.experiment_session, experiment=self.experiment)
+        bot_response = topic_bot.process_input(user_input=self.experiment.seed_message, save_input_to_history=False)
+        self.send_message_to_user(bot_response)
 
     def _chat_initiated(self):
         """The user initiated the chat and we need to get their consent before continuing the conversation"""
@@ -550,36 +554,29 @@ def _unsupported_message_type_response(self):
             message_type=ChatMessageType.SYSTEM,
             content=f"The user sent an unsupported message type: {self.message.content_type_unparsed}",
         )
-        return self._generate_response_for_user(
+        return EventBot(self.experiment_session, self.experiment).get_user_message(
             UNSUPPORTED_MESSAGE_BOT_PROMPT.format(supported_types=self.supported_message_types)
         )
 
     def _inform_user_of_error(self):
-        """Simply tells the user that something went wrong to keep them in the loop. This method wil not raise an error
-        if something went wrong during this operation.
+        """Simply tells the user that something went wrong to keep them in the loop.
+        This method will not raise an error if something went wrong during this operation.
         """
 
         try:
-            bot_message = self._generate_response_for_user(
-                """
-                Tell the user that something went wrong while processing their message and that they should
-                try again later
-                """
+            bot_message = EventBot(self.experiment_session, self.experiment).get_user_message(
+                "Tell the user that something went wrong while processing their message and that they should "
+                "try again later"
             )
-        except Exception:
+        except Exception:  # noqa BLE001
             logger.exception("Something went wrong while trying to generate an appropriate error message for the user")
             bot_message = DEFAULT_ERROR_RESPONSE_TEXT
 
         try:
             self.send_message_to_user(bot_message)
-        except Exception:
+        except Exception:  # noqa BLE001
             logger.exception("Something went wrong while trying to inform the user of an error")
 
-    def _generate_response_for_user(self, prompt: str) -> str:
-        """Generates a response based on the `prompt`."""
-        topic_bot = self.bot or get_bot(self.experiment_session, experiment=self.experiment)
-        return topic_bot.process_input(user_input=prompt, save_input_to_history=False)
-
 
 class WebChannel(ChannelBase):
     """Message Handler for the UI"""
diff --git a/apps/chat/tests/test_event_bot.py b/apps/chat/tests/test_event_bot.py
new file mode 100644
index 0000000000..9f78478647
--- /dev/null
+++ b/apps/chat/tests/test_event_bot.py
@@ -0,0 +1,73 @@
+from unittest.mock import MagicMock, PropertyMock, patch
+
+import pytest
+from langchain_core.messages import AIMessage
+
+from apps.chat.bots import EventBot
+from apps.experiments.models import Experiment, ExperimentSession
+from apps.utils.factories.experiment import ExperimentSessionFactory
+from apps.utils.langchain import build_fake_llm_service
+
+
+@pytest.fixture()
+def event_bot():
+    session = MagicMock(spec=ExperimentSession)
+    experiment = MagicMock(spec=Experiment)
+    return EventBot(session, experiment)
+
+
+@patch("apps.chat.bots.get_default_model")
+@patch("apps.chat.bots.EventBot.llm_provider", new_callable=PropertyMock)
+@patch("apps.chat.bots.PromptTemplateContext.get_context")
+def test_get_user_message(mock_get_context, mock_llm_provider, mock_get_default_model, event_bot):
+    mock_get_context.return_value = {
+        "participant_data": "Test Participant",
+        "current_datetime": "2023-10-01 12:00:00",
+    }
+    mock_llm_provider.return_value = MagicMock()
+    mock_service = mock_llm_provider.return_value.get_llm_service.return_value
+    mock_llm = mock_service.get_chat_model.return_value
+    mock_llm.invoke.return_value = AIMessage(content="Generated message")
+
+    event_prompt = "Test event prompt"
+    result = event_bot.get_user_message(event_prompt)
+
+    assert result == "Generated message"
+    mock_llm.invoke.assert_called()
+
+
+@patch("apps.chat.bots.EventBot.get_conversation_history")
+@patch("apps.chat.bots.PromptTemplateContext.get_context")
+def test_system_prompt(mock_get_context, mock_get_conversation_history, event_bot):
+    mock_get_context.return_value = {
+        "participant_data": "Test Participant",
+        "current_datetime": "2023-10-01 12:00:00",
+    }
+    mock_get_conversation_history.return_value = "Test history"
+    expected_prompt = event_bot.SYSTEM_PROMPT.format(
+        participant_data="Test Participant",
+        current_datetime="2023-10-01 12:00:00",
+        conversation_history="Test history",
+    )
+    assert event_bot.system_prompt == expected_prompt
+    assert "Test history" in event_bot.system_prompt
+    assert "Test Participant" in event_bot.system_prompt
+    assert "2023-10-01 12:00:00" in event_bot.system_prompt
+
+
+@pytest.mark.django_db()
+@patch("apps.service_providers.models.LlmProvider.get_llm_service")
+def test_get_user_message_with_llm_provider(mock_get_llm_service):
+    fake_llm_service = build_fake_llm_service(responses=["this is a test message"], token_counts=[30, 20, 10])
+    mock_get_llm_service.return_value = fake_llm_service
+    session = ExperimentSessionFactory()
+    event_bot = EventBot(session, session.experiment)
+    response = event_bot.get_user_message("Test event prompt")
+    mock_get_llm_service.assert_called()
+    assert response == "this is a test message"
+    calls = fake_llm_service.llm.get_call_messages()
+    assert len(calls) == 1
+    assert calls[0][0].type == "system"
+    assert session.participant.name in calls[0][0].content
+    assert calls[0][1].type == "human"
+    assert calls[0][1].content == "Generate the message for the user based on this text: Test event prompt"
diff --git a/apps/chat/tests/test_routing.py b/apps/chat/tests/test_routing.py
index 8ced46f204..884cdd8729 100644
--- a/apps/chat/tests/test_routing.py
+++ b/apps/chat/tests/test_routing.py
@@ -10,7 +10,7 @@
 from apps.utils.factories.assistants import OpenAiAssistantFactory
 from apps.utils.factories.experiment import ExperimentFactory, ExperimentSessionFactory
 from apps.utils.factories.team import TeamFactory
-from apps.utils.langchain import build_fake_llm_service, mock_experiment_llm
+from apps.utils.langchain import build_fake_llm_service, mock_llm
 
 
 @pytest.mark.django_db()
@@ -47,7 +47,7 @@ def test_experiment_routing_with_tracing():
     session = ExperimentSessionFactory(experiment=experiment)
     provider = TraceProvider(type="langfuse", config={})
     session.experiment.trace_provider = provider
-    with mock_experiment_llm(None, responses=["anything", "How can I help today?"], token_counts=[0]) as fake_service:
+    with mock_llm(responses=["anything", "How can I help today?"], token_counts=[0]) as fake_service:
         bot = TopicBot(session)
         response = bot.process_input("Hi")
     assert response == "How can I help today?"
diff --git a/apps/chat/tests/test_tasks.py b/apps/chat/tests/test_tasks.py
index e2f28c2dcc..34cd89f7a6 100644
--- a/apps/chat/tests/test_tasks.py
+++ b/apps/chat/tests/test_tasks.py
@@ -1,5 +1,3 @@
-from unittest.mock import patch
-
 import pytest
 from django.test import TestCase
 
@@ -11,9 +9,8 @@
 from apps.service_providers.models import LlmProvider, LlmProviderModel
 from apps.teams.models import Team
 from apps.users.models import CustomUser
-from apps.utils.factories.assistants import OpenAiAssistantFactory
 from apps.utils.factories.experiment import ExperimentSessionFactory
-from apps.utils.langchain import mock_experiment_llm
+from apps.utils.langchain import mock_llm
 
 
 class TasksTest(TestCase):
@@ -54,7 +51,7 @@ def setUp(self):
 
     def test_getting_ping_message_saves_history(self):
         expected_ping_message = "Hey, answer me!"
-        with mock_experiment_llm(self.experiment, responses=[expected_ping_message]):
+        with mock_llm(responses=[expected_ping_message]):
             response = self.experiment_session._bot_prompt_for_user("Some message")
         messages = ChatMessage.objects.filter(chat=self.experiment_session.chat).all()
         # Only the AI message should be there
@@ -81,25 +78,6 @@ def _add_chats(self, experiment_session: ExperimentSession, last_message_type: C
             )
 
 
-@pytest.mark.django_db()
-@patch("apps.service_providers.llm_service.runnables.AssistantChat._get_output_with_annotations")
-def test_no_activity_ping_with_assistant_bot(save_response_annotations):
-    save_response_annotations.return_value = "Hey, answer me!", {}
-    session = ExperimentSessionFactory()
-    local_assistant = OpenAiAssistantFactory()
-    session.experiment.assistant = local_assistant
-
-    expected_ping_message = "Hey, answer me!"
-    with mock_experiment_llm(session.experiment, responses=[expected_ping_message]):
-        response = session._bot_prompt_for_user("Some message")
-    messages = ChatMessage.objects.filter(chat=session.chat).all()
-    # Only the AI message should be there
-    assert len(messages) == 1
-    assert messages[0].message_type == "ai"
-    assert response == expected_ping_message
-    assert messages[0].content == expected_ping_message
-
-
 @pytest.mark.django_db()
 def test_get_latest_sessions_for_participants():
     p1_session1 = ExperimentSessionFactory()
diff --git a/apps/chat/tests/test_topic_bot.py b/apps/chat/tests/test_topic_bot.py
index b70821e070..d827113cbc 100644
--- a/apps/chat/tests/test_topic_bot.py
+++ b/apps/chat/tests/test_topic_bot.py
@@ -9,7 +9,7 @@
 from apps.experiments.models import ExperimentRoute, ExperimentRouteType, ExperimentSession, SafetyLayer
 from apps.service_providers.models import TraceProvider
 from apps.utils.factories.experiment import ExperimentFactory, ExperimentSessionFactory
-from apps.utils.langchain import build_fake_llm_service, mock_experiment_llm
+from apps.utils.langchain import build_fake_llm_service, mock_llm
 
 
 @pytest.mark.django_db()
@@ -49,7 +49,7 @@ def test_bot_with_terminal_bot(get_output_check_cancellation):
 
     expected = "Sorry I can't help with that."
     bot = TopicBot(session)
-    with mock_experiment_llm(experiment, responses=[expected]):
+    with mock_llm(responses=[expected]):
         bot.process_input("What are we going to do?")
 
     assert session.chat.messages.count() == 2
@@ -79,7 +79,7 @@ def test_tracing_service():
     with (
         patch(f"{service}.get_callback") as mock_get_callback,
         patch(f"{service}.get_current_trace_info") as mock_get_trace_info,
-        mock_experiment_llm(None, responses=["response"]),
+        mock_llm(responses=["response"]),
     ):
         bot = TopicBot(session)
         assert bot.process_input("test") == "response"
@@ -113,7 +113,7 @@ def _run_bot_with_wrapped_service(session, response):
         assert bot.process_input("test") == response
         mock_service.get_callback.assert_called_once()
 
-    with mock_experiment_llm(None, responses=["response1", "response2"]):
+    with mock_llm(responses=["response1", "response2"]):
         _run_bot_with_wrapped_service(session, "response1")
 
         # reload the session from the DB
diff --git a/apps/experiments/models.py b/apps/experiments/models.py
index 34b02adb95..2b7bb15322 100644
--- a/apps/experiments/models.py
+++ b/apps/experiments/models.py
@@ -1715,10 +1715,15 @@ def _bot_prompt_for_user(self, instruction_prompt: str, use_experiment: Experime
         """Sends the `instruction_prompt` along with the chat history to the LLM to formulate an appropriate prompt
         message. The response from the bot will be saved to the chat history.
         """
-        from apps.chat.bots import get_bot
+        from apps.chat.bots import EventBot
 
-        bot = get_bot(self, experiment=use_experiment, disable_tools=True)
-        return bot.process_input(user_input=instruction_prompt, save_input_to_history=False)
+        bot = EventBot(self, use_experiment)
+        message = bot.get_user_message(instruction_prompt)
+        chat_message = ChatMessage.objects.create(chat=self.chat, message_type=ChatMessageType.AI, content=message)
+        chat_message.add_version_tag(
+            version_number=bot.experiment.version_number, is_a_version=bot.experiment.is_a_version
+        )
+        return message
 
     def try_send_message(self, message: str, fail_silently=True):
         """Tries to send a message to this user session as the bot. Note that `message` will be send to the user
diff --git a/apps/experiments/tests/test_get_response_for_webchat_task.py b/apps/experiments/tests/test_get_response_for_webchat_task.py
index 46934912ca..53ccbdcae0 100644
--- a/apps/experiments/tests/test_get_response_for_webchat_task.py
+++ b/apps/experiments/tests/test_get_response_for_webchat_task.py
@@ -3,7 +3,7 @@
 from apps.channels.datamodels import Attachment
 from apps.experiments.tasks import get_response_for_webchat_task
 from apps.utils.factories.experiment import ExperimentSessionFactory
-from apps.utils.langchain import mock_experiment_llm
+from apps.utils.langchain import mock_llm
 
 
 @pytest.fixture()
@@ -19,7 +19,7 @@ def test_get_response_for_webchat_task(session):
         Attachment(file_id=1, type="code_interpreter", name="code.py", size=100),
         Attachment(file_id=2, type="file_search", name="file_search.json", size=100),
     ]
-    with mock_experiment_llm(None, responses=["how can I help?"]):
+    with mock_llm(responses=["how can I help?"]):
         response = get_response_for_webchat_task(
             session.id, session.experiment.id, "Hi", [att.model_dump() for att in attachments]
         )
diff --git a/apps/experiments/tests/test_models.py b/apps/experiments/tests/test_models.py
index af23312cb7..d488d9bb2e 100644
--- a/apps/experiments/tests/test_models.py
+++ b/apps/experiments/tests/test_models.py
@@ -410,14 +410,14 @@ def test_get_participant_data_timezone(self, use_participant_tz):
 
     @pytest.mark.parametrize("fail_silently", [True, False])
     @patch("apps.chat.channels.ChannelBase.from_experiment_session")
-    @patch("apps.chat.bots.TopicBot.process_input")
-    def test_ad_hoc_message(self, process_input, from_experiment_session, fail_silently, experiment_session):
+    @patch("apps.chat.bots.EventBot.get_user_message")
+    def test_ad_hoc_message(self, get_user_message, from_experiment_session, fail_silently, experiment_session):
         mock_channel = Mock()
         mock_channel.send_message_to_user = Mock()
         if not fail_silently:
             mock_channel.send_message_to_user.side_effect = Exception("Cannot send message")
         from_experiment_session.return_value = mock_channel
-        process_input.return_value = "We're testing"
+        get_user_message.return_value = "We're testing"
 
         def _test():
             experiment_session.ad_hoc_bot_message(
diff --git a/apps/service_providers/llm_service/default_models.py b/apps/service_providers/llm_service/default_models.py
index cec214994e..86cd10c5ec 100644
--- a/apps/service_providers/llm_service/default_models.py
+++ b/apps/service_providers/llm_service/default_models.py
@@ -10,6 +10,7 @@
 class Model:
     name: str
     token_limit: int
+    is_default: bool = False
 
 
 def k(n: int) -> int:
@@ -18,7 +19,7 @@ def k(n: int) -> int:
 
 DEFAULT_LLM_PROVIDER_MODELS = {
     "azure": [
-        Model("gpt-4o-mini", 128000),
+        Model("gpt-4o-mini", 128000, is_default=True),
         Model("gpt-4o", 128000),
         Model("gpt-4", k(8)),
         Model("gpt-4-32k", 32768),
@@ -27,14 +28,14 @@ def k(n: int) -> int:
     ],
     "anthropic": [
         Model("claude-3-5-sonnet-latest", k(200)),
-        Model("claude-3-5-haiku-latest", k(200)),
+        Model("claude-3-5-haiku-latest", k(200), is_default=True),
         Model("claude-3-opus-latest", k(200)),
         Model("claude-2.0", k(100)),
         Model("claude-2.1", k(200)),
         Model("claude-instant-1.2", k(100)),
     ],
     "openai": [
-        Model("gpt-4o-mini", 128000),
+        Model("gpt-4o-mini", 128000, is_default=True),
         Model("gpt-4o", 128000),
         Model("chatgpt-4o-latest", 128000),
         Model("o1-preview", 128000),
@@ -53,7 +54,7 @@ def k(n: int) -> int:
         Model("gemma-7b-it", k(8)),
         Model("llama3-groq-70b-8192-tool-use-preview", k(8)),
         Model("llama3-groq-8b-8192-tool-use-preview", k(8)),
-        Model("llama-3.1-70b-versatile", k(128)),
+        Model("llama-3.1-70b-versatile", k(128), is_default=True),
         Model("llama-3.1-8b-instant", k(128)),
         Model("llama-3.2-1b-preview", k(128)),
         Model("llama-3.2-3b-preview", k(128)),
@@ -65,7 +66,7 @@ def k(n: int) -> int:
         Model("mixtral-8x7b-32768", 32768),
     ],
     "perplexity": [
-        Model("llama-3.1-sonar-small-128k-online", 127072),
+        Model("llama-3.1-sonar-small-128k-online", 127072, is_default=True),
         Model("llama-3.1-sonar-large-128k-online", 127072),
         Model("llama-3.1-sonar-huge-128k-online", 127072),
         Model("llama-3.1-sonar-small-128k-chat", 127072),
@@ -73,16 +74,23 @@ def k(n: int) -> int:
         Model("llama-3.1-8b-instruct", 131072),
         Model("llama-3.1-70b-instruct", 131072),
     ],
-    "deepseek": [Model("deepseek-chat", 128000), Model("deepseek-reaoner", 128000)],
+    "deepseek": [
+        Model("deepseek-chat", 128000, is_default=True),
+        Model("deepseek-reaoner", 128000),
+    ],
     "google": [
         Model("gemini-2.0-flash", 1048576),
-        Model("gemini-1.5-flash", 1048576),
+        Model("gemini-1.5-flash", 1048576, is_default=True),
         Model("gemini-1.5-flash-8b", 1048576),
         Model("gemini-1.5-pro", 2097152),
     ],
 }
 
 
+def get_default_model(provider_type: str) -> Model:
+    return next((m for m in DEFAULT_LLM_PROVIDER_MODELS[provider_type] if m.is_default), None)
+
+
 @transaction.atomic()
 def update_llm_provider_models():
     from apps.service_providers.models import LlmProviderModel
diff --git a/apps/service_providers/tests/test_assistant_runnable.py b/apps/service_providers/tests/test_assistant_runnable.py
index e1c93487d9..65070da649 100644
--- a/apps/service_providers/tests/test_assistant_runnable.py
+++ b/apps/service_providers/tests/test_assistant_runnable.py
@@ -27,7 +27,7 @@
 from apps.utils.factories.assistants import OpenAiAssistantFactory
 from apps.utils.factories.experiment import ExperimentSessionFactory
 from apps.utils.factories.files import FileFactory
-from apps.utils.langchain import mock_experiment_llm
+from apps.utils.langchain import mock_llm
 
 ASSISTANT_ID = "test_assistant_id"
 
@@ -210,10 +210,8 @@ def test_assistant_includes_file_type_information(
 @patch("apps.service_providers.llm_service.history_managers.ExperimentHistoryManager.save_message_to_history", Mock())
 @patch("apps.service_providers.llm_service.adapters.AssistantAdapter.get_attachments", Mock())
 def test_assistant_runnable_raises_error(session):
-    experiment = session.experiment
-
     error = openai.BadRequestError("test", response=mock.Mock(), body={})
-    with mock_experiment_llm(experiment, [error]):
+    with mock_llm([error]):
         assistant_runnable = create_experiment_runnable(session.experiment, session)
         with pytest.raises(openai.BadRequestError):
             assistant_runnable.invoke("test")
@@ -227,10 +225,8 @@ def test_assistant_runnable_raises_error(session):
 @patch("apps.service_providers.llm_service.history_managers.ExperimentHistoryManager.save_message_to_history", Mock())
 @patch("apps.service_providers.llm_service.adapters.AssistantAdapter.get_attachments", Mock())
 def test_assistant_runnable_handles_cancellation_status(session):
-    experiment = session.experiment
-
     error = ValueError("unexpected status: cancelled")
-    with mock_experiment_llm(experiment, [error]):
+    with mock_llm([error]):
         assistant_runnable = create_experiment_runnable(session.experiment, session)
         with pytest.raises(GenerationCancelled):
             assistant_runnable.invoke("test")
@@ -286,7 +282,7 @@ def test_assistant_runnable_cancels_existing_run(save_response_annotations, resp
     assistant_runnable = create_experiment_runnable(session.experiment, session)
     cancel_run = mock.Mock()
     assistant_runnable.__dict__["_cancel_run"] = cancel_run
-    with mock_experiment_llm(session.experiment, responses):
+    with mock_llm(responses):
         with exception:
             result = assistant_runnable.invoke("test")
 
diff --git a/apps/service_providers/tests/test_default_models.py b/apps/service_providers/tests/test_default_models.py
index 809bd32228..2ca05633c1 100644
--- a/apps/service_providers/tests/test_default_models.py
+++ b/apps/service_providers/tests/test_default_models.py
@@ -1,12 +1,21 @@
 import pytest
 
-from apps.service_providers.llm_service.default_models import DEFAULT_LLM_PROVIDER_MODELS, update_llm_provider_models
+from apps.service_providers.llm_service.default_models import (
+    DEFAULT_LLM_PROVIDER_MODELS,
+    get_default_model,
+    update_llm_provider_models,
+)
 from apps.service_providers.models import LlmProviderModel
 from apps.utils.factories.experiment import ExperimentFactory
 from apps.utils.factories.pipelines import PipelineFactory
 from apps.utils.factories.service_provider_factories import LlmProviderModelFactory
 
 
+def test_all_providers_have_default_models():
+    for provider_type in DEFAULT_LLM_PROVIDER_MODELS:
+        assert get_default_model(provider_type) is not None
+
+
 @pytest.mark.django_db()
 def test_updates_existing_models():
     candidate = DEFAULT_LLM_PROVIDER_MODELS["openai"][0]
diff --git a/apps/slack/tests/test_listeners.py b/apps/slack/tests/test_listeners.py
index c4cfe79df2..6745fb6df4 100644
--- a/apps/slack/tests/test_listeners.py
+++ b/apps/slack/tests/test_listeners.py
@@ -9,7 +9,7 @@
 from apps.slack.models import SlackInstallation
 from apps.slack.slack_listeners import new_message
 from apps.utils.factories.channels import ExperimentChannelFactory
-from apps.utils.langchain import mock_experiment_llm
+from apps.utils.langchain import mock_llm
 
 SLACK_TEAM_ID = "SLACK_TEAM_ID"
 SLACK_CHANNEL_ID = "SLACK_CHANNEL_ID"
@@ -47,7 +47,7 @@
 @pytest.mark.django_db()
 @pytest.mark.usefixtures("experiment_channel")
 def test_response_to_bot_mention_in_assigned_channel(bolt_context):
-    with mock_experiment_llm(None, responses=["Hello"]):
+    with mock_llm(responses=["Hello"]):
         new_message(BOT_MENTION_EVENT, bolt_context)
     assert bolt_context.say.call_args_list == [(("Hello",), {"thread_ts": BOT_MENTION_EVENT["ts"]})]
     assert ExperimentSession.objects.count() == 1
@@ -77,12 +77,12 @@ def test_ignores_messages_from_unassigned_channel(bolt_context):
 @pytest.mark.usefixtures("experiment_channel")
 def test_responds_to_session_thread(bolt_context):
     bolt_context["say"] = Mock()
-    with mock_experiment_llm(None, responses=["Hello"]):
+    with mock_llm(responses=["Hello"]):
         new_message(BOT_MENTION_EVENT, bolt_context)
         assert bolt_context.say.call_args_list == [(("Hello",), {"thread_ts": BOT_MENTION_EVENT["ts"]})]
         assert ExperimentSession.objects.count() == 1
 
-    with mock_experiment_llm(None, responses=["How can I help?"]):
+    with mock_llm(responses=["How can I help?"]):
         bolt_context["say"] = Mock()
         new_message(THREAD_REPLY_EVENT, bolt_context)
 
@@ -105,13 +105,13 @@ def test_ignores_non_session_thread(bolt_context):
 
 @pytest.mark.usefixtures("experiment_channel")
 def test_response_to_mention_in_session_thread(bolt_context):
-    with mock_experiment_llm(None, responses=["Hello"]):
+    with mock_llm(responses=["Hello"]):
         # start a thread with the bot
         new_message(BOT_MENTION_EVENT, bolt_context)
         assert bolt_context.say.call_args_list == [(("Hello",), {"thread_ts": BOT_MENTION_EVENT["ts"]})]
         assert ExperimentSession.objects.count() == 1
 
-    with mock_experiment_llm(None, responses=["How can I help?"]):
+    with mock_llm(responses=["How can I help?"]):
         # reply in the thread and mention the bot
         bolt_context["say"] = Mock()
         thread_reply_with_mention = THREAD_REPLY_EVENT.copy()
@@ -124,7 +124,7 @@ def test_response_to_mention_in_session_thread(bolt_context):
 
 @pytest.mark.usefixtures("experiment_channel")
 def test_response_to_mention_in_non_session_thread(bolt_context):
-    with mock_experiment_llm(None, responses=["Hello"]):
+    with mock_llm(responses=["Hello"]):
         # Bot mention in a thread that isn't associated with a session
         thread_reply_with_mention = THREAD_REPLY_EVENT.copy()
         thread_reply_with_mention["text"] = f"<@{BOT_USER_ID}> can you jump in here"
diff --git a/apps/utils/langchain.py b/apps/utils/langchain.py
index 20ec91d2f4..52c3452398 100644
--- a/apps/utils/langchain.py
+++ b/apps/utils/langchain.py
@@ -157,7 +157,7 @@ def _call(self, messages: list[BaseMessage], *args, **kwargs) -> str | BaseMessa
 
 
 @contextmanager
-def mock_experiment_llm(experiment, responses: list[Any], token_counts: list[int] = None):
+def mock_llm(responses: list[Any], token_counts: list[int] = None):
     service = build_fake_llm_service(responses=responses, token_counts=token_counts)
 
     def fake_llm_service(self):
@@ -171,6 +171,7 @@ def fake_get_assistant(self):
     with (
         patch("apps.experiments.models.Experiment.get_llm_service", new=fake_llm_service),
         patch("apps.assistants.models.OpenAiAssistant.get_assistant", new=fake_get_assistant),
+        patch("apps.service_providers.models.LlmProvider.get_llm_service", new=fake_llm_service),
     ):
         yield service
 


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>apps/chat/bots.py (2)</summary><blockquote>

`344-360`: **Add more robust error handling for LLM interactions.**

While there's basic error handling for missing LLM provider, the method could benefit from more comprehensive error handling, especially for LLM service interactions.

Consider making the temperature configurable and adding try/catch blocks:

```diff
-def get_user_message(self, event_prompt: str):
+def get_user_message(self, event_prompt: str, temperature: float = 0.7):
     provider = self.llm_provider
     if not provider:
-        raise Exception("No LLM provider found")
+        raise Exception("No LLM provider configured for this experiment or team")

     model = get_default_model(provider.type)

-    service = provider.get_llm_service()
-    llm = service.get_chat_model(model.name, 0.7)
-    response = llm.invoke(
-        [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": f"Generate the message for the user based on this text: {event_prompt}"},
-        ]
-    )
-    return response.content
+    try:
+        service = provider.get_llm_service()
+        llm = service.get_chat_model(model.name, temperature)
+        response = llm.invoke(
+            [
+                {"role": "system", "content": self.system_prompt},
+                {"role": "user", "content": f"Generate the message for the user based on this text: {event_prompt}"},
+            ]
+        )
+        return response.content
+    except Exception as e:
+        # Log the error
+        raise Exception(f"Failed to generate message: {str(e)}") from e
```

---

`375-381`: **Make conversation history limit configurable.**

The hardcoded limit of 10 messages could be made configurable for flexibility across different use cases.

Consider making the message limit configurable:

```diff
-def get_conversation_history(self):
+def get_conversation_history(self, message_limit: int = 10):
     messages = []
     for message in self.session.chat.message_iterator(with_summaries=False):
         messages.append(f"{message.role}: {message.content}")
-        if len(messages) > 10:
+        if len(messages) >= message_limit:
             break
     return "\n".join(reversed(messages))
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d5cd3fb8b6a0e905038c31658943bc98416181a6 and b57388f519b04aafda1e63d390464db0260c3ed6.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `apps/chat/bots.py` (3 hunks)
* `apps/chat/tests/test_event_bot.py` (1 hunks)
* `apps/experiments/models.py` (1 hunks)
* `apps/service_providers/llm_service/default_models.py` (5 hunks)
* `apps/service_providers/llm_service/prompt_context.py` (1 hunks)
* `apps/service_providers/tests/test_default_models.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (1)</summary>

<details>
<summary>`**/*.py`: Do not review for Python compatibility below 3.11...</summary>

> `**/*.py`: Do not review for Python compatibility below 3.11
> 

- `apps/service_providers/llm_service/prompt_context.py`
- `apps/service_providers/tests/test_default_models.py`
- `apps/experiments/models.py`
- `apps/chat/bots.py`
- `apps/chat/tests/test_event_bot.py`
- `apps/service_providers/llm_service/default_models.py`

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: python-tests
* GitHub Check: Analyze (python)

</details>

<details>
<summary>🔇 Additional comments (15)</summary><blockquote>

<details>
<summary>apps/service_providers/llm_service/prompt_context.py (1)</summary>

`36-38`: **Performance optimization for avoiding unnecessary DB queries**

This check prevents database queries when `source_material_id` is falsy, which is a good performance improvement. The method now returns early if there's no valid ID to query.

</details>
<details>
<summary>apps/experiments/models.py (1)</summary>

`1667-1670`: <details>
<summary>✅ Verification successful</summary>

**Refactored to use EventBot for message generation**

The code has been refactored to use the dedicated `EventBot` class instead of the generic bot implementation, which better encapsulates the responsibility of generating event and reminder messages.


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for implementations of EventBot to verify its capabilities
rg -A 5 "class EventBot" --type py
```

Length of output: 415

---



</details>

**Refactored to Use EventBot for Message Generation – Verified Implementation**

The code in `apps/experiments/models.py` (lines 1667-1670) now instantiates and uses `EventBot` from `apps/chat/bots.py` to generate user messages. The ripgrep search confirms that `EventBot` is implemented with the expected message-generation capabilities, ensuring the refactor properly encapsulates the functionality.

</details>
<details>
<summary>apps/service_providers/llm_service/default_models.py (3)</summary>

`13-13`: **Added default model property to support model selection**

The `is_default` attribute allows designating specific models as defaults for each provider type.

---

`22-23`: **Configured default models for each provider**

Each provider now has a designated default model marked with `is_default=True`, which enables automatic model selection.



Also applies to: 31-32, 38-39, 57-58, 69-70, 78-79, 83-84

---

`90-92`: **Added utility function to fetch default models**

This function efficiently retrieves the first model marked as default for a given provider type, which will be useful for the EventBot when selecting models.

</details>
<details>
<summary>apps/service_providers/tests/test_default_models.py (2)</summary>

`3-7`: **Updated imports to include the new function**

Correctly updated the import statement to include the newly added `get_default_model` function.

---

`14-17`: **Added test to verify default models for all providers**

This test ensures that all providers in `DEFAULT_LLM_PROVIDER_MODELS` have at least one model marked as default, which is important for the reliability of the `get_default_model` function.

</details>
<details>
<summary>apps/chat/tests/test_event_bot.py (4)</summary>

`12-16`: **Well-structured fixture.**

The fixture creates a mock EventBot with appropriate dependencies. This provides a clean setup for your tests.

---

`19-37`: **Good test coverage for get_user_message method.**

The test properly mocks external dependencies and validates both the return value and the expected method calls. The response assertion ensures the method correctly extracts content from the AI message.

---

`39-56`: **Comprehensive testing of system prompt formatting.**

The test verifies that all expected contextual data is correctly incorporated into the system prompt. The multiple assertions ensure each component is present in the final output.

---

`58-74`: **Integration test with database access is well implemented.**

This test provides a more integrated approach by using a real session and fake LLM service. The call message validations ensure the correct prompt structure and content are being sent to the language model.

</details>
<details>
<summary>apps/chat/bots.py (4)</summary>

`317-338`: **Clear and well-structured system prompt for EventBot.**

The system prompt effectively describes the bot's purpose and provides the necessary context structure for generating messages. The use of textwrap.dedent keeps the multiline string properly formatted without unnecessary indentation.

---

`340-343`: **Simple and effective initialization.**

The constructor appropriately handles the case when experiment might be None by defaulting to the session's experiment version.

---

`361-368`: **Fallback to team's first provider is sensible.**

The property correctly handles the case when the experiment doesn't have a specified LLM provider by falling back to the team's providers.

---

`369-374`: **Efficient context gathering for system prompt.**

The method selectively retrieves only the needed context values ("participant_data" and "current_datetime"), which is efficient.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:16:36Z
user: login: SmittieC
body: Sweet
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T05:38:43Z
user: login: SmittieC
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:55:38Z
user: login: snopoke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T12:01:46Z
user: login: SmittieC
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T12:13:16Z
user: login: SmittieC
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T12:14:20Z
user: login: snopoke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T12:29:19Z
user: login: snopoke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T12:30:21Z

Review comments:
diff_hunk: @@ -550,36 +551,29 @@ def _unsupported_message_type_response(self):
             message_type=ChatMessageType.SYSTEM,
             content=f"The user sent an unsupported message type: {self.message.content_type_unparsed}",
         )
-        return self._generate_response_for_user(
+        return EventBot(self.experiment_session, self.experiment).get_user_message(
             UNSUPPORTED_MESSAGE_BOT_PROMPT.format(supported_types=self.supported_message_types)
         )
 
     def _inform_user_of_error(self):
-        """Simply tells the user that something went wrong to keep them in the loop. This method wil not raise an error
-        if something went wrong during this operation.
+        """Simply tells the user that something went wrong to keep them in the loop.
+        This method wil not raise an error if something went wrong during this operation.
path: apps/chat/channels.py
user: login: SmittieC
body: wil -> will
created_at: 2025-03-20T11:55:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -550,36 +551,29 @@ def _unsupported_message_type_response(self):
             message_type=ChatMessageType.SYSTEM,
             content=f"The user sent an unsupported message type: {self.message.content_type_unparsed}",
         )
-        return self._generate_response_for_user(
+        return EventBot(self.experiment_session, self.experiment).get_user_message(
             UNSUPPORTED_MESSAGE_BOT_PROMPT.format(supported_types=self.supported_message_types)
         )
 
     def _inform_user_of_error(self):
-        """Simply tells the user that something went wrong to keep them in the loop. This method wil not raise an error
-        if something went wrong during this operation.
+        """Simply tells the user that something went wrong to keep them in the loop.
+        This method wil not raise an error if something went wrong during this operation.
path: apps/chat/channels.py
user: login: snopoke
body: ```suggestion
        This method will not raise an error if something went wrong during this operation.
```
created_at: 2025-03-20T12:01:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -331,8 +331,12 @@ def start_conversation(self):
         self.experiment_session.update_status(SessionStatus.ACTIVE)
         # This is technically the start of the conversation
         if self.experiment.seed_message:
-            bot_response = self._generate_response_for_user(self.experiment.seed_message)
-            self.send_message_to_user(bot_response)
+            self._send_seed_message()
+
+    def _send_seed_message(self):
+        topic_bot = self.bot or get_bot(self.experiment_session, experiment=self.experiment)
+        bot_response = topic_bot.process_input(user_input=self.experiment.seed_message, save_input_to_history=False)
path: apps/chat/channels.py
user: login: SmittieC
body: Is the seed message a message that 1. OCS sends to the bot on the user's behalf or 2. OCS sends to the user on the bot's behalf? From here, it looks like 1. We should probably add some help text to make that clearer
created_at: 2025-03-20T12:11:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1715,10 +1715,15 @@ def _bot_prompt_for_user(self, instruction_prompt: str, use_experiment: Experime
         """Sends the `instruction_prompt` along with the chat history to the LLM to formulate an appropriate prompt
         message. The response from the bot will be saved to the chat history.
         """
-        from apps.chat.bots import get_bot
+        from apps.chat.bots import EventBot
 
-        bot = get_bot(self, experiment=use_experiment, disable_tools=True)
-        return bot.process_input(user_input=instruction_prompt, save_input_to_history=False)
+        bot = EventBot(self, use_experiment)
+        message = bot.get_user_message(instruction_prompt)
+        chat_message = ChatMessage.objects.create(chat=self.chat, message_type=ChatMessageType.AI, content=message)
path: apps/experiments/models.py
user: login: SmittieC
body: Don't we always want to create a chat message for the bot's message? I'm thinking for instance when we send an error message to the user (saying that something went wrong), it might be good to have that on record as well, in which case I'm wondering if the `ChatMessage` creation should move into the `get_user_message` method
created_at: 2025-03-20T12:13:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -331,8 +331,12 @@ def start_conversation(self):
         self.experiment_session.update_status(SessionStatus.ACTIVE)
         # This is technically the start of the conversation
         if self.experiment.seed_message:
-            bot_response = self._generate_response_for_user(self.experiment.seed_message)
-            self.send_message_to_user(bot_response)
+            self._send_seed_message()
+
+    def _send_seed_message(self):
+        topic_bot = self.bot or get_bot(self.experiment_session, experiment=self.experiment)
+        bot_response = topic_bot.process_input(user_input=self.experiment.seed_message, save_input_to_history=False)
path: apps/chat/channels.py
user: login: snopoke
body: yes, it is 1 - a message we send to the bot on the user's behalf to generate an initial response.
created_at: 2025-03-20T12:29:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1715,10 +1715,15 @@ def _bot_prompt_for_user(self, instruction_prompt: str, use_experiment: Experime
         """Sends the `instruction_prompt` along with the chat history to the LLM to formulate an appropriate prompt
         message. The response from the bot will be saved to the chat history.
         """
-        from apps.chat.bots import get_bot
+        from apps.chat.bots import EventBot
 
-        bot = get_bot(self, experiment=use_experiment, disable_tools=True)
-        return bot.process_input(user_input=instruction_prompt, save_input_to_history=False)
+        bot = EventBot(self, use_experiment)
+        message = bot.get_user_message(instruction_prompt)
+        chat_message = ChatMessage.objects.create(chat=self.chat, message_type=ChatMessageType.AI, content=message)
path: apps/experiments/models.py
user: login: snopoke
body: Since this is used differently in different places I think it's fine to leave it as is for now. I also don't want to push more logic into EventBot.
created_at: 2025-03-20T12:30:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: snopoke
created_at: 2025-03-12T16:13:42Z
body: @coderabbit review
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-03-12T16:16:32Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

The pull request introduces a new bot class, **EventBot**, into the chat application. This class aggregates contextual information—including conversation history, participant data, and current datetime—to format a system prompt that guides the generation of user messages. EventBot retrieves its language model provider from the associated experiment or team settings, and it uses a helper function to select a default model based on an added `is_default` attribute in the provider models. In addition, the ExperimentSession model has been updated to instantiate EventBot and use its `get_user_message` method instead of the previous implementation. New test cases validate the functionality and integration of EventBot while changes in prompt context handling prevent unnecessary database queries.

## Sequence Diagram(s)

```mermaid
sequenceDiagram
  participant U as User
  participant EB as EventBot
  participant LP as LLM Provider
  participant CM as Chat Model

  U->>EB: get_user_message(event_prompt)
  EB->>EB: Retrieve llm_provider & format system_prompt
  EB->>LP: Get default model (using get_default_model)
  EB->>CM: Invoke chat(model, combined prompt)
  CM-->>EB: Return response content
  EB-->>U: Return generated message
```

```mermaid
sequenceDiagram
  participant U as User
  participant ES as ExperimentSession
  participant EB as EventBot

  U->>ES: Provide instruction prompt
  ES->>EB: Instantiate EventBot with session and experiment
  EB->>EB: Process prompt via get_user_message
  EB-->>ES: Return generated message
  ES-->>U: Deliver generated message
```

</details>

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=dimagi/open-chat-studio&utm_content=1318):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov-commenter
created_at: 2025-03-12T16:20:48Z
body: ## [Codecov](https://app.codecov.io/gh/dimagi/open-chat-studio/pull/1318?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dimagi) Report
Attention: Patch coverage is `95.56962%` with `7 lines` in your changes missing coverage. Please review.

:white_check_mark: All tests successful. No failed tests found.

| [Files with missing lines](https://app.codecov.io/gh/dimagi/open-chat-studio/pull/1318?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dimagi) | Patch % | Lines |
|---|---|---|
| [apps/chat/channels.py](https://app.codecov.io/gh/dimagi/open-chat-studio/pull/1318?src=pr&el=tree&filepath=apps%2Fchat%2Fchannels.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dimagi#diff-YXBwcy9jaGF0L2NoYW5uZWxzLnB5) | 60.00% | [4 Missing :warning: ](https://app.codecov.io/gh/dimagi/open-chat-studio/pull/1318?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dimagi) |
| [apps/chat/bots.py](https://app.codecov.io/gh/dimagi/open-chat-studio/pull/1318?src=pr&el=tree&filepath=apps%2Fchat%2Fbots.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dimagi#diff-YXBwcy9jaGF0L2JvdHMucHk=) | 91.17% | [3 Missing :warning: ](https://app.codecov.io/gh/dimagi/open-chat-studio/pull/1318?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dimagi) |


:loudspeaker: Thoughts on this report? [Let us know!](https://about.codecov.io/pull-request-comment-report/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dimagi)

<details><summary>🚀 New features to boost your workflow: </summary>

- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: snopoke
created_at: 2025-03-20T12:10:18Z
body: @SmittieC do you have any other feedback or is this ready to go?
author_association: CONTRIBUTOR
user: login: SmittieC
created_at: 2025-03-20T12:14:09Z
body: > @SmittieC do you have any other feedback or is this ready to go?

@snopoke Feel free to merge. We can discuss my comments and make changes afterwards if need be
author_association: CONTRIBUTOR