import json from collections.abc import Iterator from datetime import datetime from decimal import Decimal from unittest.mock import MagicMock import pytest from sqlalchemy.engine import Engine from sqlalchemy.orm import Session, scoped_session import models.model as model_module import services.message_service as service_module from core.app.entities.app_invoke_entities import InvokeFrom from graphon.model_runtime.entities.model_entities import ModelType from models.account import Account, AccountStatus from models.enums import ( ConversationFromSource, EndUserType, FeedbackFromSource, FeedbackRating, ) from models.model import ( App, AppAnnotationSetting, AppMode, AppModelConfig, Conversation, EndUser, Message, MessageFeedback, ) from repositories.sqlalchemy_execution_extra_content_repository import SQLAlchemyExecutionExtraContentRepository from services.errors.message import ( FirstMessageNotExistsError, LastMessageNotExistsError, MessageNotExistsError, SuggestedQuestionsAfterAnswerDisabledError, ) from services.message_service import MessageService, attach_message_extra_contents SQLITE_MODELS = (Conversation, Message, MessageFeedback, AppModelConfig, AppAnnotationSetting) pytestmark = [ pytest.mark.usefixtures("sqlite_session"), pytest.mark.parametrize("sqlite_session", [SQLITE_MODELS], indirect=True), ] class _DatabaseBinding: """Expose the SQLite engine and shared session through the production DB interface.""" engine: Engine session: scoped_session[Session] def __init__(self, engine: Engine, session: scoped_session[Session]) -> None: self.engine = engine self.session = session class MessageServiceTestDataFactory: """Create real service inputs and persistent message-domain rows.""" @staticmethod def create_app( app_id: str = "app-123", mode: AppMode = AppMode.ADVANCED_CHAT, tenant_id: str = "tenant-123", ) -> App: return App( id=app_id, tenant_id=tenant_id, name="Test App", description="", mode=mode, enable_site=True, enable_api=True, max_active_requests=0, ) @staticmethod def create_end_user(user_id: str = "user-456") -> EndUser: return EndUser( id=user_id, tenant_id="tenant-123", app_id="app-123", type=EndUserType.SERVICE_API, session_id="session-789", ) @staticmethod def create_account(user_id: str = "account-123") -> Account: account = Account(name="Admin", email="admin@example.com", status=AccountStatus.ACTIVE) account.id = user_id return account @staticmethod def create_conversation( conversation_id: str = "conv-001", app_id: str = "app-123", *, app_model_config_id: str | None = None, override_model_configs: str | None = None, ) -> Conversation: conversation = Conversation( id=conversation_id, app_id=app_id, app_model_config_id=app_model_config_id, override_model_configs=override_model_configs, mode=AppMode.CHAT, name="Test conversation", status="normal", from_source=ConversationFromSource.API, from_end_user_id="user-456", ) conversation._inputs = {} return conversation @staticmethod def create_message( message_id: str = "msg-001", conversation_id: str = "conv-001", app_id: str = "app-123", *, created_at: datetime | None = None, from_source: ConversationFromSource = ConversationFromSource.API, from_end_user_id: str | None = "user-456", from_account_id: str | None = None, ) -> Message: message = Message( id=message_id, app_id=app_id, conversation_id=conversation_id, query="What is AI?", message={"role": "user", "content": "What is AI?"}, answer="AI stands for Artificial Intelligence.", message_unit_price=Decimal("0.0001"), answer_unit_price=Decimal("0.0002"), currency="USD", from_source=from_source, from_end_user_id=from_end_user_id, from_account_id=from_account_id, ) message._inputs = {} timestamp = created_at or datetime.now() message.created_at = timestamp message.updated_at = timestamp return message @staticmethod def create_feedback( feedback_id: str, message: Message, *, source: FeedbackFromSource, rating: FeedbackRating = FeedbackRating.LIKE, ) -> MessageFeedback: feedback = MessageFeedback( app_id=message.app_id, conversation_id=message.conversation_id, message_id=message.id, rating=rating, from_source=source, from_end_user_id="user-456" if source == FeedbackFromSource.USER else None, from_account_id="account-123" if source == FeedbackFromSource.ADMIN else None, ) feedback.id = feedback_id return feedback @pytest.fixture def factory() -> MessageServiceTestDataFactory: return MessageServiceTestDataFactory() @pytest.fixture(autouse=True) def database_boundaries( monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine, sqlite_session: Session, ) -> Iterator[None]: """Bind global model properties and service-owned factories to the shared SQLite session.""" sessions = scoped_session(lambda: sqlite_session) database = _DatabaseBinding(engine=sqlite_engine, session=sessions) monkeypatch.setattr(service_module, "db", database) monkeypatch.setattr(model_module, "db", database) try: yield finally: sessions.remove() @pytest.fixture def empty_extra_content_repository(monkeypatch: pytest.MonkeyPatch) -> MagicMock: repository = MagicMock() repository.get_by_message_ids.side_effect = lambda message_ids: [[] for _ in message_ids] monkeypatch.setattr(service_module, "_create_execution_extra_content_repository", lambda: repository) return repository def _persist(session: Session, *records: object) -> None: session.add_all(records) session.commit() def _patch_conversation(monkeypatch: pytest.MonkeyPatch, conversation: Conversation) -> MagicMock: get_conversation = MagicMock(return_value=conversation) monkeypatch.setattr(service_module.ConversationService, "get_conversation", get_conversation) return get_conversation class TestMessageServicePaginationByFirstId: """Verify cursor pagination using persisted message timestamps and IDs.""" @pytest.mark.parametrize(("user", "conversation_id"), [(None, "conv-001"), ("end_user", "")]) def test_early_return( self, user: str | None, conversation_id: str, factory: MessageServiceTestDataFactory, sqlite_session: Session, ) -> None: result = MessageService.pagination_by_first_id( app_model=factory.create_app(), user=factory.create_end_user() if user else None, conversation_id=conversation_id, first_id=None, limit=10, session=sqlite_session, ) assert result.data == [] assert result.limit == 10 assert result.has_more is False @pytest.mark.parametrize( ("order", "expected_ids"), [ ("desc", ["msg-004", "msg-003", "msg-002", "msg-001", "msg-000"]), ("asc", ["msg-000", "msg-001", "msg-002", "msg-003", "msg-004"]), ], ) def test_orders_persisted_messages( self, order: str, expected_ids: list[str], monkeypatch: pytest.MonkeyPatch, factory: MessageServiceTestDataFactory, sqlite_session: Session, empty_extra_content_repository: MagicMock, ) -> None: conversation = factory.create_conversation() messages = [ factory.create_message(f"msg-{index:03d}", created_at=datetime(2024, 1, 1, 12, index)) for index in range(5) ] _persist(sqlite_session, conversation, *messages) _patch_conversation(monkeypatch, conversation) result = MessageService.pagination_by_first_id( app_model=factory.create_app(), user=factory.create_end_user(), conversation_id=conversation.id, first_id=None, limit=10, order=order, session=sqlite_session, ) assert [message.id for message in result.data] == expected_ids assert result.has_more is False def test_first_id_excludes_cursor_and_newer_messages( self, monkeypatch: pytest.MonkeyPatch, factory: MessageServiceTestDataFactory, sqlite_session: Session, empty_extra_content_repository: MagicMock, ) -> None: conversation = factory.create_conversation() messages = [ factory.create_message(f"msg-{index:03d}", created_at=datetime(2024, 1, 1, 12, index)) for index in range(7) ] _persist(sqlite_session, conversation, *messages) _patch_conversation(monkeypatch, conversation) result = MessageService.pagination_by_first_id( app_model=factory.create_app(), user=factory.create_end_user(), conversation_id=conversation.id, first_id="msg-005", limit=10, order="desc", session=sqlite_session, ) assert [message.id for message in result.data] == [f"msg-{index:03d}" for index in range(4, -1, -1)] def test_missing_first_id_raises( self, monkeypatch: pytest.MonkeyPatch, factory: MessageServiceTestDataFactory, sqlite_session: Session, ) -> None: conversation = factory.create_conversation() _persist(sqlite_session, conversation) _patch_conversation(monkeypatch, conversation) with pytest.raises(FirstMessageNotExistsError): MessageService.pagination_by_first_id( app_model=factory.create_app(), user=factory.create_end_user(), conversation_id=conversation.id, first_id="missing", limit=10, session=sqlite_session, ) def test_has_more_trims_oldest_extra_row( self, monkeypatch: pytest.MonkeyPatch, factory: MessageServiceTestDataFactory, sqlite_session: Session, empty_extra_content_repository: MagicMock, ) -> None: conversation = factory.create_conversation() messages = [ factory.create_message(f"msg-{index:03d}", created_at=datetime(2024, 1, 1, 12, index)) for index in range(11) ] _persist(sqlite_session, conversation, *messages) _patch_conversation(monkeypatch, conversation) result = MessageService.pagination_by_first_id( app_model=factory.create_app(), user=factory.create_end_user(), conversation_id=conversation.id, first_id=None, limit=10, order="desc", session=sqlite_session, ) assert len(result.data) == 10 assert result.has_more is True assert result.data[-1].id == "msg-001" def test_empty_conversation( self, monkeypatch: pytest.MonkeyPatch, factory: MessageServiceTestDataFactory, sqlite_session: Session, empty_extra_content_repository: MagicMock, ) -> None: conversation = factory.create_conversation() _persist(sqlite_session, conversation) _patch_conversation(monkeypatch, conversation) result = MessageService.pagination_by_first_id( app_model=factory.create_app(), user=factory.create_end_user(), conversation_id=conversation.id, first_id=None, limit=10, session=sqlite_session, ) assert result.data == [] assert result.has_more is False class TestMessageServicePaginationByLastId: """Verify reverse cursor, conversation, and include-ID filtering.""" def test_no_user(self, factory: MessageServiceTestDataFactory, sqlite_session: Session) -> None: result = MessageService.pagination_by_last_id( app_model=factory.create_app(), user=None, last_id=None, limit=10, session=sqlite_session ) assert result.data == [] assert result.limit == 10 assert result.has_more is False def test_without_last_id(self, factory: MessageServiceTestDataFactory, sqlite_session: Session) -> None: messages = [ factory.create_message(f"msg-{index:03d}", created_at=datetime(2024, 1, 1, 12, index)) for index in range(5) ] _persist(sqlite_session, *messages) result = MessageService.pagination_by_last_id( app_model=factory.create_app(), user=factory.create_end_user(), last_id=None, limit=10, session=sqlite_session, ) assert [message.id for message in result.data] == [f"msg-{index:03d}" for index in range(4, -1, -1)] assert result.has_more is False def test_last_id_returns_older_rows(self, factory: MessageServiceTestDataFactory, sqlite_session: Session) -> None: messages = [ factory.create_message(f"msg-{index:03d}", created_at=datetime(2024, 1, 1, 12, index)) for index in range(7) ] _persist(sqlite_session, *messages) result = MessageService.pagination_by_last_id( app_model=factory.create_app(), user=factory.create_end_user(), last_id="msg-005", limit=10, session=sqlite_session, ) assert [message.id for message in result.data] == [f"msg-{index:03d}" for index in range(4, -1, -1)] def test_missing_last_id_raises(self, factory: MessageServiceTestDataFactory, sqlite_session: Session) -> None: with pytest.raises(LastMessageNotExistsError): MessageService.pagination_by_last_id( app_model=factory.create_app(), user=factory.create_end_user(), last_id="missing", limit=10, session=sqlite_session, ) def test_conversation_filter( self, monkeypatch: pytest.MonkeyPatch, factory: MessageServiceTestDataFactory, sqlite_session: Session, ) -> None: conversation = factory.create_conversation() other_conversation = factory.create_conversation("conv-002") matching = factory.create_message("matching", conversation_id=conversation.id) excluded = factory.create_message("excluded", conversation_id=other_conversation.id) _persist(sqlite_session, conversation, other_conversation, matching, excluded) get_conversation = _patch_conversation(monkeypatch, conversation) result = MessageService.pagination_by_last_id( app_model=factory.create_app(), user=factory.create_end_user(), last_id=None, limit=10, conversation_id=conversation.id, session=sqlite_session, ) assert [message.id for message in result.data] == [matching.id] get_conversation.assert_called_once() def test_include_ids_filter(self, factory: MessageServiceTestDataFactory, sqlite_session: Session) -> None: messages = [ factory.create_message(f"msg-{index:03d}", created_at=datetime(2024, 1, 1, 12, index)) for index in range(4) ] _persist(sqlite_session, *messages) result = MessageService.pagination_by_last_id( app_model=factory.create_app(), user=factory.create_end_user(), last_id=None, limit=10, include_ids=["msg-001", "msg-003"], session=sqlite_session, ) assert [message.id for message in result.data] == ["msg-003", "msg-001"] def test_has_more(self, factory: MessageServiceTestDataFactory, sqlite_session: Session) -> None: messages = [ factory.create_message(f"msg-{index:03d}", created_at=datetime(2024, 1, 1, 12, index)) for index in range(11) ] _persist(sqlite_session, *messages) result = MessageService.pagination_by_last_id( app_model=factory.create_app(), user=factory.create_end_user(), last_id=None, limit=10, session=sqlite_session, ) assert len(result.data) == 10 assert result.has_more is True class TestMessageServiceUtilities: def test_attach_message_extra_contents_empty(self) -> None: attach_message_extra_contents([]) def test_attach_message_extra_contents( self, monkeypatch: pytest.MonkeyPatch, factory: MessageServiceTestDataFactory, ) -> None: messages = [factory.create_message("msg-1"), factory.create_message("msg-2")] content_one = MagicMock() content_one.model_dump.return_value = {"key": "value1"} content_two = MagicMock() content_two.model_dump.return_value = {"key": "value2"} repository = MagicMock() repository.get_by_message_ids.return_value = [[content_one], [content_two]] monkeypatch.setattr(service_module, "_create_execution_extra_content_repository", lambda: repository) attach_message_extra_contents(messages) assert messages[0].extra_contents == [{"key": "value1"}] assert messages[1].extra_contents == [{"key": "value2"}] def test_attach_message_extra_contents_missing_list( self, monkeypatch: pytest.MonkeyPatch, factory: MessageServiceTestDataFactory, ) -> None: message = factory.create_message("msg-1") repository = MagicMock() repository.get_by_message_ids.return_value = [] monkeypatch.setattr(service_module, "_create_execution_extra_content_repository", lambda: repository) attach_message_extra_contents([message]) assert message.extra_contents == [] def test_create_execution_extra_content_repository_uses_sqlite_factory(self, sqlite_engine: Engine) -> None: repository = service_module._create_execution_extra_content_repository() assert isinstance(repository, SQLAlchemyExecutionExtraContentRepository) assert repository._session_maker.kw["bind"] is sqlite_engine with repository._session_maker() as session: assert isinstance(session, Session) class TestMessageServiceGetMessage: @pytest.mark.parametrize("actor", ["end_user", "account"]) def test_identity_scoped_success( self, actor: str, factory: MessageServiceTestDataFactory, sqlite_session: Session, ) -> None: if actor == "end_user": user: Account | EndUser = factory.create_end_user("end-user-123") message = factory.create_message( "msg-123", from_end_user_id=user.id, from_account_id=None, from_source=ConversationFromSource.API ) else: user = factory.create_account("account-123") message = factory.create_message( "msg-123", from_end_user_id=None, from_account_id=user.id, from_source=ConversationFromSource.CONSOLE, ) distractor = factory.create_message("wrong-app", app_id="app-456") _persist(sqlite_session, message, distractor) result = MessageService.get_message( app_model=factory.create_app(), user=user, message_id=message.id, session=sqlite_session ) assert result.id == message.id def test_not_found(self, factory: MessageServiceTestDataFactory, sqlite_session: Session) -> None: with pytest.raises(MessageNotExistsError): MessageService.get_message( app_model=factory.create_app(), user=factory.create_end_user(), message_id="missing", session=sqlite_session, ) class TestMessageServiceFeedback: def test_create_new_end_user_feedback( self, factory: MessageServiceTestDataFactory, sqlite_session: Session, sqlite_engine: Engine, ) -> None: user = factory.create_end_user() message = factory.create_message("msg-123") _persist(sqlite_session, message) feedback = MessageService.create_feedback( app_model=factory.create_app(), message_id=message.id, user=user, rating=FeedbackRating.LIKE, content="Good answer", session=sqlite_session, ) with Session(sqlite_engine) as verification_session: persisted = verification_session.get(MessageFeedback, feedback.id) assert persisted is not None assert persisted.rating == FeedbackRating.LIKE assert persisted.content == "Good answer" assert persisted.from_source == FeedbackFromSource.USER def test_update_account_feedback( self, factory: MessageServiceTestDataFactory, sqlite_session: Session, sqlite_engine: Engine, ) -> None: user = factory.create_account() message = factory.create_message( "msg-123", from_source=ConversationFromSource.CONSOLE, from_end_user_id=None, from_account_id=user.id, ) feedback = factory.create_feedback("feedback-1", message, source=FeedbackFromSource.ADMIN) _persist(sqlite_session, message, feedback) result = MessageService.create_feedback( app_model=factory.create_app(), message_id=message.id, user=user, rating=FeedbackRating.DISLIKE, content="Bad answer", session=sqlite_session, ) assert result.id == feedback.id with Session(sqlite_engine) as verification_session: persisted = verification_session.get(MessageFeedback, feedback.id) assert persisted is not None assert persisted.rating == FeedbackRating.DISLIKE assert persisted.content == "Bad answer" def test_delete_feedback( self, factory: MessageServiceTestDataFactory, sqlite_session: Session, sqlite_engine: Engine, ) -> None: user = factory.create_end_user() message = factory.create_message("msg-123") feedback = factory.create_feedback("feedback-1", message, source=FeedbackFromSource.USER) _persist(sqlite_session, message, feedback) MessageService.create_feedback( app_model=factory.create_app(), message_id=message.id, user=user, rating=None, content=None, session=sqlite_session, ) with Session(sqlite_engine) as verification_session: assert verification_session.get(MessageFeedback, feedback.id) is None def test_get_all_feedbacks_is_app_scoped_and_paginated( self, factory: MessageServiceTestDataFactory, sqlite_session: Session, ) -> None: message = factory.create_message("msg-123") newest = factory.create_feedback("feedback-new", message, source=FeedbackFromSource.USER) oldest = factory.create_feedback("feedback-old", message, source=FeedbackFromSource.USER) other_message = factory.create_message("other-msg", app_id="app-456") other_app = factory.create_feedback("feedback-other", other_message, source=FeedbackFromSource.USER) newest.created_at = datetime(2024, 1, 2) oldest.created_at = datetime(2024, 1, 1) other_app.created_at = datetime(2024, 1, 3) _persist(sqlite_session, newest, oldest, other_app) result = MessageService.get_all_messages_feedbacks( app_model=factory.create_app(), page=1, limit=1, session=sqlite_session ) assert [record["id"] for record in result] == [newest.id] class TestMessageServiceSuggestedQuestions: @staticmethod def _chat_boundaries( monkeypatch: pytest.MonkeyPatch, conversation: Conversation, ) -> tuple[MagicMock, MagicMock, MagicMock]: message = MagicMock() message.conversation_id = conversation.id monkeypatch.setattr(service_module.MessageService, "get_message", MagicMock(return_value=message)) monkeypatch.setattr( service_module.ConversationService, "get_conversation", MagicMock(return_value=conversation) ) model_manager = MagicMock() monkeypatch.setattr(service_module.ModelManager, "for_tenant", MagicMock(return_value=model_manager)) memory = MagicMock() memory.return_value.get_history_prompt_text.return_value = "histories" monkeypatch.setattr(service_module, "TokenBufferMemory", memory) llm_generator = MagicMock() llm_generator.generate_suggested_questions_after_answer.return_value = ["Q1?"] monkeypatch.setattr(service_module, "LLMGenerator", llm_generator) monkeypatch.setattr(service_module, "TraceQueueManager", MagicMock()) return model_manager, memory, llm_generator def test_user_none(self, factory: MessageServiceTestDataFactory, sqlite_session: Session) -> None: with pytest.raises(ValueError, match="user cannot be None"): MessageService.get_suggested_questions_after_answer( app_model=factory.create_app(), user=None, message_id="msg-123", invoke_from=InvokeFrom.WEB_APP, session=sqlite_session, ) def test_advanced_chat_success( self, monkeypatch: pytest.MonkeyPatch, factory: MessageServiceTestDataFactory, sqlite_session: Session, ) -> None: conversation = factory.create_conversation() _, _, llm_generator = self._chat_boundaries(monkeypatch, conversation) workflow = MagicMock() workflow.features_dict = {"suggested_questions_after_answer": {"enabled": True}} workflow_service = MagicMock() workflow_service.return_value.get_published_workflow.return_value = workflow monkeypatch.setattr(service_module, "WorkflowService", workflow_service) app_config_manager = MagicMock() app_config_manager.get_app_config.return_value.additional_features.suggested_questions_after_answer = True monkeypatch.setattr(service_module, "AdvancedChatAppConfigManager", app_config_manager) result = MessageService.get_suggested_questions_after_answer( app_model=factory.create_app(mode=AppMode.ADVANCED_CHAT), user=factory.create_end_user(), message_id="msg-123", invoke_from=InvokeFrom.WEB_APP, session=sqlite_session, ) assert result == ["Q1?"] llm_generator.generate_suggested_questions_after_answer.assert_called_once() @pytest.mark.parametrize( ("config", "expected_prompt", "expected_model"), [ ({"enabled": True}, None, None), ( { "enabled": True, "prompt": "custom prompt", "model": { "provider": "openai", "name": "gpt-4o-mini", "completion_params": {"max_tokens": 2048, "temperature": 0.1}, }, }, "custom prompt", { "provider": "openai", "name": "gpt-4o-mini", "completion_params": {"max_tokens": 2048, "temperature": 0.1}, }, ), ( {"enabled": True, "model": {"provider": "openai", "name": "invalid-model"}}, None, {"provider": "openai", "name": "invalid-model"}, ), ], ) def test_chat_app_uses_persisted_model_config( self, config: dict[str, object], expected_prompt: str | None, expected_model: dict[str, object] | None, monkeypatch: pytest.MonkeyPatch, factory: MessageServiceTestDataFactory, sqlite_session: Session, ) -> None: app_model_config = AppModelConfig( app_id="app-123", suggested_questions_after_answer=json.dumps(config), ) app_model_config.id = "config-1" conversation = factory.create_conversation(app_model_config_id=app_model_config.id) _persist(sqlite_session, app_model_config) model_manager, memory, llm_generator = self._chat_boundaries(monkeypatch, conversation) result = MessageService.get_suggested_questions_after_answer( app_model=factory.create_app(mode=AppMode.CHAT), user=factory.create_end_user(), message_id="msg-123", invoke_from=InvokeFrom.WEB_APP, session=sqlite_session, ) assert result == ["Q1?"] model_manager.get_default_model_instance.assert_called_once_with( tenant_id="tenant-123", model_type=ModelType.LLM ) memory.assert_called_once_with( conversation=conversation, model_instance=model_manager.get_default_model_instance.return_value, ) llm_generator.generate_suggested_questions_after_answer.assert_called_once_with( tenant_id="tenant-123", histories="histories", instruction_prompt=expected_prompt, model_config=expected_model, ) def test_chat_app_uses_compatible_override_model_config( self, monkeypatch: pytest.MonkeyPatch, factory: MessageServiceTestDataFactory, sqlite_session: Session, ) -> None: override = { "model": {"provider": "openai", "name": "gpt-4o-mini", "mode": "chat"}, "suggested_questions_after_answer": {"enabled": True, "prompt": "legacy prompt"}, } conversation = factory.create_conversation(override_model_configs=json.dumps(override)) _, _, llm_generator = self._chat_boundaries(monkeypatch, conversation) result = MessageService.get_suggested_questions_after_answer( app_model=factory.create_app(mode=AppMode.CHAT), user=factory.create_end_user(), message_id="msg-123", invoke_from=InvokeFrom.WEB_APP, session=sqlite_session, ) assert result == ["Q1?"] llm_generator.generate_suggested_questions_after_answer.assert_called_once_with( tenant_id="tenant-123", histories="histories", instruction_prompt="legacy prompt", model_config=None, ) def test_disabled_error( self, monkeypatch: pytest.MonkeyPatch, factory: MessageServiceTestDataFactory, sqlite_session: Session, ) -> None: conversation = factory.create_conversation() self._chat_boundaries(monkeypatch, conversation) workflow = MagicMock() workflow_service = MagicMock() workflow_service.return_value.get_published_workflow.return_value = workflow monkeypatch.setattr(service_module, "WorkflowService", workflow_service) app_config_manager = MagicMock() app_config_manager.get_app_config.return_value.additional_features.suggested_questions_after_answer = False monkeypatch.setattr(service_module, "AdvancedChatAppConfigManager", app_config_manager) with pytest.raises(SuggestedQuestionsAfterAnswerDisabledError): MessageService.get_suggested_questions_after_answer( app_model=factory.create_app(mode=AppMode.ADVANCED_CHAT), user=factory.create_end_user(), message_id="msg-123", invoke_from=InvokeFrom.WEB_APP, session=sqlite_session, )