Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
* }
* }</pre>
*
* <p>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}.
*
* <p>Example usage:
* <pre>{@code
* DifyResponse response = difyClient.retrieve(...).block();
Expand Down
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>Dify serializes the top-level {@code query} field of a retrieval response in two different
* ways depending on the deployment:
*
* <pre>{@code
* // Dify Cloud (api.dify.ai)
* {"query": "ping", "records": [...]}
*
* // Self-hosted Dify
* {"query": {"content": "ping"}, "records": [...]}
* }</pre>
*
* <p>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<DifyResponse.Query> {

@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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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}.
*
* <p>This class maps the JSON response from Dify's retrieve endpoint:
* <pre>{@code
* {
Expand Down Expand Up @@ -65,8 +69,12 @@ public void setRecords(List<Record> records) {

/**
* Query information from the response.
*
* <p>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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Loading