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