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
@@ -0,0 +1,97 @@
package com.redis.agentmemory.models.common;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Arrays;
import java.util.List;

/**
* Filter for tag-style string fields (user_id, session_id, namespace, topics, entities).
* Match the server-side TagFilter; supports eq, ne, any, all, and startswith operators.
*
* <p>
* Example — match any of several user IDs in one search:
* <pre>{@code
* SearchRequest.builder()
* .userId(TagFilter.any("user-123", "__account__"))
* .build()
* }</pre>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class TagFilter {

@Nullable
private String eq;

@Nullable
private String ne;

@Nullable
@JsonProperty("any")
private List<String> any;

@Nullable
@JsonProperty("all")
private List<String> all;

@Nullable
private String startswith;

private TagFilter() {}

public static TagFilter eq(@NotNull String value) {
TagFilter f = new TagFilter();
f.eq = value;
return f;
}

public static TagFilter ne(@NotNull String value) {
TagFilter f = new TagFilter();
f.ne = value;
return f;
}

public static TagFilter any(@NotNull List<String> values) {
TagFilter f = new TagFilter();
f.any = List.copyOf(values);
return f;
}

public static TagFilter any(@NotNull String... values) {
return any(Arrays.asList(values));
}

public static TagFilter all(@NotNull List<String> values) {
TagFilter f = new TagFilter();
f.all = List.copyOf(values);
return f;
}

public static TagFilter all(@NotNull String... values) {
return all(Arrays.asList(values));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Empty any/all filters rejected

Medium Severity

TagFilter.any and TagFilter.all accept empty lists (including zero-arg varargs), producing JSON such as {"any":[]}. The server TagFilter rejects empty any/all lists, so searches built this way fail validation. List-based SearchRequest helpers omit empty topic/entity lists, but the TagFilter path does not.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 457057f. Configure here.


public static TagFilter startsWith(@NotNull String prefix) {
TagFilter f = new TagFilter();
f.startswith = prefix;
return f;
}

@Nullable
public String getEq() { return eq; }

@Nullable
public String getNe() { return ne; }

@Nullable
public List<String> getAny() { return any; }

@Nullable
public List<String> getAll() { return all; }

@Nullable
public String getStartswith() { return startswith; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.redis.agentmemory.models.common.TagFilter;
import org.jetbrains.annotations.Nullable;

import java.util.List;
Expand Down Expand Up @@ -29,20 +30,20 @@ public class SearchRequest {

@Nullable
@JsonProperty("session_id")
private String sessionId;
private TagFilter sessionId;

@Nullable
private String namespace;
private TagFilter namespace;

@Nullable
private List<String> topics;
private TagFilter topics;

@Nullable
private List<String> entities;
private TagFilter entities;

@Nullable
@JsonProperty("user_id")
private String userId;
private TagFilter userId;

@Nullable
@JsonProperty("distance_threshold")
Expand Down Expand Up @@ -125,47 +126,69 @@ public void setTextScorer(@Nullable String textScorer) {
}

@Nullable
public String getSessionId() {
public TagFilter getSessionId() {
return sessionId;
}

public void setSessionId(@Nullable String sessionId) {
this.sessionId = sessionId != null ? TagFilter.eq(sessionId) : null;
}

public void setSessionId(@Nullable TagFilter sessionId) {
this.sessionId = sessionId;
}

@Nullable
public String getNamespace() {
public TagFilter getNamespace() {
return namespace;
}

public void setNamespace(@Nullable String namespace) {
this.namespace = namespace != null ? TagFilter.eq(namespace) : null;
}

public void setNamespace(@Nullable TagFilter namespace) {
this.namespace = namespace;
}

@Nullable
public List<String> getTopics() {
public TagFilter getTopics() {
return topics;
}

public void setTopics(@Nullable List<String> topics) {
var present = topics != null && !topics.isEmpty();
this.topics = present ? TagFilter.any(topics) : null;
}

public void setTopics(@Nullable TagFilter topics) {
this.topics = topics;
}

@Nullable
public List<String> getEntities() {
public TagFilter getEntities() {
return entities;
}

public void setEntities(@Nullable List<String> entities) {
var present = entities != null && !entities.isEmpty();
this.entities = present ? TagFilter.any(entities) : null;
}

public void setEntities(@Nullable TagFilter entities) {
this.entities = entities;
}

@Nullable
public String getUserId() {
public TagFilter getUserId() {
return userId;
}

public void setUserId(@Nullable String userId) {
this.userId = userId != null ? TagFilter.eq(userId) : null;
}

public void setUserId(@Nullable TagFilter userId) {
this.userId = userId;
}

Expand Down Expand Up @@ -273,11 +296,11 @@ public String toString() {
", searchMode='" + searchMode + '\'' +
", hybridAlpha=" + hybridAlpha +
", textScorer='" + textScorer + '\'' +
", sessionId='" + sessionId + '\'' +
", namespace='" + namespace + '\'' +
", sessionId=" + sessionId +
", namespace=" + namespace +
", topics=" + topics +
", entities=" + entities +
", userId='" + userId + '\'' +
", userId=" + userId +
", distanceThreshold=" + distanceThreshold +
", limit=" + limit +
", offset=" + offset +
Expand Down Expand Up @@ -327,26 +350,53 @@ public Builder textScorer(@Nullable String textScorer) {
}

public Builder sessionId(@Nullable String sessionId) {
request.sessionId = sessionId != null ? TagFilter.eq(sessionId) : null;
return this;
}

public Builder sessionId(@Nullable TagFilter sessionId) {
request.sessionId = sessionId;
return this;
}

public Builder namespace(@Nullable String namespace) {
request.namespace = namespace != null ? TagFilter.eq(namespace) : null;
return this;
}

public Builder namespace(@Nullable TagFilter namespace) {
request.namespace = namespace;
return this;
}

public Builder topics(@Nullable List<String> topics) {
var present = topics != null && !topics.isEmpty();
request.topics = present ? TagFilter.any(topics) : null;
return this;
}

public Builder topics(@Nullable TagFilter topics) {
request.topics = topics;
return this;
}

public Builder entities(@Nullable List<String> entities) {
var present = entities != null && !entities.isEmpty();
request.entities = present ? TagFilter.any(entities) : null;
return this;
}

public Builder entities(@Nullable TagFilter entities) {
request.entities = entities;
return this;
}

public Builder userId(@Nullable String userId) {
request.userId = userId != null ? TagFilter.eq(userId) : null;
return this;
}

public Builder userId(@Nullable TagFilter userId) {
request.userId = userId;
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.redis.agentmemory.exceptions.MemoryClientException;
import com.redis.agentmemory.models.common.AckResponse;
import com.redis.agentmemory.models.common.TagFilter;
import com.redis.agentmemory.models.longtermemory.*;
import okhttp3.*;
import org.jetbrains.annotations.NotNull;
Expand Down Expand Up @@ -86,22 +87,22 @@ public MemoryRecordResults searchLongTermMemories(@NotNull SearchRequest request

// Add filters if present
if (request.getSessionId() != null) {
payload.put("session_id", Map.of("eq", request.getSessionId()));
payload.put("session_id", request.getSessionId());
}
if (request.getUserId() != null) {
payload.put("user_id", Map.of("eq", request.getUserId()));
payload.put("user_id", request.getUserId());
}
if (request.getNamespace() != null) {
payload.put("namespace", Map.of("eq", request.getNamespace()));
payload.put("namespace", request.getNamespace());
} else if (defaultNamespace != null) {
payload.put("namespace", Map.of("eq", defaultNamespace));
payload.put("namespace", TagFilter.eq(defaultNamespace));
}

if (request.getTopics() != null && !request.getTopics().isEmpty()) {
payload.put("topics", Map.of("any", request.getTopics()));
if (request.getTopics() != null) {
payload.put("topics", request.getTopics());
}
if (request.getEntities() != null && !request.getEntities().isEmpty()) {
payload.put("entities", Map.of("any", request.getEntities()));
if (request.getEntities() != null) {
payload.put("entities", request.getEntities());
}
if (request.getDistanceThreshold() != null) {
payload.put("distance_threshold", request.getDistanceThreshold());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.redis.agentmemory.models.common;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class TagFilterTest {

private ObjectMapper objectMapper;

@BeforeEach
void setUp() {
objectMapper = new ObjectMapper();
}

@Test
void eq_serializesToEqOperator() throws Exception {
String json = objectMapper.writeValueAsString(TagFilter.eq("user-123"));
assertEquals("{\"eq\":\"user-123\"}", json);
}

@Test
void ne_serializesToNeOperator() throws Exception {
String json = objectMapper.writeValueAsString(TagFilter.ne("user-123"));
assertEquals("{\"ne\":\"user-123\"}", json);
}

@Test
void any_varargs_serializesToAnyOperator() throws Exception {
String json = objectMapper.writeValueAsString(TagFilter.any("user-123", "__account__"));
assertEquals("{\"any\":[\"user-123\",\"__account__\"]}", json);
}

@Test
void any_list_serializesToAnyOperator() throws Exception {
String json = objectMapper.writeValueAsString(TagFilter.any(List.of("a", "b", "c")));
assertEquals("{\"any\":[\"a\",\"b\",\"c\"]}", json);
}

@Test
void all_serializesToAllOperator() throws Exception {
String json = objectMapper.writeValueAsString(TagFilter.all("x", "y"));
assertEquals("{\"all\":[\"x\",\"y\"]}", json);
}

@Test
void startsWith_serializesToStartswithOperator() throws Exception {
String json = objectMapper.writeValueAsString(TagFilter.startsWith("tenant-"));
assertEquals("{\"startswith\":\"tenant-\"}", json);
}
}
Loading