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());