feat: add web reader api (#63)
This commit is contained in:
parent
b3ba8ec7bc
commit
b0f2e80a79
15 changed files with 405 additions and 9 deletions
|
|
@ -21,6 +21,8 @@ import ai.z.openapi.service.batches.BatchService;
|
|||
import ai.z.openapi.service.batches.BatchServiceImpl;
|
||||
import ai.z.openapi.service.web_search.WebSearchService;
|
||||
import ai.z.openapi.service.web_search.WebSearchServiceImpl;
|
||||
import ai.z.openapi.service.web_reader.WebReaderService;
|
||||
import ai.z.openapi.service.web_reader.WebReaderServiceImpl;
|
||||
import ai.z.openapi.service.videos.VideosService;
|
||||
import ai.z.openapi.service.videos.VideosServiceImpl;
|
||||
import ai.z.openapi.service.assistant.AssistantService;
|
||||
|
|
@ -99,6 +101,9 @@ public abstract class AbstractAiClient extends AbstractClientBaseService {
|
|||
/** Web search service for internet search capabilities */
|
||||
private WebSearchService webSearchService;
|
||||
|
||||
/** Web reader service for parsing web pages */
|
||||
private WebReaderService webReaderService;
|
||||
|
||||
/** Videos service for video processing */
|
||||
private VideosService videosService;
|
||||
|
||||
|
|
@ -230,6 +235,18 @@ public abstract class AbstractAiClient extends AbstractClientBaseService {
|
|||
return webSearchService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the web reader service for parsing web pages. This service reads and
|
||||
* extracts content, metadata, images, and links from URLs.
|
||||
* @return the WebReaderService instance (lazily initialized)
|
||||
*/
|
||||
public synchronized WebReaderService webReader() {
|
||||
if (webReaderService == null) {
|
||||
this.webReaderService = new WebReaderServiceImpl(this);
|
||||
}
|
||||
return webReaderService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the videos service for video processing. This service handles video
|
||||
* analysis, generation, and manipulation.
|
||||
|
|
@ -601,7 +618,7 @@ public abstract class AbstractAiClient extends AbstractClientBaseService {
|
|||
|
||||
/**
|
||||
* Configures network request timeout settings.
|
||||
* @param requestTimeOut the overall request timeout
|
||||
* @param requestTimeOut the overall request timeout, 0 is no timeout
|
||||
* @param connectTimeout the connection timeout
|
||||
* @param readTimeout the read timeout
|
||||
* @param writeTimeout the write timeout
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ public class ZhipuAiClient extends AbstractAiClient {
|
|||
* </p>
|
||||
* <pre>{@code
|
||||
* ZhipuAiClient client = new ZhipuAiClient.Builder("your-api-key")
|
||||
* .networkConfig(30, 10, 30, 30, TimeUnit.SECONDS)
|
||||
* .networkConfig(0, 10, 30, 30, TimeUnit.SECONDS)
|
||||
* .connectionPool(10, 5, TimeUnit.MINUTES)
|
||||
* .enableTokenCache()
|
||||
* .build();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package ai.z.openapi.api.web_reader;
|
||||
|
||||
import ai.z.openapi.service.web_reader.WebReaderRequest;
|
||||
import ai.z.openapi.service.web_reader.WebReaderResult;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.POST;
|
||||
|
||||
/**
|
||||
* Web Reader API for reading and parsing web page content.
|
||||
*/
|
||||
public interface WebReaderApi {
|
||||
|
||||
/**
|
||||
* Read and parse content from a given URL.
|
||||
* @param request reader parameters including url and options
|
||||
* @return parsed content and metadata
|
||||
*/
|
||||
@POST("reader")
|
||||
Single<WebReaderResult> reader(@Body WebReaderRequest request);
|
||||
|
||||
}
|
||||
|
|
@ -97,7 +97,7 @@ public class ZaiConfig {
|
|||
|
||||
/**
|
||||
* Request timeout in specified time unit. The whole timeout for complete calls, is
|
||||
* the okhttp call timeout.
|
||||
* the okhttp call timeout. The 0 value means no timeout.
|
||||
*/
|
||||
private Integer requestTimeOut;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ import ai.z.openapi.utils.RequestSupplier;
|
|||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
|
@ -26,9 +27,10 @@ import java.util.UUID;
|
|||
/**
|
||||
* Audio service implementation
|
||||
*/
|
||||
@Slf4j
|
||||
public class AudioServiceImpl implements AudioService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AudioServiceImpl.class);
|
||||
|
||||
protected static final ObjectMapper mapper = MessageDeserializeFactory.defaultObjectMapper();
|
||||
|
||||
private final AbstractAiClient zAiClient;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
package ai.z.openapi.service.web_reader;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import ai.z.openapi.core.model.ClientRequest;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class WebReaderRequest implements ClientRequest<WebReaderRequest> {
|
||||
|
||||
/**
|
||||
* The target URL to read and parse content from.
|
||||
*/
|
||||
@JsonProperty("url")
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* Unique request identifier, used for tracing.
|
||||
*/
|
||||
@JsonProperty("request_id")
|
||||
private String requestId;
|
||||
|
||||
/**
|
||||
* User ID associated with the request.
|
||||
*/
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* Timeout in seconds for the reader operation.
|
||||
*/
|
||||
@JsonProperty("timeout")
|
||||
private Integer timeout;
|
||||
|
||||
/**
|
||||
* Whether to bypass cache when reading.
|
||||
*/
|
||||
@JsonProperty("no_cache")
|
||||
private Boolean noCache;
|
||||
|
||||
/**
|
||||
* Return format of the reader output, e.g., markdown or plain.
|
||||
*/
|
||||
@JsonProperty("return_format")
|
||||
private String returnFormat;
|
||||
|
||||
/**
|
||||
* Whether to retain image placeholders in the content.
|
||||
*/
|
||||
@JsonProperty("retain_images")
|
||||
private Boolean retainImages;
|
||||
|
||||
/**
|
||||
* Whether to disable GitHub-Flavored Markdown processing.
|
||||
*/
|
||||
@JsonProperty("no_gfm")
|
||||
private Boolean noGfm;
|
||||
|
||||
/**
|
||||
* Whether to keep image data URLs inline.
|
||||
*/
|
||||
@JsonProperty("keep_img_data_url")
|
||||
private Boolean keepImgDataUrl;
|
||||
|
||||
/**
|
||||
* Whether to include images summary in the result.
|
||||
*/
|
||||
@JsonProperty("with_images_summary")
|
||||
private Boolean withImagesSummary;
|
||||
|
||||
/**
|
||||
* Whether to include links summary in the result.
|
||||
*/
|
||||
@JsonProperty("with_links_summary")
|
||||
private Boolean withLinksSummary;
|
||||
|
||||
/**
|
||||
* Validate request fields that require constraints. Ensures {@code url} is non-empty
|
||||
* and a syntactically valid HTTP/HTTPS URL.
|
||||
* @throws IllegalArgumentException if validation fails
|
||||
*/
|
||||
public void validate() {
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("request url cannot be null or empty");
|
||||
}
|
||||
String normalized = url.trim();
|
||||
try {
|
||||
URI initial = new URI(normalized);
|
||||
URI candidate = initial;
|
||||
String scheme = initial.getScheme();
|
||||
if (scheme == null) {
|
||||
String candidateStr = normalized.startsWith("//") ? ("https:" + normalized) : ("https://" + normalized);
|
||||
candidate = new URI(candidateStr);
|
||||
scheme = candidate.getScheme();
|
||||
}
|
||||
if (!("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme))) {
|
||||
throw new IllegalArgumentException("request url must use http or https");
|
||||
}
|
||||
if (candidate.getHost() == null || candidate.getHost().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("request url must contain a valid host");
|
||||
}
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
throw new IllegalArgumentException("request url is invalid: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package ai.z.openapi.service.web_reader;
|
||||
|
||||
import ai.z.openapi.core.model.ClientResponse;
|
||||
import ai.z.openapi.service.model.ChatError;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WebReaderResponse implements ClientResponse<WebReaderResult> {
|
||||
|
||||
private int code;
|
||||
|
||||
private String msg;
|
||||
|
||||
private boolean success;
|
||||
|
||||
private WebReaderResult data;
|
||||
|
||||
private ChatError error;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package ai.z.openapi.service.web_reader;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class WebReaderResult {
|
||||
|
||||
@JsonProperty("reader_result")
|
||||
private ReaderData readerResult;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
public static class ReaderData {
|
||||
|
||||
private Map<String, String> images;
|
||||
|
||||
private Map<String, String> links;
|
||||
|
||||
private String title;
|
||||
|
||||
private String description;
|
||||
|
||||
private String url;
|
||||
|
||||
private String content;
|
||||
|
||||
private String publishedTime;
|
||||
|
||||
private Map<String, Object> metadata;
|
||||
|
||||
private Map<String, Object> external;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package ai.z.openapi.service.web_reader;
|
||||
|
||||
/**
|
||||
* Web reader service interface
|
||||
*/
|
||||
public interface WebReaderService {
|
||||
|
||||
/**
|
||||
* Creates a web reader request to parse a URL.
|
||||
* @param request the web reader request
|
||||
* @return WebReaderResponse containing the reader result
|
||||
*/
|
||||
WebReaderResponse createWebReader(WebReaderRequest request);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package ai.z.openapi.service.web_reader;
|
||||
|
||||
import ai.z.openapi.AbstractAiClient;
|
||||
import ai.z.openapi.api.web_reader.WebReaderApi;
|
||||
import ai.z.openapi.utils.RequestSupplier;
|
||||
|
||||
/**
|
||||
* Web reader service implementation
|
||||
*/
|
||||
public class WebReaderServiceImpl implements WebReaderService {
|
||||
|
||||
private final AbstractAiClient zAiClient;
|
||||
|
||||
private final WebReaderApi webReaderApi;
|
||||
|
||||
public WebReaderServiceImpl(AbstractAiClient zAiClient) {
|
||||
this.zAiClient = zAiClient;
|
||||
this.webReaderApi = zAiClient.retrofit().create(WebReaderApi.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebReaderResponse createWebReader(WebReaderRequest request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("request cannot be null");
|
||||
}
|
||||
request.validate();
|
||||
RequestSupplier<WebReaderRequest, WebReaderResult> supplier = webReaderApi::reader;
|
||||
return this.zAiClient.executeRequest(request, supplier, WebReaderResponse.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,6 +4,9 @@ import ai.z.openapi.core.config.ZaiConfig;
|
|||
import ai.z.openapi.core.token.HttpRequestInterceptor;
|
||||
import okhttp3.ConnectionPool;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.internal.Util;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
|
@ -15,6 +18,8 @@ import java.util.concurrent.TimeUnit;
|
|||
*/
|
||||
public final class OkHttps {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OkHttps.class);
|
||||
|
||||
// The default value is 0 which imposes no timeout.
|
||||
private static final int DEFAULT_CALL_TIMEOUT_SECONDS = 0;
|
||||
|
||||
|
|
@ -60,37 +65,53 @@ public final class OkHttps {
|
|||
*/
|
||||
private static void configureTimeouts(OkHttpClient.Builder builder, ZaiConfig config) {
|
||||
TimeUnit timeUnit = config.getTimeOutTimeUnit();
|
||||
|
||||
int callTimeout;
|
||||
int connectTimeout;
|
||||
int readTimeout;
|
||||
int writeTimeout;
|
||||
// Configure call timeout
|
||||
if (config.getRequestTimeOut() != null && config.getRequestTimeOut() > 0) {
|
||||
builder.callTimeout(config.getRequestTimeOut(), timeUnit);
|
||||
callTimeout = Util.checkDuration("callTimeout", config.getRequestTimeOut(), timeUnit);
|
||||
}
|
||||
else {
|
||||
builder.callTimeout(DEFAULT_CALL_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
callTimeout = Util.checkDuration("callTimeout", DEFAULT_CALL_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
// Configure connect timeout
|
||||
if (config.getConnectTimeout() != null && config.getConnectTimeout() > 0) {
|
||||
builder.connectTimeout(config.getConnectTimeout(), timeUnit);
|
||||
connectTimeout = Util.checkDuration("connectTimeout", config.getConnectTimeout(), timeUnit);
|
||||
}
|
||||
else {
|
||||
builder.connectTimeout(DEFAULT_CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
connectTimeout = Util.checkDuration("connectTimeout", DEFAULT_CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
// Configure read timeout
|
||||
if (config.getReadTimeout() != null && config.getReadTimeout() > 0) {
|
||||
builder.readTimeout(config.getReadTimeout(), timeUnit);
|
||||
readTimeout = Util.checkDuration("readTimeout", config.getReadTimeout(), timeUnit);
|
||||
}
|
||||
else {
|
||||
builder.readTimeout(DEFAULT_READ_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
readTimeout = Util.checkDuration("readTimeout", DEFAULT_READ_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
// Configure write timeout
|
||||
if (config.getWriteTimeout() != null && config.getWriteTimeout() > 0) {
|
||||
builder.writeTimeout(config.getWriteTimeout(), timeUnit);
|
||||
writeTimeout = Util.checkDuration("writeTimeout", config.getWriteTimeout(), timeUnit);
|
||||
}
|
||||
else {
|
||||
builder.writeTimeout(DEFAULT_WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
writeTimeout = Util.checkDuration("writeTimeout", DEFAULT_WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
if (callTimeout != 0 && (callTimeout <= (connectTimeout + readTimeout + writeTimeout))) {
|
||||
logger.error("Wrong Request(Call) timeout configuration");
|
||||
logger.error(
|
||||
"Request(Call) timeout is less than or equal to the sum of connect, read, and write timeouts. This may cause issues with the client.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package ai.z.openapi.service.web_reader;
|
||||
|
||||
import ai.z.openapi.ZaiClient;
|
||||
import ai.z.openapi.core.config.ZaiConfig;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("WebReaderService Tests")
|
||||
public class WebReaderServiceTest {
|
||||
|
||||
private WebReaderService webReaderService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ZaiConfig zaiConfig = new ZaiConfig();
|
||||
if (zaiConfig.getApiKey() == null) {
|
||||
zaiConfig.setApiKey("id.test-api-key");
|
||||
}
|
||||
ZaiClient client = new ZaiClient(zaiConfig);
|
||||
webReaderService = client.webReader();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test WebReaderService Instantiation")
|
||||
void testWebReaderServiceInstantiation() {
|
||||
assertNotNull(webReaderService, "WebReaderService should be instantiated");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -34,9 +34,8 @@ public class CustomClientExample {
|
|||
.baseUrl(Constants.ZHIPU_AI_BASE_URL)
|
||||
.customHeaders(Collections.emptyMap())
|
||||
.disableTokenCache(true)
|
||||
.requestTimeOut(600)
|
||||
.readTimeout(600)
|
||||
.timeOutTimeUnit(TimeUnit.SECONDS)
|
||||
.connectTimeout(60)
|
||||
.connectionPoolKeepAliveDuration(10)
|
||||
.connectionPoolTimeUnit(TimeUnit.SECONDS)
|
||||
.connectionPoolMaxIdleConnections(20)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public class CustomTimeoutExample {
|
|||
// export ZAI_API_KEY=your.api_key
|
||||
// for Z.ai use the `ZaiClient`, for Zhipu AI use the ZhipuAiClient
|
||||
ZhipuAiClient client = ZhipuAiClient.builder()
|
||||
.networkConfig(30, 10, 30, 30, TimeUnit.SECONDS)
|
||||
.networkConfig(0, 10, 30, 30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
// Create chat request
|
||||
|
|
|
|||
80
samples/src/main/ai.z.openapi.samples/WebReaderExample.java
Normal file
80
samples/src/main/ai.z.openapi.samples/WebReaderExample.java
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package ai.z.openapi.samples;
|
||||
|
||||
import ai.z.openapi.ZaiClient;
|
||||
import ai.z.openapi.service.web_reader.WebReaderRequest;
|
||||
import ai.z.openapi.service.web_reader.WebReaderResponse;
|
||||
import ai.z.openapi.service.web_reader.WebReaderResult;
|
||||
|
||||
/**
|
||||
* Web Reader Example
|
||||
* Demonstrates how to use ZaiClient for web page reading and parsing capabilities
|
||||
*/
|
||||
public class WebReaderExample {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// Create client, recommended to set API Key via environment variable
|
||||
// export ZAI_API_KEY=your.api_key
|
||||
// for Z.ai use the `ZaiClient`, for Zhipu AI use the ZhipuAiClient
|
||||
ZaiClient client = ZaiClient.builder().ofZHIPU().build();
|
||||
|
||||
basicWebReader(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Example of basic web reader functionality
|
||||
*/
|
||||
private static void basicWebReader(ZaiClient client) {
|
||||
System.out.println("\n=== Web Reader Example ===");
|
||||
|
||||
// Create web reader request
|
||||
WebReaderRequest request = WebReaderRequest.builder()
|
||||
.url("https://example.com/")
|
||||
.returnFormat("markdown")
|
||||
.withImagesSummary(Boolean.TRUE)
|
||||
.withLinksSummary(Boolean.TRUE)
|
||||
.build();
|
||||
|
||||
try {
|
||||
// Execute request
|
||||
WebReaderResponse response = client.webReader().createWebReader(request);
|
||||
|
||||
if (response.isSuccess()) {
|
||||
System.out.println("Read successful!");
|
||||
|
||||
WebReaderResult result = response.getData();
|
||||
if (result != null && result.getReaderResult() != null) {
|
||||
WebReaderResult.ReaderData data = result.getReaderResult();
|
||||
System.out.println("Title: " + data.getTitle());
|
||||
System.out.println("URL: " + data.getUrl());
|
||||
System.out.println("Description: " + data.getDescription());
|
||||
|
||||
String content = data.getContent();
|
||||
if (content != null) {
|
||||
String preview = content.length() > 300 ? content.substring(0, 300) + "..." : content;
|
||||
System.out.println("\nContent preview:\n" + preview);
|
||||
}
|
||||
|
||||
if (data.getImages() != null) {
|
||||
System.out.println("\nImages count: " + data.getImages().size());
|
||||
}
|
||||
if (data.getLinks() != null) {
|
||||
System.out.println("Links count: " + data.getLinks().size());
|
||||
}
|
||||
}
|
||||
else {
|
||||
System.out.println("No reader result returned.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
System.err.println("Error: " + response.getMsg());
|
||||
if (response.getError() != null) {
|
||||
System.err.println("Error detail: " + response.getError());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
System.err.println("Exception occurred: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue