Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.13.0</version>
</dependency>
<dependency>
<groupId>com.github.luben</groupId>
<artifactId>zstd-jni</artifactId>
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/net/ravendb/client/documents/DocumentStoreBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import net.ravendb.client.primitives.EventHandler;
import net.ravendb.client.primitives.EventHelper;
import net.ravendb.client.primitives.VoidArgs;
import net.ravendb.client.documents.operations.AI.AiOperations;
import org.apache.commons.lang3.StringUtils;

import java.net.MalformedURLException;
Expand Down Expand Up @@ -146,6 +147,15 @@ public TimeSeriesOperations timeSeries() {
}

private DocumentConventions conventions;
private AiOperations aiOperations;

@Override
public AiOperations getAiOperations(){
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not this be

Suggested change
public AiOperations getAiOperations(){
public AiOperations AI(){

or

Suggested change
public AiOperations getAiOperations(){
public AiOperations ai(){

similar to timeseris, bulkinsert, subscriptions, etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if (this.aiOperations == null) {
this.aiOperations = new AiOperations(this);
}
return this.aiOperations;
}

/**
* Gets the conventions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import net.ravendb.client.documents.conventions.DocumentConventions;
import net.ravendb.client.documents.identity.IHiLoIdGenerator;
import net.ravendb.client.documents.indexes.IAbstractIndexCreationTask;
import net.ravendb.client.documents.operations.AI.AiOperations;
import net.ravendb.client.documents.operations.MaintenanceOperationExecutor;
import net.ravendb.client.documents.operations.OperationExecutor;
import net.ravendb.client.documents.session.*;
Expand All @@ -28,6 +29,8 @@ public interface IDocumentStore extends IDisposalNotification {

KeyStore getCertificate();

AiOperations getAiOperations();

IHiLoIdGenerator getHiLoIdGenerator();

void addBeforeStoreListener(EventHandler<BeforeStoreEventArgs> handler);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package net.ravendb.client.documents.commands;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.node.ObjectNode;
import net.ravendb.client.documents.conventions.DocumentConventions;
import net.ravendb.client.documents.operations.AI.agents.AddOrUpdateAiAgentOperation;
import net.ravendb.client.documents.operations.AI.agents.AiAgentConfigurationResult;
import net.ravendb.client.documents.operations.AI.agents.config.AiAgentConfiguration;
import net.ravendb.client.http.IRaftCommand;
import net.ravendb.client.http.RavenCommand;
import net.ravendb.client.http.ServerNode;
import net.ravendb.client.json.ContentProviderHttpEntity;
import net.ravendb.client.util.RaftIdGenerator;
import org.apache.hc.client5.http.classic.methods.HttpPut;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.core5.http.ContentType;
import java.io.IOException;

public class AddOrUpdateAiAgentCommand extends RavenCommand<AiAgentConfigurationResult> implements IRaftCommand {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the class is public
in cs private sealed class AddOrUpdateAiAgentOperationCommand
it means it is not exposed to the client

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

private final AiAgentConfiguration configuration;
private final DocumentConventions conventions;
private final Object sampleSchema;

public AddOrUpdateAiAgentCommand(AiAgentConfiguration configuration, Object sampleSchema, DocumentConventions conventions) {
super(AiAgentConfigurationResult.class);
if(AddOrUpdateAiAgentOperation.hasNoSampleObjectAndScheme(configuration))
throw new IllegalArgumentException("Please provide a non-empty value for either outputSchema or sampleObject.");
this.configuration = configuration;
this.conventions = conventions;
this.sampleSchema = sampleSchema;
}

@Override
public boolean isReadRequest() {return false;}

@Override
public String getRaftUniqueRequestId() {
return RaftIdGenerator.newId();
}

@Override
public HttpUriRequestBase createRequest(ServerNode node) {
String uri = node.getUrl() + "/databases/" + node.getDatabase() + "/admin/ai/agent";

if (this.configuration == null && sampleSchema != null) {
this.configuration.setSampleObject(sampleSchema.toString());
}

HttpPut request = new HttpPut(uri);

request.setEntity(new ContentProviderHttpEntity(outputStream -> {
try (JsonGenerator generator = createSafeJsonGenerator(outputStream)) {
ObjectNode config = mapper.valueToTree(configuration);
generator.writeTree(config);
}
}, ContentType.APPLICATION_JSON, conventions));

return request;
}

@Override
public void setResponse(String response, boolean fromCache) throws IOException {
result = mapper.readValue(response, AiAgentConfigurationResult.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package net.ravendb.client.documents.commands;

import net.ravendb.client.documents.conventions.DocumentConventions;
import net.ravendb.client.documents.operations.AI.agents.AiAgentConfigurationResult;
import net.ravendb.client.http.RavenCommand;
import net.ravendb.client.http.ServerNode;
import net.ravendb.client.util.UrlUtils;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;

import java.io.IOException;

public class DeleteAiAgentCommand extends RavenCommand<AiAgentConfigurationResult> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same issue
in the cs code it is private.
it needs to be an innerclass of DeleteAiAgentOperation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

private final String identifier;
private final DocumentConventions conventions;

public DeleteAiAgentCommand(String identifier, DocumentConventions conventions) {
super(AiAgentConfigurationResult.class);
this.identifier = identifier;
this.conventions = conventions;
}

@Override
public boolean isReadRequest() {return false;}

@Override
public HttpUriRequestBase createRequest(ServerNode node){
String uri = node.getUrl() + "/databases/" + node.getDatabase() + "/admin/ai/agent"
+ "?agentId=" + UrlUtils.escapeDataString(identifier);
return new HttpDelete(uri);
}

@Override
public void setResponse(String response, boolean fromCache) throws IOException {
result = mapper.readValue(response, AiAgentConfigurationResult.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package net.ravendb.client.documents.commands;

import net.ravendb.client.documents.conventions.DocumentConventions;
import net.ravendb.client.documents.operations.AI.agents.GetAiAgentsResponse;
import net.ravendb.client.http.RavenCommand;
import net.ravendb.client.http.ServerNode;
import net.ravendb.client.util.UrlUtils;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import java.io.IOException;

public class GetAiAgentsCommand extends RavenCommand<GetAiAgentsResponse> {
private final String agentId;
private final DocumentConventions conventions;

public GetAiAgentsCommand(String agentId, DocumentConventions conventions) {
super(GetAiAgentsResponse.class);
this.agentId = agentId;
this.conventions = conventions;
}

@Override
public boolean isReadRequest() {
return true;
}

@Override
public HttpUriRequestBase createRequest(ServerNode node) {
StringBuilder uri = new StringBuilder(node.getUrl())
.append("/databases/")
.append(node.getDatabase())
.append("/admin/ai/agent");

if (agentId != null && !agentId.isEmpty()) {
uri.append("?agentId=").append(UrlUtils.escapeDataString(agentId));
}

return new HttpGet(uri.toString());
}

@Override
public void setResponse(String response, boolean fromCache) throws IOException {
result = mapper.readValue(response, GetAiAgentsResponse.class);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package net.ravendb.client.documents.commands;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import net.ravendb.client.documents.conventions.DocumentConventions;
import net.ravendb.client.documents.operations.AI.AiStreamCallback;
import net.ravendb.client.documents.operations.AI.agents.AiAgentActionResponse;
import net.ravendb.client.documents.operations.AI.agents.AiConversationCreationOptions;
import net.ravendb.client.documents.operations.AI.agents.ConversationResult;
import net.ravendb.client.http.IRaftCommand;
import net.ravendb.client.http.RavenCommand;
import net.ravendb.client.http.RavenCommandResponseType;
import net.ravendb.client.http.ServerNode;
import net.ravendb.client.json.ContentProviderHttpEntity;
import net.ravendb.client.util.RaftIdGenerator;
import net.ravendb.client.util.UrlUtils;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.core5.http.ContentType;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.stream.Collectors;

public class RunConversationCommand<TAnswer>
extends RavenCommand<ConversationResult<TAnswer>>
implements IRaftCommand {

private final String conversationId;
private final String agentId;
private final String prompt;
private final List<AiAgentActionResponse> actionResponses;
private final AiConversationCreationOptions options;
private final String changeVector;
private final String streamPropertyPath;
private final AiStreamCallback streamCallback;
private final DocumentConventions conventions;
private String raftId;

public RunConversationCommand(
String conversationId,
String agentId,
String prompt,
List<AiAgentActionResponse> actionResponses,
AiConversationCreationOptions options,
String changeVector,
DocumentConventions conventions,
String streamPropertyPath,
AiStreamCallback streamCallback){
super((Class<ConversationResult<TAnswer>>) (Class<?>) ConversationResult.class);
this.conversationId = conversationId;
this.agentId = agentId;
this.prompt = prompt;
this.actionResponses = actionResponses;
this.options = options;
this.changeVector = changeVector;
this.streamPropertyPath = streamPropertyPath;
this.streamCallback = streamCallback;
this.conventions = conventions;

if (this.streamPropertyPath != null && this.streamCallback != null) {
this.responseType = RavenCommandResponseType.RAW;
}

if (conversationId != null && conversationId.endsWith("|")) {
this.raftId = RaftIdGenerator.newId();
}
}

@Override
public boolean isReadRequest() {
return false;
}

@Override
public String getRaftUniqueRequestId() {
return raftId;
}

@Override
public HttpUriRequestBase createRequest(ServerNode node) {
StringBuilder uriBuilder = new StringBuilder();
uriBuilder.append(node.getUrl())
.append("/databases/")
.append(node.getDatabase())
.append("/ai/agent?")
.append("conversationId=").append(UrlUtils.escapeDataString(this.conversationId))
.append("&agentId=").append(UrlUtils.escapeDataString(this.agentId));

if (this.changeVector != null && !this.changeVector.isEmpty()) {
uriBuilder.append("&changeVector=").append(UrlUtils.escapeDataString(this.changeVector));
}
if (this.streamPropertyPath != null) {
uriBuilder.append("&streamPropertyPath=").append(UrlUtils.escapeDataString(this.streamPropertyPath));
uriBuilder.append("&streaming=").append(UrlUtils.escapeDataString("true"));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why escape ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

HttpPost request = new HttpPost(uriBuilder.toString());

request.setEntity(new ContentProviderHttpEntity(outputStream -> {
try (JsonGenerator generator = createSafeJsonGenerator(outputStream)) {
ObjectNode bodyObj = mapper.createObjectNode();
bodyObj.set("ActionResponses", mapper.valueToTree(this.actionResponses));
bodyObj.put("UserPrompt", this.prompt);
bodyObj.set("CreationOptions", mapper.valueToTree(this.options));
generator.writeTree(bodyObj);
}
}, ContentType.APPLICATION_JSON,conventions));

return request;
}

@Override
public CompletableFuture<String> setResponseAsync(InputStream bodyStream, boolean fromCache) {
if (bodyStream == null ) {
this.throwInvalidResponse();
}

if (this.streamPropertyPath != null && this.streamCallback != null) {
return processStreamingResponse(bodyStream);
}
return this.parseResponseDefaultAsync(bodyStream);
}

private CompletableFuture<String> parseResponseDefaultAsync(InputStream bodyStream) {
return CompletableFuture.supplyAsync(() -> {
try {
String body = new BufferedReader(new InputStreamReader(bodyStream, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));

this.result = parseAndTransform(body, new TypeReference<ConversationResult<TAnswer>>() {});
return body;
} catch (Exception e) {
throw new CompletionException(e);
}
});
}

private CompletableFuture<String> processStreamingResponse(InputStream bodyStream) {
return CompletableFuture.supplyAsync(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(bodyStream, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.trim().isEmpty()) continue;

if (line.startsWith("{")) {
this.result = parseAndTransform(line, new TypeReference<ConversationResult<TAnswer>>() {});
return line;
}

try {
Object parsed = new ObjectMapper().readValue(line, Object.class);
String chunk;
if (parsed instanceof String) {
chunk = (String) parsed;
} else {
chunk = new ObjectMapper().writeValueAsString(parsed);
}
streamCallback.onChunk(chunk).get();
} catch (Exception e) {
streamCallback.onChunk(line).get();
}
}

if (this.result == null) {
throw new IllegalStateException("No final result received in streaming response");
}

return null;
} catch (Exception e) {
throw new CompletionException(e);
}
});
}
}
Loading
Loading