-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(ragflow): support request-scoped retrieval filters #2633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7959d6d
8c51af6
1786e07
643f39b
df0ced4
fe4b314
5f62215
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,9 @@ | |
| import io.agentscope.core.rag.integration.ragflow.model.RAGFlowResponse; | ||
| import io.agentscope.core.util.JsonUtils; | ||
| import java.io.IOException; | ||
| import java.util.ArrayList; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.concurrent.TimeUnit; | ||
| import okhttp3.Interceptor; | ||
|
|
@@ -102,6 +104,43 @@ public Mono<RAGFlowResponse> retrieve( | |
| Double similarityThreshold, | ||
| Map<String, Object> metadataCondition) { | ||
|
|
||
| return retrieve(question, topK, similarityThreshold, null, metadataCondition); | ||
| } | ||
|
|
||
| /** | ||
| * Retrieve documents with request-specific dataset IDs and metadata conditions. | ||
| * | ||
| * <p>Non-empty request parameters take precedence over values from {@link RAGFlowConfig}. | ||
| * When a request parameter is {@code null} or empty, the corresponding config value is used. | ||
| * The dataset ID list and top-level metadata condition map are copied when this method is | ||
| * called. Later changes to the list or top-level map entries therefore cannot affect the | ||
| * asynchronous request, but nested mutable metadata values are not deep-copied. | ||
| * | ||
| * @param question the query text (required) | ||
| * @param topK the number of documents to retrieve (optional, defaults to config value) | ||
| * @param similarityThreshold the minimum similarity threshold (optional, defaults to config | ||
| * value) | ||
| * @param datasetIds dataset IDs for this request (optional, defaults to config value) | ||
| * @param metadataCondition metadata filtering conditions for this request (optional, defaults | ||
| * to config value) | ||
| * @return a Mono emitting the retrieval response | ||
| */ | ||
| public Mono<RAGFlowResponse> retrieve( | ||
| String question, | ||
| Integer topK, | ||
| Double similarityThreshold, | ||
| List<String> datasetIds, | ||
| Map<String, Object> metadataCondition) { | ||
|
|
||
| List<String> effectiveDatasetIds = | ||
| datasetIds != null && !datasetIds.isEmpty() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [praise] Good defensive-copying pattern. Copying |
||
| ? new ArrayList<>(datasetIds) | ||
| : copyList(config.getDatasetIds()); | ||
| Map<String, Object> effectiveMetadataCondition = | ||
| metadataCondition != null && !metadataCondition.isEmpty() | ||
| ? new HashMap<>(metadataCondition) | ||
| : copyMap(config.getMetadataCondition()); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nit] The shallow copy via
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nit] The shallow copy via
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. I addressed the overbroad wording in The focused knowledge test passes 16/16, and the full RAGFlow test selection passes 98/98. |
||
|
|
||
| return Mono.fromCallable( | ||
| () -> { | ||
| if (question == null || question.trim().isEmpty()) { | ||
|
|
@@ -113,8 +152,10 @@ public Mono<RAGFlowResponse> retrieve( | |
| // Required: question text | ||
| requestBody.put("question", question); | ||
|
|
||
| // Required: dataset_ids (array) | ||
| requestBody.put("dataset_ids", config.getDatasetIds()); | ||
| // Required unless document_ids is provided: dataset_ids (array) | ||
| if (!effectiveDatasetIds.isEmpty()) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [minor] Subtle behavior change: the old code unconditionally put
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [minor] Subtle behavior change: the old code unconditionally put
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for pointing this out. I rechecked the valid configuration path: The updated client omits I updated the PR description to disclose the serialization change and added |
||
| requestBody.put("dataset_ids", effectiveDatasetIds); | ||
| } | ||
|
|
||
| // Optional: document_ids (filter to specific documents) | ||
| if (config.getDocumentIds() != null && !config.getDocumentIds().isEmpty()) { | ||
|
|
@@ -185,11 +226,8 @@ public Mono<RAGFlowResponse> retrieve( | |
| } | ||
|
|
||
| // Optional: metadata_condition for filtering | ||
| if (metadataCondition != null && !metadataCondition.isEmpty()) { | ||
| requestBody.put("metadata_condition", metadataCondition); | ||
| } else if (config.getMetadataCondition() != null | ||
| && !config.getMetadataCondition().isEmpty()) { | ||
| requestBody.put("metadata_condition", config.getMetadataCondition()); | ||
| if (!effectiveMetadataCondition.isEmpty()) { | ||
| requestBody.put("metadata_condition", effectiveMetadataCondition); | ||
| } | ||
|
|
||
| String jsonBody = JsonUtils.getJsonCodec().toJson(requestBody); | ||
|
|
@@ -262,6 +300,14 @@ public Mono<RAGFlowResponse> retrieve( | |
| }); | ||
| } | ||
|
|
||
| private static <T> List<T> copyList(List<T> values) { | ||
| return values == null ? new ArrayList<>() : new ArrayList<>(values); | ||
| } | ||
|
|
||
| private static <K, V> Map<K, V> copyMap(Map<K, V> values) { | ||
| return values == null ? new HashMap<>() : new HashMap<>(values); | ||
| } | ||
|
|
||
| private void handleErrorResponse(int statusCode, String responseBody) { | ||
| logger.error("RAGFlow API error: status={}, body={}", statusCode, responseBody); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,16 +16,21 @@ | |
| package io.agentscope.core.rag.integration.ragflow; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||
| import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
|
||
| import com.fasterxml.jackson.core.type.TypeReference; | ||
| import io.agentscope.core.rag.model.Document; | ||
| import io.agentscope.core.rag.model.RetrieveConfig; | ||
| import io.agentscope.core.util.JsonUtils; | ||
| import java.io.IOException; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import okhttp3.mockwebserver.MockResponse; | ||
| import okhttp3.mockwebserver.MockWebServer; | ||
| import okhttp3.mockwebserver.RecordedRequest; | ||
| import org.junit.jupiter.api.AfterEach; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
@@ -239,6 +244,78 @@ void testRetrieveWithNullChunksResponse() throws Exception { | |
| assertTrue(documents.isEmpty()); | ||
| } | ||
|
|
||
| @Test | ||
| void testRetrieveWithDynamicFiltersAndConfigFallback() throws Exception { | ||
| mockWebServer.enqueue(createSuccessResponse()); | ||
| mockWebServer.enqueue(createSuccessResponse()); | ||
| mockWebServer.enqueue(createSuccessResponse()); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [praise] Good test coverage: verifies two consecutive requests with different dynamic filters produce distinct request bodies, and that empty/null parameters correctly fall back to config values. This gives confidence that request isolation and config fallback both work as intended.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [praise] Good test coverage: verifies two consecutive requests with different dynamic filters produce distinct request bodies, and that empty/null parameters correctly fall back to config values. This gives confidence that request isolation and config fallback both work as intended. |
||
|
|
||
| RAGFlowConfig config = | ||
| RAGFlowConfig.builder() | ||
| .apiKey("test-api-key") | ||
| .baseUrl(mockWebServer.url("").toString().replaceAll("/$", "")) | ||
| .addDatasetId("default-dataset") | ||
| .metadataCondition(Map.of("source", "default")) | ||
| .maxRetries(0) | ||
| .build(); | ||
| RAGFlowKnowledge knowledge = RAGFlowKnowledge.builder().config(config).build(); | ||
| RetrieveConfig retrieveConfig = | ||
| RetrieveConfig.builder().limit(10).scoreThreshold(0.5).build(); | ||
|
|
||
| knowledge | ||
| .retrieve( | ||
| "first query", | ||
| retrieveConfig, | ||
| List.of("dataset-a"), | ||
| Map.of("source", "first")) | ||
| .block(); | ||
| knowledge | ||
| .retrieve( | ||
| "second query", | ||
| retrieveConfig, | ||
| List.of("dataset-b"), | ||
| Map.of("source", "second")) | ||
| .block(); | ||
| knowledge.retrieve("fallback query", retrieveConfig, List.of(), Map.of()).block(); | ||
|
|
||
| Map<String, Object> firstBody = readRequestBody(mockWebServer.takeRequest()); | ||
| Map<String, Object> secondBody = readRequestBody(mockWebServer.takeRequest()); | ||
| Map<String, Object> fallbackBody = readRequestBody(mockWebServer.takeRequest()); | ||
|
|
||
| assertEquals(List.of("dataset-a"), firstBody.get("dataset_ids")); | ||
| assertEquals(Map.of("source", "first"), firstBody.get("metadata_condition")); | ||
| assertEquals(List.of("dataset-b"), secondBody.get("dataset_ids")); | ||
| assertEquals(Map.of("source", "second"), secondBody.get("metadata_condition")); | ||
| assertEquals(List.of("default-dataset"), fallbackBody.get("dataset_ids")); | ||
| assertEquals(Map.of("source", "default"), fallbackBody.get("metadata_condition")); | ||
| } | ||
|
|
||
| @Test | ||
| void testRetrieveWithDocumentOnlyConfigOmitsDatasetIds() throws Exception { | ||
| mockWebServer.enqueue(createSuccessResponse()); | ||
|
|
||
| RAGFlowConfig config = | ||
| RAGFlowConfig.builder() | ||
| .apiKey("test-api-key") | ||
| .baseUrl(mockWebServer.url("").toString().replaceAll("/$", "")) | ||
| .addDocumentId("document-only") | ||
| .maxRetries(0) | ||
| .build(); | ||
| RAGFlowKnowledge knowledge = RAGFlowKnowledge.builder().config(config).build(); | ||
|
|
||
| knowledge.retrieve("document query", null).block(); | ||
|
|
||
| Map<String, Object> requestBody = readRequestBody(mockWebServer.takeRequest()); | ||
| assertFalse(requestBody.containsKey("dataset_ids")); | ||
| assertEquals(List.of("document-only"), requestBody.get("document_ids")); | ||
| } | ||
|
|
||
| private Map<String, Object> readRequestBody(RecordedRequest request) { | ||
| return JsonUtils.getJsonCodec() | ||
| .fromJson( | ||
| request.getBody().readUtf8(), new TypeReference<Map<String, Object>>() {}); | ||
| } | ||
|
|
||
| // === AddDocuments Tests === | ||
|
|
||
| @Test | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[praise] Good defensive-copying pattern. Copying
datasetIdsandmetadataConditioneagerly (beforeMono.fromCallable) ensures that mutations on the caller's collections after this method returns cannot affect the in-flight async request. Clean implementation.