langgenius / dify

Dify is an open-source LLM app development platform. Dify's intuitive interface combines AI workflow, RAG pipeline, agent capabilities, model management, observability features and more, letting you quickly go from prototype to production.
https://dify.ai
Other
44.12k stars 6.17k forks source link

Omitted Tracing Information and Wrong Status showing when clicking the 'Reset Conversation' button in the embedded chatbot. #6341

Closed Dinnnng closed 4 days ago

Dinnnng commented 1 month ago

Self Checks

Dify version

0.6.11

Cloud or Self Hosted

Cloud, Self Hosted (Docker), Self Hosted (Source)

Steps to reproduce

When running a workflow on the embedded chatbot and clicking the 'Reset Conversation' icon, it results in incomplete workflow information for the previously reset conversation in the user's backend.

The current issue has been identified as follows:

The frontend and backend of Flask utilize the SSE (Server-Sent Events) protocol.

When the frontend request is aborted, the backend thread running the workflow is also terminated (blocked), causing the workflow information from the previous conversation to be unrecorded in the database.

This leads to strange logs in the user's dashboard, showing the workflow stopping at arbitrary nodes without proceeding further. and the status is still showing 'running'

Moreover, the workflow task thread completes its execution, but this completion is not reflected in the user's backend. This situation is unfavorable for both billing purposes and the user experience, as it does not accurately represent the workflow's progress.

`

api/core/app/apps/advanced_chat/app_generator.py

    try:
        return generate_task_pipeline.process()
    except ValueError as e:
        if e.args[0] == "I/O operation on closed file.":  # ignore this error
            raise GenerateTaskStoppedException()
        else:
            logger.exception(e)
            raise e

`

✔️ Expected Behavior

Normal tracing information, as well as the display of interrupted or reset statuses.

❌ Actual Behavior

image

image

image

dosubot[bot] commented 1 month ago

Hey @Dinnnng! 👋 I'm here to help you with your bug report. I'm a bot and I can assist you with solving bugs, answering questions, and even becoming a contributor.

To resolve the issue where the 'Reset Conversation' button in your embedded chatbot causes incomplete tracing information and incorrect status display, you need to ensure that the context values and state updates are properly handled when the conversation is reset.

Focus on the handleNewConversation and handleNewConversationCompleted functions. These functions are likely responsible for resetting the conversation and updating the relevant state.

Here is a possible approach to ensure that the context values are correctly updated:

  1. Ensure handleNewConversation properly resets the state:

    • Clear the current conversation ID.
    • Reset any other state variables that are relevant to the conversation.
  2. Ensure handleNewConversationCompleted updates the state correctly:

    • Set the new conversation ID.
    • Update any other state variables that are relevant to the new conversation.

Here is an example of how you might implement these functions based on the useEmbeddedChatbot hook:

import { useCallback, useState, useMemo, useRef } from 'react';
import useSWR from 'swr';
import { useLocalStorageState } from 'ahooks';
import produce from 'immer';
import { fetchConversations, fetchChatList, generationConversationName } from '@/service/share';
import { CONVERSATION_ID_INFO } from '../constants';
import { useToastContext } from '@/app/components/base/toast';

export const useEmbeddedChatbot = () => {
  const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, string>>(CONVERSATION_ID_INFO, {
    defaultValue: {},
  });
  const [newConversationId, setNewConversationId] = useState('');
  const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false);
  const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: () => {} });

  const currentConversationId = useMemo(() => conversationIdInfo?.[appId || ''] || '', [appId, conversationIdInfo]);

  const handleConversationIdInfoChange = useCallback((changeConversationId: string) => {
    if (appId) {
      setConversationIdInfo({
        ...conversationIdInfo,
        [appId || '']: changeConversationId,
      });
    }
  }, [appId, conversationIdInfo, setConversationIdInfo]);

  const handleNewConversation = useCallback(() => {
    currentChatInstanceRef.current.handleStop();
    setNewConversationId('');

    if (showNewConversationItemInList) {
      handleChangeConversation('');
    } else if (currentConversationId) {
      handleConversationIdInfoChange('');
      setShowNewConversationItemInList(true);
    }
  }, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowNewConversationItemInList, showNewConversationItemInList]);

  const handleNewConversationCompleted = useCallback((newConversationId: string) => {
    setNewConversationId(newConversationId);
    handleConversationIdInfoChange(newConversationId);
    setShowNewConversationItemInList(false);
    mutateAppConversationData();
  }, [mutateAppConversationData, handleConversationIdInfoChange]);

  return {
    currentConversationId,
    handleNewConversation,
    handleNewConversationCompleted,
    currentChatInstanceRef,
  };
};

Additionally, ensure that the handleNewConversation function is correctly integrated into the Chatbot component to handle the reset action:

import {
  useEffect,
  useState,
} from 'react';
import cn from 'classnames';
import { useAsyncEffect } from 'ahooks';
import {
  EmbeddedChatbotContext,
  useEmbeddedChatbotContext,
} from './context';
import { useEmbeddedChatbot } from './hooks';
import { isDify } from './utils';
import { useThemeContext } from './theme/theme-context';
import { checkOrSetAccessToken } from '@/app/components/share/utils';
import AppUnavailable from '@/app/components/base/app-unavailable';
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints';
import Loading from '@/app/components/base/loading';
import LogoHeader from '@/app/components/base/logo/logo-embeded-chat-header';
import Header from '@/app/components/base/chat/embedded-chatbot/header';
import ConfigPanel from '@/app/components/base/chat/embedded-chatbot/config-panel';
import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper';

const Chatbot = () => {
  const {
    isMobile,
    appInfoError,
    appInfoLoading,
    appData,
    appPrevChatList,
    showConfigPanelBeforeChat,
    appChatListDataLoading,
    handleNewConversation,
    themeBuilder,
  } = useEmbeddedChatbotContext();

  const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatList.length);
  const customConfig = appData?.custom_config;
  const site = appData?.site;

  const difyIcon = <LogoHeader />;

  useEffect(() => {
    themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted);
    if (site) {
      if (customConfig)
        document.title = `${site.title}`;
      else
        document.title = `${site.title} - Powered by Dify`;
    }
  }, [site, customConfig]);

  if (appInfoLoading) {
    return (
      <Loading type='app' />
    );
  }

  if (appInfoError) {
    return (
      <AppUnavailable />
    );
  }
  return (
    <div>
      <Header
        isMobile={isMobile}
        title={site?.title || ''}
        customerIcon={isDify() ? difyIcon : ''}
        theme={themeBuilder?.theme}
        onCreateNewChat={handleNewConversation}
      />
      <div className='flex bg-white overflow-hidden'>
        <div className={cn('h-[100vh] grow flex flex-col overflow-y-auto', isMobile && '!h-[calc(100vh_-_3rem)]')}>
          {showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatList.length && (
            <div className={cn('flex w-full items-center justify-center h-full tablet:px-4', isMobile && 'px-4')}>
              <ConfigPanel />
            </div>
          )}
          {appChatListDataLoading && chatReady && (
            <Loading type='app' />
          )}
          {chatReady && !appChatListDataLoading && (
            <ChatWrapper />
          )}
        </div>
      </div>
    </div>
  );
};

const EmbeddedChatbotWrapper = () => {
  const media = useBreakpoints();
  const isMobile = media === MediaType.mobile;
  const themeBuilder = useThemeContext();

  const {
    appInfoError,
    appInfoLoading,
    appData,
    appParams,
    appMeta,
    appChatListDataLoading,
    currentConversationId,
    currentConversationItem,
    appPrevChatList,
    pinnedConversationList,
    conversationList,
    showConfigPanelBeforeChat,
    newConversationInputs,
    handleNewConversationInputsChange,
    inputsForms,
    handleNewConversation,
    handleStartChat,
    handleChangeConversation,
    handleNewConversationCompleted,
    chatShouldReloadKey,
    isInstalledApp,
    appId,
    handleFeedback,
    currentChatInstanceRef,
  } = useEmbeddedChatbot();

  return <EmbeddedChatbotContext.Provider value={{
    appInfoError,
    appInfoLoading,
    appData,
    appParams,
    appMeta,
    appChatListDataLoading,
    currentConversationId,
    currentConversationItem,
    appPrevChatList,
    pinnedConversationList,
    conversationList,
    showConfigPanelBeforeChat,
    newConversationInputs,
    handleNewConversationInputsChange,
    inputsForms,
    handleNewConversation,
    handleStartChat,
    handleChangeConversation,
    handleNewConversationCompleted,
    chatShouldReloadKey,
    isMobile,
    isInstalledApp,
    appId,
    handleFeedback,
    currentChatInstanceRef,
    themeBuilder,
  }}>
    <Chatbot />
  </EmbeddedChatbotContext.Provider>;
};

const EmbeddedChatbot = () => {
  const [initialized, setInitialized] = useState(false);
  const [appUnavailable, setAppUnavailable] = useState<boolean>(false);
  const [isUnknownReason, setIsUnknownReason] = useState<boolean>(false);

  useAsyncEffect(async () => {
    if (!initialized) {
      try {
        await checkOrSetAccessToken();
      }
      catch (e: any) {
        if (e.status === 404) {
          setAppUnavailable(true);
        }
        else {
          setIsUnknownReason(true);
          setAppUnavailable(true);
        }
      }
      setInitialized(true);
    }
  }, []);

  if (!initialized)
    return null;

  if (appUnavailable)
    return <AppUnavailable isUnknownReason={isUnknownReason} />;

  return <EmbeddedChatbotWrapper />;
};

export default EmbeddedChatbot;

Make sure to replace the placeholder logic with actual state management code as per your application's requirements. This should help in maintaining complete tracing information and correct status display when the 'Reset Conversation' button is used [1][2][3].

To continue talking to Dosu, mention @dosu.