-
Notifications
You must be signed in to change notification settings - Fork 16
RDBC-959: Add AI agent feature #123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v7.1
Are you sure you want to change the base?
Changes from 4 commits
bd23b75
db1aad7
36b9414
cdd9ab9
f331779
ee65ff8
28fbf7f
ad3fe10
a99bbae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 { | ||
|
||
| 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> { | ||
|
||
| 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); | ||
garayx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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")); | ||
|
||
| } | ||
|
|
||
| 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); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should not this be
or
similar to timeseris, bulkinsert, subscriptions, etc?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done