From c5b6cccafdb713506b0a0a0e74eb589eba87126d Mon Sep 17 00:00:00 2001 From: Tahier Hussain Date: Tue, 5 May 2026 16:39:12 +0530 Subject: [PATCH 1/3] refactor(backend): migrate chat_intent from client-supplied to AI-detected The AI service now classifies chat intent server-side (via the gatekeeper) and emits a dedicated CHAT_INTENT (event_type=5) stream message before any prompt_response chunks. Visitran-cloud no longer reads chat_intent_id from the POST /chat request body and no longer forwards it in the outbound LLM payload. Instead, the backend extracts chat_intent from the inbound stream, persists it on ChatMessage on first sight, and pushes chat_intent_name to the frontend via socket so the UI can branch (Apply / Run SQL / etc.) in real time. Pre-charge token-balance check now defaults to TRANSFORM (worst-case cost) since intent isn't yet known at request time. Both ChatSerializer and ChatMessageSerializer surface a new chat_intent_name field so the frontend can render historical messages without an id->name lookup. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../application/context/chat_ai_context.py | 11 +++- .../context/chat_message_context.py | 12 +--- .../application/context/llm_context.py | 61 ++++++++++++++----- .../backend/core/routers/chat/serializers.py | 4 ++ backend/backend/core/routers/chat/views.py | 19 ++---- .../serializers/chat_message_serializer.py | 2 + backend/backend/core/web_socket.py | 1 + 7 files changed, 69 insertions(+), 41 deletions(-) diff --git a/backend/backend/application/context/chat_ai_context.py b/backend/backend/application/context/chat_ai_context.py index 8506dca..be966f5 100644 --- a/backend/backend/application/context/chat_ai_context.py +++ b/backend/backend/application/context/chat_ai_context.py @@ -326,9 +326,18 @@ def _process_completed(self, *args, **kwargs): # On successful completion, Closing the event thread raise StopIteration + def _process_chat_intent(self, *args, **kwargs): + send_socket_message( + sid=kwargs["sid"], + channel_id=kwargs["channel_id"], + chat_id=kwargs["chat_id"], + chat_message_id=kwargs["chat_message_id"], + chat_intent_name=kwargs.get("chat_intent"), + ) + def process_event(self, *args, **kwargs): supported_events = ["thought_chain", "prompt_response", - "summary", "chat_name", "completed"] + "summary", "chat_name", "completed", "chat_intent"] event_type = kwargs.get("event_type") if event_type not in supported_events: raise ValueError(f"Unsupported event type: {event_type}") diff --git a/backend/backend/application/context/chat_message_context.py b/backend/backend/application/context/chat_message_context.py index aeb41d9..ddd11de 100644 --- a/backend/backend/application/context/chat_message_context.py +++ b/backend/backend/application/context/chat_message_context.py @@ -144,37 +144,30 @@ def persist_prompt( llm_model_architect: str, llm_model_developer: str, generated_chat_res_id: str = None, - chat_intent_id: str = None, chat_id: str = None, user=None, ) -> ChatMessage: """ Create a new prompt within a Chat. If chat_id is None, create a new Chat. Return the chat_message_id of the newly created ChatMessage. + chat_intent is left null here; the AI service auto-detects it and the + backend persists it on the message when the response arrives. """ if not prompt.strip(): raise InvalidChatPrompt() - chat_intent = None transformation_type = 'TRANSFORM' if discussion_type == 'GENERATE' else 'DISCUSSION' - if chat_intent_id: - try: - chat_intent = ChatIntent.objects.get(chat_intent_id=chat_intent_id) - except ChatIntent.DoesNotExist: - chat_intent = None if not chat_id: chat = Chat.objects.create( project=self.project_instance, chat_name="Untitled Chat", - chat_intent=chat_intent, llm_model_architect=llm_model_architect, llm_model_developer=llm_model_developer, user=user, ) else: chat = self._get_chat_or_raise(chat_id=chat_id, must_be_active=True) - chat.chat_intent = chat_intent chat.llm_model_architect = llm_model_architect chat.llm_model_developer = llm_model_developer chat.discussion_type = discussion_type @@ -185,7 +178,6 @@ def persist_prompt( chat_message = ChatMessage.objects.create( chat=chat, prompt=prompt, - chat_intent=chat_intent, llm_model_architect=llm_model_architect, llm_model_developer=llm_model_developer, discussion_type= discussion_type, diff --git a/backend/backend/application/context/llm_context.py b/backend/backend/application/context/llm_context.py index b9c563e..d58ffd2 100644 --- a/backend/backend/application/context/llm_context.py +++ b/backend/backend/application/context/llm_context.py @@ -1,6 +1,6 @@ import json import logging -from typing import Any +from typing import Any, Optional import eventlet import redis @@ -52,16 +52,48 @@ def create_redis_xgroup(self, channel_id, group_id): else: raise + def _resolve_chat_intent(self, chat_message_id: str, data: dict, content: Any) -> Optional[str]: + """ + Pull the AI-detected chat_intent out of the inbound payload and persist it + to ChatMessage.chat_intent the first time we see it for a given message. + Subsequent events re-use the cached value. + """ + if not hasattr(self, "_chat_intent_by_msg"): + self._chat_intent_by_msg = {} + + cached = self._chat_intent_by_msg.get(chat_message_id) + if cached: + return cached + + intent_name = data.get("chat_intent") + if not intent_name and isinstance(content, dict): + intent_name = content.get("chat_intent") + if not intent_name: + return None + + try: + from backend.core.models.chat_intent import ChatIntent + from backend.core.models.chat_message import ChatMessage + chat_intent = ChatIntent.objects.get(name=intent_name) + chat_message = ChatMessage.objects.get(chat_message_id=chat_message_id) + chat_message.chat_intent = chat_intent + chat_message.save(update_fields=["chat_intent"]) + except Exception as e: + logging.error(f"Failed to persist chat_intent={intent_name}: {e}") + + self._chat_intent_by_msg[chat_message_id] = intent_name + return intent_name + def process_message( self, sid: str, channel_id: str, chat_id: str, - chat_intent: str, payload: dict[str, Any], discussion_status: str ): data = json.loads(payload["data"]) + print("\n=============\n", data, "\n==========\n") if payload.get("type") == "status" and payload.get("status") == "failed": payload = json.loads(payload["data"]) if payload and "error_message" in payload: @@ -75,6 +107,7 @@ def process_message( 2: "summary", 3: "chat_name", 4: "completed", + 5: "chat_intent", 99: "stop", } @@ -83,6 +116,8 @@ def process_message( chat_message_id = data["chat_message_id"] content = data["content"] + chat_intent = self._resolve_chat_intent(chat_message_id, data, content) + if event_type == "chat_name": self.chat_name = data["content"] self.persist_response( @@ -120,7 +155,7 @@ def _validate_message(self, group_id, channel_id): ) return messages - def _handle_redis_message(self, sid, channel_id, chat_id, chat_intent, group_id, messages, discussion_status: str): + def _handle_redis_message(self, sid, channel_id, chat_id, group_id, messages, discussion_status: str): for _, msg_list in messages: for message_id, payload in msg_list: logging.info(f" === Message ID: {message_id} ===") @@ -129,7 +164,6 @@ def _handle_redis_message(self, sid, channel_id, chat_id, chat_intent, group_id, sid=sid, channel_id=channel_id, chat_id=chat_id, - chat_intent=chat_intent, payload=payload, discussion_status=discussion_status ) @@ -138,7 +172,7 @@ def _handle_redis_message(self, sid, channel_id, chat_id, chat_intent, group_id, self.redis_client.xack(channel_id, group_id, message_id) def __stream_listener( - self, sid: str, channel_id: str, chat_id: str, chat_message_id: str, chat_intent: str, group_id: str, discussion_status: str + self, sid: str, channel_id: str, chat_id: str, chat_message_id: str, group_id: str, discussion_status: str ): while True: @@ -148,7 +182,7 @@ def __stream_listener( if not messages: continue - self._handle_redis_message(sid, channel_id, chat_id, chat_intent, group_id, messages, discussion_status) + self._handle_redis_message(sid, channel_id, chat_id, group_id, messages, discussion_status) except redis.exceptions.RedisError as e: logging.error(f"[REDIS ERROR] {e}") @@ -196,15 +230,15 @@ def __stream_listener( ) break - def listen_to_redis_stream(self, sid: str, channel_id: str, chat_id: str, chat_message_id: str, chat_intent: str, discussion_status: str): + def listen_to_redis_stream(self, sid: str, channel_id: str, chat_id: str, chat_message_id: str, discussion_status: str): """Listens to the Redis stream from llm server and processes the messages.""" group_id = f"group_{chat_id}_{chat_message_id}" self.create_redis_xgroup(channel_id, group_id) - self.__stream_listener(sid, channel_id, chat_id, chat_message_id, chat_intent, group_id, discussion_status) + self.__stream_listener(sid, channel_id, chat_id, chat_message_id, group_id, discussion_status) - def stream_prompt_response(self, sid: str, channel_id: str, chat_id: str, chat_message_id: str, chat_intent: str, discussion_status: str): + def stream_prompt_response(self, sid: str, channel_id: str, chat_id: str, chat_message_id: str, discussion_status: str): """Starts a background thread to listen redis pubsub channel from AI server""" - args = (sid, channel_id, chat_id, chat_message_id, chat_intent, discussion_status) + args = (sid, channel_id, chat_id, chat_message_id, discussion_status) try: sio.start_background_task(self.listen_to_redis_stream, *args) except Exception as e: @@ -237,12 +271,10 @@ def process_prompt(self, sid: str, channel_id: str, chat_id: str, chat_message_i "GENERATE": ChatMessageStatus.GENERATE, } if is_retry: - chat_intent = ChatMessageStatus.TRANSFORM_RETRY prompt = ( f"Faulty yaml:{chat_message.technical_content} \n Error:{chat_message.transformation_error_message}" ) else: - chat_intent = chat_message.chat_intent.name prompt = chat_message.prompt if discussion_status in DISCUSSION_STATUS_MAP: @@ -311,7 +343,6 @@ def process_prompt(self, sid: str, channel_id: str, chat_id: str, chat_message_i "db_map": db_metadata, "visitran_model": visitran_models, "chat_name": chat_name, - "chat_intent": chat_intent, "db_type": self.project_instance.database_type, "llm_model_architect": chat_message.llm_model_architect, "llm_model_developer": chat_message.llm_model_developer, @@ -335,7 +366,6 @@ def process_prompt(self, sid: str, channel_id: str, chat_id: str, chat_message_i channel_id=channel_id, chat_id=chat_id, chat_message_id=chat_message_id, - chat_intent=chat_intent, discussion_status=chat_message.discussion_type, ) @@ -347,10 +377,9 @@ def process_prompt(self, sid: str, channel_id: str, chat_id: str, chat_message_i channel_id=channel_id, chat_id=chat_id, chat_message_id=chat_message_id, - chat_intent=chat_intent, discussion_status=chat_message.discussion_type, ) - logging.info(f"process_prompt: chat_intent={chat_intent}, sid={sid}, channel_id={channel_id}") + logging.info(f"process_prompt: sid={sid}, channel_id={channel_id}") chat_message = self._get_chat_message(chat_id=chat_id, chat_message_id=chat_message_id) return chat_message diff --git a/backend/backend/core/routers/chat/serializers.py b/backend/backend/core/routers/chat/serializers.py index 2e8f057..36dfe68 100644 --- a/backend/backend/core/routers/chat/serializers.py +++ b/backend/backend/core/routers/chat/serializers.py @@ -5,6 +5,9 @@ class ChatSerializer(serializers.ModelSerializer): user = UserMinimalSerializer(read_only=True) + chat_intent_name = serializers.CharField( + source='chat_intent.display_name', read_only=True, default=None + ) class Meta: model = Chat @@ -13,6 +16,7 @@ class Meta: 'project_id', 'chat_name', 'chat_intent', + 'chat_intent_name', 'created_at', 'modified_at', 'is_deleted', diff --git a/backend/backend/core/routers/chat/views.py b/backend/backend/core/routers/chat/views.py index 9bd8334..5f283d4 100644 --- a/backend/backend/core/routers/chat/views.py +++ b/backend/backend/core/routers/chat/views.py @@ -128,7 +128,6 @@ def persist_prompt(self, request: Request, project_id: str, *args, **kwargs) -> data = request.data chat_id = data.get("chat_id") prompt = data.get("prompt") - chat_intent_id = data.get("chat_intent_id") llm_model_architect = data.get("llm_model_architect") llm_model_developer = data.get("llm_model_developer") discussion_type = data.get('discussion_status') @@ -139,26 +138,19 @@ def persist_prompt(self, request: Request, project_id: str, *args, **kwargs) -> if discussion_type == "GENERATE": generated_chat_res_id = data.get('final_discussion_id') - # Check token balance before processing the request + # Check token balance before processing the request. + # Intent is auto-detected by the AI service, so we don't know it yet — + # check against the worst-case (TRANSFORM) cost to avoid letting through + # a request the org can't afford. try: project = ProjectDetails.objects.get(project_uuid=project_id) organization = project.organization - # Determine chat intent name for token calculation - chat_intent_name = "INFO" # Default - if chat_intent_id: - from backend.core.models.chat_intent import ChatIntent - try: - chat_intent = ChatIntent.objects.get(chat_intent_id=chat_intent_id) - chat_intent_name = chat_intent.name - except ChatIntent.DoesNotExist: - pass - self.fetch_token_balance( llm_model_architect=llm_model_architect, llm_model_developer=llm_model_developer, organization=organization, - chat_intent_name=chat_intent_name + chat_intent_name="TRANSFORM" ) except ProjectDetails.DoesNotExist: @@ -172,7 +164,6 @@ def persist_prompt(self, request: Request, project_id: str, *args, **kwargs) -> chat_message = chat_message_context.persist_prompt( prompt=prompt, chat_id=chat_id, - chat_intent_id=chat_intent_id, llm_model_architect=llm_model_architect, llm_model_developer=llm_model_developer, discussion_type=discussion_type, diff --git a/backend/backend/core/routers/chat_message/serializers/chat_message_serializer.py b/backend/backend/core/routers/chat_message/serializers/chat_message_serializer.py index ec9e4a8..0fb345a 100644 --- a/backend/backend/core/routers/chat_message/serializers/chat_message_serializer.py +++ b/backend/backend/core/routers/chat_message/serializers/chat_message_serializer.py @@ -11,6 +11,7 @@ class Meta: class ChatMessageSerializer(serializers.ModelSerializer): user = UserMinimalSerializer(read_only=True) + chat_intent_name = serializers.CharField(source='chat_intent.name', read_only=True, default=None) class Meta: model = ChatMessage @@ -29,6 +30,7 @@ class Meta: 'transformation_status', 'transformation_error_message', 'chat_intent', + 'chat_intent_name', 'llm_model_architect', 'llm_model_developer', 'created_at', diff --git a/backend/backend/core/web_socket.py b/backend/backend/core/web_socket.py index 8fe2e26..f5c5fbc 100644 --- a/backend/backend/core/web_socket.py +++ b/backend/backend/core/web_socket.py @@ -263,6 +263,7 @@ def send_socket_message(sid, channel_id, **kwargs): "is_retry_transform", "discussion_status", "token_usage_data", # Add token usage data + "chat_intent_name", ] unsupported_args = [arg for arg in kwargs.keys() if arg not in allowed_args] From 360f4f0f8a3b40e9ff74913e9af30c93cc0ed8d6 Mon Sep 17 00:00:00 2001 From: Tahier Hussain Date: Tue, 5 May 2026 16:39:36 +0530 Subject: [PATCH 2/3] refactor(frontend): drop intent selector and consume backend-detected intent Removes the user-facing chat intent Segmented selector from PromptActions, along with the chat-intent state, getChatIntents fetch, onboarding auto-select effect, and the chat_intent_id field in postChatPrompt. Strips the intent props that were drilled through Body, NewChat, ExistingChat, InputPrompt, ResponseFooter, and ActionButtons. Conversation now derives the intent object directly from message.chat_intent_name (added to the chat-message serializer), and PastConversations renders its badge from chat.chat_intent_name. ChatAI merges chat_intent_name into the message's local state when the new CHAT_INTENT socket event arrives, so ResponseFooter renders the correct branch (Apply / Run SQL / Build Models) in real time without a refresh. Body.jsx falls back llm_model_developer to the architect model so that removing the (already gated-off) Coder selector doesn't leave the field null on Chat creation. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/ide/chat-ai/ActionButtons.jsx | 8 +- frontend/src/ide/chat-ai/Body.jsx | 73 +--------- frontend/src/ide/chat-ai/ChatAI.jsx | 4 + frontend/src/ide/chat-ai/Conversation.jsx | 23 ++-- frontend/src/ide/chat-ai/ExistingChat.jsx | 19 +-- frontend/src/ide/chat-ai/InputPrompt.jsx | 24 +--- frontend/src/ide/chat-ai/NewChat.jsx | 12 -- .../src/ide/chat-ai/PastConversations.jsx | 23 +--- frontend/src/ide/chat-ai/PromptActions.jsx | 130 +----------------- frontend/src/ide/chat-ai/ResponseFooter.jsx | 3 - frontend/src/ide/chat-ai/services.js | 2 - 11 files changed, 35 insertions(+), 286 deletions(-) diff --git a/frontend/src/ide/chat-ai/ActionButtons.jsx b/frontend/src/ide/chat-ai/ActionButtons.jsx index 01b1dc5..bb96dba 100644 --- a/frontend/src/ide/chat-ai/ActionButtons.jsx +++ b/frontend/src/ide/chat-ai/ActionButtons.jsx @@ -25,7 +25,6 @@ const INFO_APPROVED = const ActionButtons = memo(function ActionButtons({ chatMessageId, savePrompt, - selectedChatIntent, isLatestTransform, uiAction, message, @@ -101,12 +100,12 @@ const ActionButtons = memo(function ActionButtons({ setTimeout(() => setIsOperationInProgress(false), 3000); if (value === "GENERATE") { - savePrompt?.(label, selectedChatIntent, false, value, chatMessageId); + savePrompt?.(label, false, value, chatMessageId); return; } - savePrompt?.(label, selectedChatIntent, false, value); + savePrompt?.(label, false, value); }, - [savePrompt, selectedChatIntent, chatMessageId, isOperationInProgress] + [savePrompt, chatMessageId, isOperationInProgress] ); const onApplyClick = useCallback(() => { @@ -321,7 +320,6 @@ ActionButtons.displayName = "ActionButtons"; ActionButtons.propTypes = { chatMessageId: PropTypes.string.isRequired, savePrompt: PropTypes.func, - selectedChatIntent: PropTypes.string, isLatestTransform: PropTypes.bool.isRequired, uiAction: PropTypes.object, message: PropTypes.object, diff --git a/frontend/src/ide/chat-ai/Body.jsx b/frontend/src/ide/chat-ai/Body.jsx index 9ce04bd..dd7d792 100644 --- a/frontend/src/ide/chat-ai/Body.jsx +++ b/frontend/src/ide/chat-ai/Body.jsx @@ -47,8 +47,6 @@ const Body = function Body({ onSendButtonClick, }) { const [isGetChatMessages, setIsGetChatMessages] = useState(false); - const [chatIntents, setChatIntents] = useState([]); - const [selectedChatIntent, setSelectedChatIntent] = useState(null); const [llmModels, setLlmModels] = useState([]); const [selectedLlmModel, setSelectedLlmModel] = useState(null); const [selectedCoderLlmModel, setSelectedCoderLlmModel] = useState(null); @@ -75,8 +73,7 @@ const Body = function Body({ const explorerData = useExplorerStore((state) => state.explorerData); const dbExplorerData = useExplorerStore((state) => state.dbExplorerData); - const { postChatPrompt, getChatIntents, getChatLlmModels } = - useChatAIService(); + const { postChatPrompt, getChatLlmModels } = useChatAIService(); const { notify } = useNotificationService(); useEffect(() => { @@ -100,19 +97,6 @@ const Body = function Body({ } }, [pendingChannel, isConnected, createChannel]); - useEffect(() => { - if (!projectId || chatIntents?.length > 0 || !isChatDrawerOpen) return; - - getChatIntents() - .then((data) => { - setChatIntents(data); - }) - .catch((error) => { - console.error(error); - notify({ error }); - }); - }, [projectId, isChatDrawerOpen]); - useEffect(() => { if (!projectId || llmModels?.length > 0 || !isChatDrawerOpen) return; @@ -234,55 +218,9 @@ const Body = function Body({ setIsGetChatMessages(false); }, []); - // Auto-select intent based on onboarding step mode - useEffect(() => { - if (!isOnboardingMode || !currentOnboardingStep || !chatIntents.length) - return; - - const step = currentOnboardingStep; - let targetIntentName; - - // Map onboarding mode to intent name - switch (step.mode) { - case "transform": - targetIntentName = "TRANSFORM"; - break; - case "sql": - targetIntentName = "SQL"; - break; - case "chat": - targetIntentName = "INFO"; - break; - default: - return; - } - - // Find the intent with the matching name - const targetIntent = chatIntents.find( - (intent) => intent?.name === targetIntentName - ); - - if (targetIntent && selectedChatIntent !== targetIntent.chat_intent_id) { - setSelectedChatIntent(targetIntent.chat_intent_id); - - // Add a visual animation hint - setTimeout(() => { - // You could add a toast notification here if needed - // notify({ message: `Switched to ${targetIntentName} mode for this step` }); - }, 100); - } - }, [ - isOnboardingMode, - currentOnboardingStep, - chatIntents, - selectedChatIntent, - setSelectedChatIntent, - ]); - const savePrompt = useCallback( ( prompt, - selectedChatIntent, isNewChat = false, discussionStatus = null, chatMessageId = null @@ -290,9 +228,8 @@ const Body = function Body({ postChatPrompt({ prompt, llm_model_architect: selectedLlmModel, - llm_model_developer: selectedCoderLlmModel, + llm_model_developer: selectedCoderLlmModel || selectedLlmModel, chatId: selectedChatId, - chatIntentId: selectedChatIntent, discussionStatus, chatMessageId, }) @@ -376,9 +313,6 @@ const Body = function Body({ isGetChatMessages={isGetChatMessages} resetChatMessageIdentifier={resetChatMessageIdentifier} isPromptRunning={isPromptRunning} - chatIntents={chatIntents} - selectedChatIntent={selectedChatIntent} - setSelectedChatIntent={setSelectedChatIntent} llmModels={llmModels} selectedLlmModel={selectedLlmModel} setSelectedLlmModel={setSelectedLlmModel} @@ -421,9 +355,6 @@ const Body = function Body({ savePrompt={savePrompt} triggerGetChatMessagesApi={triggerGetChatMessagesApi} isPromptRunning={isPromptRunning} - chatIntents={chatIntents} - selectedChatIntent={selectedChatIntent} - setSelectedChatIntent={setSelectedChatIntent} llmModels={llmModels} selectedLlmModel={selectedLlmModel} setSelectedLlmModel={setSelectedLlmModel} diff --git a/frontend/src/ide/chat-ai/ChatAI.jsx b/frontend/src/ide/chat-ai/ChatAI.jsx index ff2c654..e651b02 100644 --- a/frontend/src/ide/chat-ai/ChatAI.jsx +++ b/frontend/src/ide/chat-ai/ChatAI.jsx @@ -175,6 +175,10 @@ const ChatAI = memo( existing.token_usage_data = msg?.token_usage_data; } + if (msg?.chat_intent_name) { + existing.chat_intent_name = msg.chat_intent_name; + } + updatedMessages[idx] = existing; } uuidsToRemove.push(msg?.uuid); diff --git a/frontend/src/ide/chat-ai/Conversation.jsx b/frontend/src/ide/chat-ai/Conversation.jsx index 5cc70f2..3f48fb1 100644 --- a/frontend/src/ide/chat-ai/Conversation.jsx +++ b/frontend/src/ide/chat-ai/Conversation.jsx @@ -14,7 +14,6 @@ import ModelGenerationProgress from "./ModelGenerationProgress"; const Conversation = memo(function Conversation({ savePrompt, message, - chatIntents, isPromptRunning, isLastConversation, selectedChatId, @@ -22,7 +21,6 @@ const Conversation = memo(function Conversation({ triggerRetryTransform, handleSqlRun, isLatestTransform, - selectedChatIntent, }) { const userDetails = useUserStore((state) => state.userDetails); const [detectedAction, setDetectedAction] = useState(null); @@ -36,9 +34,9 @@ const Conversation = memo(function Conversation({ const handleTroubleshoot = useCallback( (errorMessage) => { const prompt = `There was an error encountered. We have the detailed error message below. Please see how we can fix this:\n\n${errorMessage}`; - savePrompt(prompt, selectedChatIntent); + savePrompt(prompt); }, - [savePrompt, selectedChatIntent] + [savePrompt] ); // Create UI action object based on detected action @@ -80,14 +78,18 @@ const Conversation = memo(function Conversation({ }, [message?.transformation_status]); /** -------------------------------------------------------------------- - * Derive the intent once; re-computes only when its deps change. + * Derive the intent from the message itself (auto-detected by AI server, + * surfaced via chat_intent_name on the serialized chat message). * ------------------------------------------------------------------- */ const intent = useMemo( () => - chatIntents.find( - ({ chat_intent_id }) => chat_intent_id === message?.chat_intent - ), - [chatIntents, message?.chat_intent] + message?.chat_intent_name + ? { + chat_intent_id: message?.chat_intent, + name: message.chat_intent_name, + } + : null, + [message?.chat_intent, message?.chat_intent_name] ); // Memoize errorDetails to prevent unnecessary re-renders of PromptInfo @@ -161,7 +163,6 @@ const Conversation = memo(function Conversation({ handleSqlRun={handleSqlRun} isLatestTransform={isLatestTransform} savePrompt={savePrompt} - selectedChatIntent={selectedChatIntent} uiAction={uiAction} /> )} @@ -188,7 +189,6 @@ const Conversation = memo(function Conversation({ Conversation.propTypes = { savePrompt: PropTypes.func.isRequired, message: PropTypes.object.isRequired, - chatIntents: PropTypes.array.isRequired, isPromptRunning: PropTypes.bool.isRequired, isLastConversation: PropTypes.bool.isRequired, selectedChatId: PropTypes.string.isRequired, @@ -196,7 +196,6 @@ Conversation.propTypes = { triggerRetryTransform: PropTypes.bool.isRequired, handleSqlRun: PropTypes.func.isRequired, isLatestTransform: PropTypes.bool.isRequired, - selectedChatIntent: PropTypes.string, }; Conversation.displayName = "Conversation"; diff --git a/frontend/src/ide/chat-ai/ExistingChat.jsx b/frontend/src/ide/chat-ai/ExistingChat.jsx index eafeaf2..683dab3 100644 --- a/frontend/src/ide/chat-ai/ExistingChat.jsx +++ b/frontend/src/ide/chat-ai/ExistingChat.jsx @@ -38,9 +38,6 @@ const ExistingChat = memo(function ExistingChat({ isGetChatMessages, resetChatMessageIdentifier, isPromptRunning, - chatIntents, - selectedChatIntent, - setSelectedChatIntent, llmModels, selectedLlmModel, setSelectedLlmModel, @@ -236,15 +233,11 @@ const ExistingChat = memo(function ExistingChat({ }; const lastTransformIndex = useMemo(() => { - const intentsMap = chatIntents.reduce((acc, ci) => { - acc[ci?.chat_intent_id] = ci?.name; - return acc; - }, {}); for (let i = chatMessages.length - 1; i >= 0; i--) { - if (intentsMap[chatMessages[i]?.chat_intent] === "TRANSFORM") return i; + if (chatMessages[i]?.chat_intent_name === "TRANSFORM") return i; } return -1; - }, [chatMessages, chatIntents]); + }, [chatMessages]); // Check if response is actively streaming (thought chain done, response started) const isResponseStreaming = useMemo(() => { @@ -395,7 +388,6 @@ const ExistingChat = memo(function ExistingChat({ ))} @@ -462,9 +453,6 @@ const ExistingChat = memo(function ExistingChat({ savePrompt={handleSavePrompt} isPromptRunning={isPromptRunning} isResponseStreaming={isResponseStreaming} - chatIntents={chatIntents} - selectedChatIntent={selectedChatIntent} - setSelectedChatIntent={setSelectedChatIntent} llmModels={llmModels} selectedLlmModel={selectedLlmModel} setSelectedLlmModel={setSelectedLlmModel} @@ -499,9 +487,6 @@ ExistingChat.propTypes = { isGetChatMessages: PropTypes.bool.isRequired, resetChatMessageIdentifier: PropTypes.func.isRequired, isPromptRunning: PropTypes.bool.isRequired, - chatIntents: PropTypes.array.isRequired, - selectedChatIntent: PropTypes.string, - setSelectedChatIntent: PropTypes.func.isRequired, llmModels: PropTypes.array, selectedLlmModel: PropTypes.string, setSelectedLlmModel: PropTypes.func.isRequired, diff --git a/frontend/src/ide/chat-ai/InputPrompt.jsx b/frontend/src/ide/chat-ai/InputPrompt.jsx index 6951781..57922cb 100644 --- a/frontend/src/ide/chat-ai/InputPrompt.jsx +++ b/frontend/src/ide/chat-ai/InputPrompt.jsx @@ -1,4 +1,4 @@ -import { memo, useState, useCallback, useRef, useEffect, useMemo } from "react"; +import { memo, useState, useCallback, useRef, useEffect } from "react"; import PropTypes from "prop-types"; import { Space } from "antd"; @@ -11,9 +11,6 @@ const InputPrompt = memo(function InputPrompt({ isNewChat = false, isPromptRunning, isResponseStreaming = false, - chatIntents, - selectedChatIntent, - setSelectedChatIntent, llmModels = [], selectedLlmModel, setSelectedLlmModel, @@ -65,14 +62,6 @@ const InputPrompt = memo(function InputPrompt({ window.localStorage.setItem("useMonaco", useMonaco); }, [useMonaco]); - const selectedChatIntentName = useMemo(() => { - if (!selectedChatIntent) return null; - - return chatIntents.find( - (intent) => intent?.chat_intent_id === selectedChatIntent - )?.name; - }, [chatIntents, selectedChatIntent]); - const handleUseMonacoSwitch = useCallback( (checked) => { // Disable monaco switch during onboarding mode when typing @@ -87,14 +76,14 @@ const InputPrompt = memo(function InputPrompt({ const handleSubmit = useCallback( (prompt) => { setValue(""); - savePrompt(prompt, selectedChatIntent, isNewChat); + savePrompt(prompt, isNewChat); if (useMonaco) setEditorHeight(100); // Stop send button animation when clicked during onboarding if (onSendButtonClick) { onSendButtonClick(); } }, - [savePrompt, isNewChat, selectedChatIntent, useMonaco, onSendButtonClick] + [savePrompt, isNewChat, useMonaco, onSendButtonClick] ); const handleStop = useCallback(() => { @@ -232,16 +221,12 @@ const InputPrompt = memo(function InputPrompt({ useMonaco={useMonaco} isNewChat={isNewChat} onUseMonacoSwitch={handleUseMonacoSwitch} - chatIntents={chatIntents} - selectedChatIntent={selectedChatIntent} - setSelectedChatIntent={setSelectedChatIntent} llmModels={llmModels} selectedLlmModel={selectedLlmModel} setSelectedLlmModel={setSelectedLlmModel} selectedCoderLlmModel={selectedCoderLlmModel} setSelectedCoderLlmModel={setSelectedCoderLlmModel} selectedChatId={selectedChatId} - selectedChatIntentName={selectedChatIntentName} isOnboardingMode={isOnboardingMode} isTypingPrompt={isTypingPrompt} onBuyTokens={onBuyTokens} @@ -255,9 +240,6 @@ InputPrompt.propTypes = { isNewChat: PropTypes.bool, isPromptRunning: PropTypes.bool.isRequired, isResponseStreaming: PropTypes.bool, - chatIntents: PropTypes.array.isRequired, - selectedChatIntent: PropTypes.string, - setSelectedChatIntent: PropTypes.func.isRequired, llmModels: PropTypes.array, selectedLlmModel: PropTypes.string, setSelectedLlmModel: PropTypes.func.isRequired, diff --git a/frontend/src/ide/chat-ai/NewChat.jsx b/frontend/src/ide/chat-ai/NewChat.jsx index 07f3383..0176b9d 100644 --- a/frontend/src/ide/chat-ai/NewChat.jsx +++ b/frontend/src/ide/chat-ai/NewChat.jsx @@ -15,9 +15,6 @@ const NewChat = memo(function NewChat({ savePrompt, triggerGetChatMessagesApi, isPromptRunning, - chatIntents, - selectedChatIntent, - setSelectedChatIntent, llmModels = [], selectedLlmModel, setSelectedLlmModel, @@ -41,7 +38,6 @@ const NewChat = memo(function NewChat({ onSendButtonClick, }) { useEffect(() => { - setSelectedChatIntent(null); setSelectedLlmModel(null); setSelectedCoderLlmModel(null); }, []); @@ -80,9 +76,6 @@ const NewChat = memo(function NewChat({ savePrompt={savePrompt} isNewChat isPromptRunning={isPromptRunning} - chatIntents={chatIntents} - selectedChatIntent={selectedChatIntent} - setSelectedChatIntent={setSelectedChatIntent} llmModels={llmModels} selectedLlmModel={selectedLlmModel} setSelectedLlmModel={setSelectedLlmModel} @@ -103,9 +96,7 @@ const NewChat = memo(function NewChat({ isChatDrawerOpen={isChatDrawerOpen} setSelectedChatId={setSelectedChatId} setChatName={setChatName} - chatIntents={chatIntents} triggerGetChatMessagesApi={triggerGetChatMessagesApi} - setSelectedChatIntent={setSelectedChatIntent} setSelectedLlmModel={setSelectedLlmModel} setSelectedCoderLlmModel={setSelectedCoderLlmModel} /> @@ -121,9 +112,6 @@ NewChat.propTypes = { savePrompt: PropTypes.func.isRequired, triggerGetChatMessagesApi: PropTypes.func.isRequired, isPromptRunning: PropTypes.bool.isRequired, - chatIntents: PropTypes.array, - selectedChatIntent: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - setSelectedChatIntent: PropTypes.func.isRequired, llmModels: PropTypes.array, selectedLlmModel: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), setSelectedLlmModel: PropTypes.func.isRequired, diff --git a/frontend/src/ide/chat-ai/PastConversations.jsx b/frontend/src/ide/chat-ai/PastConversations.jsx index 576425e..02f61f7 100644 --- a/frontend/src/ide/chat-ai/PastConversations.jsx +++ b/frontend/src/ide/chat-ai/PastConversations.jsx @@ -1,4 +1,4 @@ -import { memo, useState, useEffect, useCallback, useMemo } from "react"; +import { memo, useState, useEffect, useCallback } from "react"; import PropTypes from "prop-types"; import { Space, Typography, Tag } from "antd"; @@ -12,9 +12,7 @@ const PastConversations = memo(function PastConversations({ isChatDrawerOpen, setSelectedChatId, setChatName, - chatIntents, triggerGetChatMessagesApi, - setSelectedChatIntent, setSelectedLlmModel, setSelectedCoderLlmModel, }) { @@ -40,17 +38,6 @@ const PastConversations = memo(function PastConversations({ fetchChats(); }, [projectId, isChatDrawerOpen]); - const chatIntentMap = useMemo(() => { - if (!chatIntents?.length) return {}; - - return chatIntents.reduce((acc, intent) => { - if (intent?.chat_intent_id && intent?.display_name) { - acc[intent.chat_intent_id] = intent.display_name; - } - return acc; - }, {}); - }, [chatIntents]); - const handleShowAll = useCallback(() => { setShowAll((prev) => !prev); }, []); @@ -92,7 +79,6 @@ const PastConversations = memo(function PastConversations({ triggerGetChatMessagesApi(); setSelectedChatId(conversation.chat_id); setChatName(conversation.chat_name || ""); - setSelectedChatIntent(conversation.chat_intent); setSelectedLlmModel(conversation.llm_model_architect); setSelectedCoderLlmModel(conversation.llm_model_developer); }, @@ -100,7 +86,6 @@ const PastConversations = memo(function PastConversations({ triggerGetChatMessagesApi, setSelectedChatId, setChatName, - setSelectedChatIntent, setSelectedLlmModel, setSelectedCoderLlmModel, ] @@ -138,9 +123,9 @@ const PastConversations = memo(function PastConversations({ > {conversation.chat_name} - {chatIntentMap?.[conversation?.chat_intent] && ( + {conversation?.chat_intent_name && ( - {chatIntentMap[conversation.chat_intent]} + {conversation.chat_intent_name} )} @@ -181,9 +166,7 @@ PastConversations.propTypes = { isChatDrawerOpen: PropTypes.bool.isRequired, setSelectedChatId: PropTypes.func.isRequired, setChatName: PropTypes.func.isRequired, - chatIntents: PropTypes.array.isRequired, triggerGetChatMessagesApi: PropTypes.func.isRequired, - setSelectedChatIntent: PropTypes.func.isRequired, setSelectedLlmModel: PropTypes.func.isRequired, setSelectedCoderLlmModel: PropTypes.func.isRequired, }; diff --git a/frontend/src/ide/chat-ai/PromptActions.jsx b/frontend/src/ide/chat-ai/PromptActions.jsx index 16e1578..2663e76 100644 --- a/frontend/src/ide/chat-ai/PromptActions.jsx +++ b/frontend/src/ide/chat-ai/PromptActions.jsx @@ -1,15 +1,8 @@ import { memo, useCallback, useEffect, useMemo } from "react"; -import { Space, Typography, Select, Switch, Segmented, Tooltip } from "antd"; -import { - ConsoleSqlOutlined, - DatabaseOutlined, - MessageOutlined, - RetweetOutlined, - WalletOutlined, -} from "@ant-design/icons"; +import { Space, Typography, Select, Switch } from "antd"; +import { DatabaseOutlined, WalletOutlined } from "@ant-design/icons"; import PropTypes from "prop-types"; -import { CHAT_INTENTS } from "./helper"; import CircularTokenDisplay from "./CircularTokenDisplay"; import InfoChip from "./InfoChip"; import { useTokenStore } from "../../store/token-store"; @@ -18,35 +11,14 @@ import { useProjectStore } from "../../store/project-store"; import { explorerService } from "../explorer/explorer-service"; import { useNotificationService } from "../../service/notification-service"; -// Define hidden intents and a fixed order array -const HIDDEN_CHAT_INTENTS = ["AUTO", "NOTA", "INFO"]; -const CHAT_INTENTS_ORDER = ["TRANSFORM", "SQL"]; - -const CHAT_INTENTS_ICONS = { - TRANSFORM: , - SQL: , - INFO: , -}; -const DEFAULT_CHAT_INTENT = "TRANSFORM"; - const HIDE_EDITOR_SELECTOR = true; -const IS_MODELS_UNIFIED = true; // Use the same model for both Architect and Coder - const PromptActions = memo(function PromptActions({ useMonaco, onUseMonacoSwitch, - chatIntents, - selectedChatIntent, - setSelectedChatIntent, llmModels = [], selectedLlmModel, setSelectedLlmModel, - selectedCoderLlmModel, - setSelectedCoderLlmModel, - selectedChatId, - selectedChatIntentName, - isNewChat = false, isOnboardingMode = false, isTypingPrompt = false, onBuyTokens, @@ -100,46 +72,12 @@ const PromptActions = memo(function PromptActions({ } }, [llmModels, selectedLlmModel]); - // If there's no selected LLM, pick the default - useEffect(() => { - if (selectedCoderLlmModel || !llmModels.length) return; - const defaultModel = llmModels.find((m) => m.default); - if (defaultModel?.model) { - setSelectedCoderLlmModel(defaultModel.model); - } - }, [llmModels, selectedCoderLlmModel]); - - // If there's no selected Chat Intent, pick the default - useEffect(() => { - if (selectedChatIntent || !chatIntents.length) return; - const defaultChatIntent = chatIntents.find( - (intent) => intent.name === DEFAULT_CHAT_INTENT - ); - if (defaultChatIntent?.chat_intent_id) { - setSelectedChatIntent(defaultChatIntent.chat_intent_id); - } - }, [chatIntents, selectedChatIntent]); - - // Filter out hidden intents, then sort by CHAT_INTENTS_ORDER - const filteredAndSortedIntents = useMemo(() => { - return chatIntents - .filter((intent) => !HIDDEN_CHAT_INTENTS.includes(intent.name)) - .sort( - (a, b) => - CHAT_INTENTS_ORDER.indexOf(a.name) - - CHAT_INTENTS_ORDER.indexOf(b.name) - ); - }, [chatIntents]); - return (
- {selectedChatIntentName === CHAT_INTENTS.TRANSFORM && - !IS_MODELS_UNIFIED - ? "Architect:" - : "Model:"} + Model: - - )} @@ -223,33 +140,8 @@ const PromptActions = memo(function PromptActions({ )} -
- - ({ - label: ( - - {intent.display_name} - - ), - value: intent.chat_intent_id, - icon: CHAT_INTENTS_ICONS[intent.name], - }))} - /> - - - {!HIDE_EDITOR_SELECTOR && ( + {!HIDE_EDITOR_SELECTOR && ( +
- )} -
+
+ )}
); }); @@ -276,17 +168,9 @@ const PromptActions = memo(function PromptActions({ PromptActions.propTypes = { useMonaco: PropTypes.bool.isRequired, onUseMonacoSwitch: PropTypes.func.isRequired, - chatIntents: PropTypes.array.isRequired, - selectedChatIntent: PropTypes.string, - setSelectedChatIntent: PropTypes.func.isRequired, llmModels: PropTypes.array, selectedLlmModel: PropTypes.string, setSelectedLlmModel: PropTypes.func.isRequired, - selectedCoderLlmModel: PropTypes.string, - setSelectedCoderLlmModel: PropTypes.func.isRequired, - selectedChatId: PropTypes.string, - selectedChatIntentName: PropTypes.string, - isNewChat: PropTypes.bool, isOnboardingMode: PropTypes.bool, isTypingPrompt: PropTypes.bool, onBuyTokens: PropTypes.func, diff --git a/frontend/src/ide/chat-ai/ResponseFooter.jsx b/frontend/src/ide/chat-ai/ResponseFooter.jsx index 87d647b..4ab93a1 100644 --- a/frontend/src/ide/chat-ai/ResponseFooter.jsx +++ b/frontend/src/ide/chat-ai/ResponseFooter.jsx @@ -17,7 +17,6 @@ const ResponseFooter = memo( handleSqlRun, isLatestTransform, savePrompt, - selectedChatIntent, uiAction, }) => { if (!intent) return null; @@ -72,7 +71,6 @@ const ResponseFooter = memo( chatMessageId={message?.chat_message_id} uiAction={uiAction} savePrompt={savePrompt} - selectedChatIntent={selectedChatIntent} isLatestTransform={isLatestTransform} message={message} selectedChatId={selectedChatId} @@ -137,7 +135,6 @@ ResponseFooter.propTypes = { handleSqlRun: PropTypes.func.isRequired, isLatestTransform: PropTypes.bool.isRequired, savePrompt: PropTypes.func.isRequired, - selectedChatIntent: PropTypes.string, uiAction: PropTypes.object, }; diff --git a/frontend/src/ide/chat-ai/services.js b/frontend/src/ide/chat-ai/services.js index 1fb5f7a..e53a5d4 100644 --- a/frontend/src/ide/chat-ai/services.js +++ b/frontend/src/ide/chat-ai/services.js @@ -48,7 +48,6 @@ export function useChatAIService() { llm_model_architect, llm_model_developer, chatId = null, - chatIntentId = null, discussionStatus = null, chatMessageId = null, }) => { @@ -60,7 +59,6 @@ export function useChatAIService() { llm_model_architect, llm_model_developer, chat_id: chatId, - chat_intent_id: chatIntentId, discussion_status: discussionStatus, final_discussion_id: chatMessageId, }, From 4454819586c9a12aeb90da5a21019fe8a8e526f8 Mon Sep 17 00:00:00 2001 From: Tahier Hussain Date: Tue, 5 May 2026 17:11:01 +0530 Subject: [PATCH 3/3] =?UTF-8?q?fix(backend):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20drop=20debug=20print,=20mirror=20intent=20onto=20Ch?= =?UTF-8?q?at?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes a leftover debug print in process_message that flooded logs with every Redis stream payload (potentially exposing user prompts). Also restores the pre-PR behavior where Chat.chat_intent tracks the latest message's intent. _resolve_chat_intent now mirrors the persisted intent onto chat_message.chat as well, so the PastConversations badge (which reads chat.chat_intent_name from ChatSerializer) populates correctly for chats created after this PR. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/backend/application/context/llm_context.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/backend/application/context/llm_context.py b/backend/backend/application/context/llm_context.py index d58ffd2..2dfc09d 100644 --- a/backend/backend/application/context/llm_context.py +++ b/backend/backend/application/context/llm_context.py @@ -78,6 +78,10 @@ def _resolve_chat_intent(self, chat_message_id: str, data: dict, content: Any) - chat_message = ChatMessage.objects.get(chat_message_id=chat_message_id) chat_message.chat_intent = chat_intent chat_message.save(update_fields=["chat_intent"]) + # Mirror the pre-PR behavior where Chat.chat_intent tracked the + # latest message's intent (used by PastConversations badge). + chat_message.chat.chat_intent = chat_intent + chat_message.chat.save(update_fields=["chat_intent"]) except Exception as e: logging.error(f"Failed to persist chat_intent={intent_name}: {e}") @@ -93,7 +97,6 @@ def process_message( discussion_status: str ): data = json.loads(payload["data"]) - print("\n=============\n", data, "\n==========\n") if payload.get("type") == "status" and payload.get("status") == "failed": payload = json.loads(payload["data"]) if payload and "error_message" in payload: