From 979748765a08f0a511da62e0ef35f1deaf497fca Mon Sep 17 00:00:00 2001 From: wahl <592211009@qq.com> Date: Sun, 11 Oct 2026 17:21:24 +0800 Subject: [PATCH] fix(dify): accept a bare string for the retrieval response query field Dify Cloud returns the top-level "query" field of a knowledge base retrieval response as a bare string, while self-hosted Dify returns an object. DifyResponse modelled it as an object only, so Jackson could not bind the payload and threw, discarding every retrieved record and leaving RAG retrieval empty. Deserialize both shapes with a dedicated deserializer. The field is informational -- the Dify integration never reads it -- so a shape that is neither a string nor an object now yields a null query instead of failing the whole response. Fixes #1917 --- .../dify/DifyDocumentConverter.java | 4 ++ .../dify/model/DifyQueryDeserializer.java | 71 +++++++++++++++++++ .../integration/dify/model/DifyResponse.java | 8 +++ .../integration/dify/DifyRAGClientTest.java | 64 +++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/main/java/io/agentscope/core/rag/integration/dify/model/DifyQueryDeserializer.java diff --git a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/main/java/io/agentscope/core/rag/integration/dify/DifyDocumentConverter.java b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/main/java/io/agentscope/core/rag/integration/dify/DifyDocumentConverter.java index 5a522f7915..7e91c38c06 100644 --- a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/main/java/io/agentscope/core/rag/integration/dify/DifyDocumentConverter.java +++ b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/main/java/io/agentscope/core/rag/integration/dify/DifyDocumentConverter.java @@ -51,6 +51,10 @@ * } * } * + *

The top-level {@code query} field is a bare string on Dify Cloud + * ({@code "query": "..."}) and an object on self-hosted Dify; see + * {@link io.agentscope.core.rag.integration.dify.model.DifyQueryDeserializer}. + * *

Example usage: *

{@code
  * DifyResponse response = difyClient.retrieve(...).block();
diff --git a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/main/java/io/agentscope/core/rag/integration/dify/model/DifyQueryDeserializer.java b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/main/java/io/agentscope/core/rag/integration/dify/model/DifyQueryDeserializer.java
new file mode 100644
index 0000000000..be0fc6b588
--- /dev/null
+++ b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/main/java/io/agentscope/core/rag/integration/dify/model/DifyQueryDeserializer.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.rag.integration.dify.model;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.IOException;
+
+/**
+ * Custom deserializer for {@link DifyResponse.Query}.
+ *
+ * 

Dify serializes the top-level {@code query} field of a retrieval response in two different + * ways depending on the deployment: + * + *

{@code
+ * // Dify Cloud (api.dify.ai)
+ * {"query": "ping", "records": [...]}
+ *
+ * // Self-hosted Dify
+ * {"query": {"content": "ping"}, "records": [...]}
+ * }
+ * + *

The field is informational only, so an unexpected shape must not fail the whole response. + * Modelling it as an object alone discarded every retrieved record when talking to Dify Cloud. + */ +public class DifyQueryDeserializer extends JsonDeserializer { + + @Override + public DifyResponse.Query deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException { + + JsonNode node = p.getCodec().readTree(p); + + if (node == null || node.isNull()) { + return null; + } + + DifyResponse.Query query = new DifyResponse.Query(); + + if (node.isTextual()) { + query.setContent(node.asText()); + return query; + } + + if (node.isObject()) { + JsonNode content = node.get("content"); + if (content != null && content.isTextual()) { + query.setContent(content.asText()); + } + return query; + } + + // Unknown shape: the query text is informational, keep the records. + return null; + } +} diff --git a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/main/java/io/agentscope/core/rag/integration/dify/model/DifyResponse.java b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/main/java/io/agentscope/core/rag/integration/dify/model/DifyResponse.java index bd258fdad0..d1cb8ec5f9 100644 --- a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/main/java/io/agentscope/core/rag/integration/dify/model/DifyResponse.java +++ b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/main/java/io/agentscope/core/rag/integration/dify/model/DifyResponse.java @@ -17,11 +17,15 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import java.util.List; /** * Response model for Dify knowledge base retrieval API. * + *

Depending on the deployment, the top-level {@code query} field is either a bare string + * (Dify Cloud) or an object (self-hosted Dify); see {@link DifyQueryDeserializer}. + * *

This class maps the JSON response from Dify's retrieve endpoint: *

{@code
  * {
@@ -65,8 +69,12 @@ public void setRecords(List records) {
 
     /**
      * Query information from the response.
+     *
+     * 

Dify Cloud returns {@code "query": "..."} while self-hosted Dify returns + * {@code "query": {"content": "..."}}; both shapes are handled by {@link DifyQueryDeserializer}. */ @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize(using = DifyQueryDeserializer.class) public static class Query { private String content; diff --git a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/test/java/io/agentscope/core/rag/integration/dify/DifyRAGClientTest.java b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/test/java/io/agentscope/core/rag/integration/dify/DifyRAGClientTest.java index 2bca9bc819..590fd8dd8d 100644 --- a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/test/java/io/agentscope/core/rag/integration/dify/DifyRAGClientTest.java +++ b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-dify/src/test/java/io/agentscope/core/rag/integration/dify/DifyRAGClientTest.java @@ -408,6 +408,70 @@ void testParseSuccessfulResponse() throws Exception { assertEquals(0.95, response.getRecords().get(0).getScore(), 0.001); } + @Test + void testParseResponseWithBareStringQuery() throws Exception { + // Dify Cloud returns "query" as a bare string, while the documented self-hosted + // shape is {"content": "..."}. Both shapes must be accepted, otherwise the whole + // retrieval fails and every record is dropped. + String responseBody = + """ + { + "query": "What is RAG?", + "records": [ + { + "segment": { + "id": "seg-1", + "content": "Test content", + "document_id": "doc-1" + }, + "score": 0.95 + } + ] + } + """; + mockWebServer.enqueue( + new MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(responseBody)); + + DifyRAGClient client = new DifyRAGClient(createConfig()); + DifyResponse response = client.retrieve("test query", 5).block(); + + assertNotNull(response); + assertNotNull(response.getQuery()); + assertEquals("What is RAG?", response.getQuery().getContent()); + assertNotNull(response.getRecords()); + assertEquals(1, response.getRecords().size()); + assertEquals("seg-1", response.getRecords().get(0).getSegment().getId()); + } + + @Test + void testParseResponseWithObjectQuery() throws Exception { + // Self-hosted Dify serializes query as {"content": "..."}; keep that shape working. + String responseBody = + """ + { + "query": { "content": "What is RAG?" }, + "records": [] + } + """; + mockWebServer.enqueue( + new MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(responseBody)); + + DifyRAGClient client = new DifyRAGClient(createConfig()); + DifyResponse response = client.retrieve("test query", 5).block(); + + assertNotNull(response); + assertNotNull(response.getQuery()); + assertEquals("What is RAG?", response.getQuery().getContent()); + assertNotNull(response.getRecords()); + assertTrue(response.getRecords().isEmpty()); + } + @Test void testHandleEmptyResponse() throws Exception { mockWebServer.enqueue(createEmptyResponse());