Skip to content

Vault workspace suggested question#7903

Merged
appflowy merged 19 commits intomainfrom
vault_workspace_suggested_question
May 9, 2025
Merged

Vault workspace suggested question#7903
appflowy merged 19 commits intomainfrom
vault_workspace_suggested_question

Conversation

@appflowy
Copy link
Contributor

@appflowy appflowy commented May 9, 2025

Feature Preview


PR Checklist

  • My code adheres to AppFlowy's Conventions
  • I've listed at least one issue that this PR fixes in the description above.
  • I've added a test(s) to validate changes in this PR, or this PR only contains semantic changes.
  • All existing tests are passing.

Summary by Sourcery

Enhance the AI chat functionality by adding context-aware suggested questions and improving the conversational retrieval chain in the AppFlowy local AI system

New Features:

  • Add context-based suggested questions when no relevant documents are found
  • Implement a mechanism to generate related questions for AI chat
  • Enhance streaming response to support suggested questions

Enhancements:

  • Improve conversational retrieval chain to handle scenarios with limited context
  • Refactor AI chat message handling to support more dynamic interactions
  • Add more robust error handling and fallback mechanisms

Tests:

  • Add test cases for context-based question generation
  • Implement integration tests for AI chat suggested questions feature

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented May 9, 2025

Reviewer's Guide

The pull request introduces a feature to provide users with suggested questions when their initial query against selected documents (RAG sources) doesn't yield a direct answer. This is implemented by:

  1. In the Rust backend, the ConversationalRetrieverChain now incorporates a ContextQuestionChain. If no relevant document snippets are found for a query with active RAG sources, this new chain fetches the full content of these sources from SqliteVectorStore (via select_all_embedded_content). It then prompts an LLM, using a specialized JSON-output format, to generate contextually relevant questions based on this content. These suggestions, along with a directive (AIFollowUpData) on whether to generate further general related questions, are streamed back to the client. The prompt system (AFContextPrompt) was also updated to dynamically switch templates based on RAG source availability.
  2. On the Flutter frontend, the chat message stream and BLoCs (ChatAIMessageBloc, ChatBloc) were updated to parse the new AIFollowUpData from the backend. This data influences whether the UI should trigger a request for general related questions after the current turn.
  3. Configuration for local AI was modified by removing the direct Ollama plugin executable path management from flowy-ai, and LLMChat initialization was refactored.
  4. Comprehensive tests were added to validate the new suggested question functionality and RAG interactions.

File-Level Changes

Change Details Files
Implemented backend logic to suggest questions based on selected document context when a direct answer isn't found.
  • Added ContextQuestionChain to generate questions from RAG source content via LLM, expecting JSON output.
  • Integrated ContextQuestionChain into ConversationalRetrieverChain to trigger suggestions if direct document search yields no results for active RAG IDs.
  • Modified stream output (QuestionStreamValue, StreamMessage) to include suggested questions and a follow-up directive (AIFollowUpData).
  • Updated AFContextPrompt to dynamically select prompt templates (context-aware or general QA) based on RAG source availability.
  • Added SqliteVectorStore::select_all_embedded_content (and corresponding DB method) to retrieve full document text for question generation.
frontend/rust-lib/flowy-ai/src/local_ai/chat/chains/conversation_chain.rs
frontend/rust-lib/flowy-ai/src/local_ai/chat/chains/context_question_chain.rs
frontend/rust-lib/flowy-ai/src/local_ai/chat/llm_chat.rs
frontend/rust-lib/flowy-ai/src/local_ai/chat/format_prompt.rs
frontend/rust-lib/flowy-sqlite-vec/src/db.rs
frontend/rust-lib/flowy-ai/src/stream_message.rs
frontend/rust-lib/flowy-ai-pub/src/entities.rs
Updated frontend to interpret backend signals for follow-up actions related to question suggestions.
  • Introduced AIFollowUpData in Dart and updated stream processing to handle new ai_follow_up: events.
  • Modified ChatBloc and ChatAIMessageBloc to use this data to decide whether to fetch general related questions after the current AI response.
frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart
frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart
frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart
frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart
frontend/appflowy_flutter/lib/ai/service/ai_entities.dart
Refactored local AI setup, Ollama interaction, and related services.
  • Removed Ollama plugin executable path configuration (e.g., get_plugin_config method).
  • Simplified LLMChat and ConversationalRetrieverChainBuilder initialization and dependency injection (e.g., passing Arc<Ollama> directly).
  • Added workspace_type to AIUserService and its implementations.
  • Implemented a new MessageIDGenerator for chat messages.
frontend/rust-lib/flowy-ai/src/local_ai/resource.rs
frontend/rust-lib/flowy-ai/src/local_ai/chat/llm_chat.rs
frontend/rust-lib/flowy-ai/src/ai_manager.rs
frontend/rust-lib/flowy-ai/src/local_ai/chat/mod.rs
frontend/rust-lib/flowy-user/src/services/authenticate_user.rs
frontend/rust-lib/flowy-ai-pub/src/user_service.rs
frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs
Enhanced testing for AI chat, RAG functionalities, and contextual question suggestions.
  • Added specific unit tests for contextual question suggestions and their streaming behavior.
  • Introduced helper structures (e.g., StreamResult) and functions for testing stream outputs.
  • Added new integration tests for chat interactions with RAG sources.
frontend/rust-lib/flowy-ai/tests/chat_test/qa_test.rs
frontend/rust-lib/flowy-ai/tests/chat_test/related_question_test.rs
frontend/rust-lib/event-integration-test/tests/chat/local_chat_test.rs
frontend/rust-lib/flowy-ai/tests/main.rs
frontend/rust-lib/event-integration-test/src/chat_event.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @appflowy - I've reviewed your changes - here's some feedback:

  • The PR introduces two structs named RelatedQuestionChain in different modules (context_question_chain.rs and related_question_chain.rs) with distinct functionalities; consider renaming one for better clarity (e.g., ContextualQuestionChain vs. GeneralFollowUpQuestionChain).
  • The use of tokio::time::sleep within the ConversationalRetrieverChain::stream method for suggested questions might introduce artificial delays; evaluate if this is the intended behavior or if an alternative streaming approach for UI effects is feasible.
  • The serde_json::from_value for QuestionStreamValue in llm_chat.rs falls back to an empty answer on parse failure; consider if a more explicit error handling or logging strategy for deserialization errors would be more robust.
Here's what I looked at during the review
  • 🟡 General issues: 3 issues found
  • 🟢 Security: all looks good
  • 🟡 Testing: 4 issues found
  • 🟢 Complexity: all looks good
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@@ -86,13 +107,44 @@ impl ConversationalRetrieverChain {

if documents.is_empty() {
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (performance): Review thread sleeping delays in streaming suggested questions.

Are these tokio::time::sleep delays necessary? They introduce pauses that can cause unpredictable latency.

Comment on lines +69 to 71
if let Ok(mut w) = self.user_tmpl.try_write() {
*w = template;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Evaluate lock acquisition strategy in set_rag_ids.

try_write() can fail silently and skip the update if the lock isn’t available. If updating the prompt template is critical, use a blocking write() instead.

Suggested change
if let Ok(mut w) = self.user_tmpl.try_write() {
*w = template;
}
let mut w = self.user_tmpl.write().expect("User template RwLock poisoned");
*w = template;

metadata: Option<serde_json::Value>,
) -> Result<ChatMessage, FlowyError> {
let mut message = ChatMessage::new_ai(timestamp(), message.to_string(), Some(question_id));
let message_id = ID_GEN.lock().await.next_id();
Copy link
Contributor

Choose a reason for hiding this comment

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

question: Examine the MessageIDGenerator logic.

Ensure message IDs remain monotonic even if the system clock moves backward.

dbg!(&result);
dbg!(&result.sources);

assert!(!result.answer.is_empty());
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Assert the actual format of the AI's response, not just non-emptiness.

In local_ollama_test_chat_format, add an assertion to verify bullet formatting (e.g., assert!(result.answer.contains("* ")) or assert!(result.answer.contains("- "))) instead of only checking non-emptiness.

Suggested implementation:

  assert!(!result.answer.is_empty());
  assert!(result.answer.contains("* ") || result.answer.contains("- "));

This change adds an assertion to ensure that the answer contains either a bullet formatted with "* " or "- " as requested. Make sure this additional assertion aligns with your expectations for the AI's response formatting.

@@ -16,6 +18,33 @@ async fn local_ollama_test_simple_question() {
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Add test for behavior when context-specific question generation is unavailable or fails.

Add a test in local_ollama_test_simple_question that builds a ConversationalRetrieverChain without a store (making context_question_chain None) and simulates an error from generate_questions. Verify it falls back to CAN_NOT_ANSWER_WITH_CONTEXT when RAG IDs exist but no documents or questions are generated.

Suggested implementation:

  // set rag_id but not rag document content, should return CAN_NOT_ANSWER_WITH_CONTEXT
  chat.set_rag_ids(vec![doc_id.to_string()]);
  // simulate missing context-specific question generation by removing the store from the chain
  chat.context_question_chain = None;
  let stream = chat
    .stream_question("hello world", Default::default())
    .await;
  let result_fallback = collect_stream(stream).await;
  assert!(result_fallback.iter().any(|msg| msg.contains("CAN_NOT_ANSWER_WITH_CONTEXT")),
          "Expected fallback message not found when context-specific question generation fails");

Make sure that:

  1. The chat object has a mutable field named context_question_chain.
  2. The fallback message "CAN_NOT_ANSWER_WITH_CONTEXT" is exactly what the code returns when generate_questions fails.
  3. If necessary, adjust the test to match your project's conventions for asynchronous tests and error handling.

use uuid::Uuid;

#[tokio::test]
async fn local_ollama_test_context_related_questions() {
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Test context-specific suggested questions with multiple RAG documents.

Add a case with multiple RAG docs (e.g., doc_id1/doc_id2). After triggering a fallback with an irrelevant question, assert that suggested questions can come from either doc and that each object_id matches its source, to better test ContextQuestionChain’s filtering.

Suggested implementation:

  let doc_id = Uuid::new_v4().to_string();
  let trip_docs = load_asset_content("japan_trip.md");
  chat.set_rag_ids(vec![doc_id.clone()]);

  // Additional test for multiple RAG documents
  let doc_id1 = Uuid::new_v4().to_string();
  let doc_id2 = Uuid::new_v4().to_string();
  chat.set_rag_ids(vec![doc_id1.clone(), doc_id2.clone()]);

  // Trigger fallback with an irrelevant question to test context-specific suggestions
  let fallback_stream = chat
    .stream_question("irrelevant question triggering fallback", Default::default())
    .await
    .unwrap();
  let fallback_result = collect_stream(fallback_stream).await;
  assert!(!fallback_result.answer.is_empty());

  // Assert that suggested questions come from either of the specified RAG documents
  for suggestion in fallback_result.suggested_questions.iter() {
      assert!(
          suggestion.object_id == doc_id1 || suggestion.object_id == doc_id2,
          "Suggested question's object_id does not match any of the RAG document IDs"
      );
  }

Ensure that the fallback_result structure has a suggested_questions field and that each suggestion has an object_id field. Adjust field names if necessary to match your codebase's definitions.

.await
.unwrap();
let result = collect_stream(stream).await;
dbg!(&result.suggested_questions);
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Test behavior if ContextQuestionChain generates no valid questions after filtering.

Add a test that when ContextQuestionChain::generate_questions returns no valid questions (empty or all filtered out), the code falls back to CAN_NOT_ANSWER_WITH_CONTEXT and sets result.gen_related_question to true. You can mock the LLM response or configure context/RAG IDs to trigger this case.

Suggested implementation:

#[tokio::test]
async fn test_no_valid_questions_fallback() {
    // Obtain a chat instance from the test harness. Adjust this to your own method of creating the instance.
    let chat = get_chat_instance(); // Replace or modify if your instance initializer is different.
    let doc_id = "test-doc-id";

    // Embed paragraphs (simulate a valid document with paragraphs)
    chat.embed_paragraphs(&doc_id, vec!["trip_docs_placeholder".to_string()])
        .await
        .unwrap();

    // Trigger the fallback scenario by providing a question that causes ContextQuestionChain::generate_questions
    // to return no valid questions. This prompt should be adjusted to your specific test configuration.
    let stream = chat
        .stream_question("Trigger fallback: no valid questions", Default::default())
        .await
        .unwrap();

    let result = collect_stream(stream).await;
    dbg!(&result.suggested_questions);

    // Assert that the fallback behavior is triggered:
    // 1. A default fallback question "CAN_NOT_ANSWER_WITH_CONTEXT" is returned.
    // 2. gen_related_question is set to true.
    assert_eq!(result.suggested_questions.len(), 1, "Should provide one fallback question");
    assert_eq!(result.suggested_questions[0].content, "CAN_NOT_ANSWER_WITH_CONTEXT");
    assert!(result.gen_related_question, "gen_related_question should be true when fallback is triggered");

    // Optionally, verify that the suggested question's object_id matches the given doc_id.
    assert_eq!(result.suggested_questions[0].object_id, doc_id);
}

Depending on your codebase, you may need to ensure that:

  1. The get_chat_instance() method is correctly defined or replaced with your actual chat initialization.
  2. The input prompt "Trigger fallback: no valid questions" or other parameters are appropriately set to simulate the fallback scenario.
  3. The expected fallback string "CAN_NOT_ANSWER_WITH_CONTEXT" matches exactly what is returned by your implementation.
    Adjust as necessary to reflect your project specifics.

@appflowy appflowy force-pushed the vault_workspace_suggested_question branch from 8614970 to 1d4aaa1 Compare May 9, 2025 13:24
@appflowy appflowy merged commit c986e41 into main May 9, 2025
8 checks passed
@appflowy appflowy deleted the vault_workspace_suggested_question branch May 9, 2025 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants