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
2 changes: 1 addition & 1 deletion symphony-bdk-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ dependencies {
}

// OpenAPI code generation
def apiBaseUrl = "https://raw.githubusercontent.com/finos/symphony-api-spec/a0b152881a0405b7510cfc7e3a9698e8cb8c566c"
def apiBaseUrl = "https://raw.githubusercontent.com/finos/symphony-api-spec/8aeca61d3724e040a93fbf4e4ddb13800174d6ea"
def generatedFolder = "${layout.buildDirectory.get()}/generated/openapi"
def apisToGenerate = [
Agent: 'agent/agent-api-public-deprecated.yaml',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,16 @@ List<V4Message> searchMessages(AuthSession session, MessageSearchQuery query,
List<V4Message> searchMessagesSemantic(AuthSession session, String query,
@Nullable String streamId, @Nullable PaginationAttribute pagination) throws Exception;

/**
* Semantic search restricted to a date range, called only when {@code from} or {@code to} is set.
*
* <p>The default implementation rejects the call, so that overrides predating the date range never
* silently return messages outside of it.
*/
default List<V4Message> searchMessagesSemantic(AuthSession session, String query, @Nullable String streamId,
@Nullable Instant from, @Nullable Instant to, @Nullable PaginationAttribute pagination) throws Exception {
throw new UnsupportedOperationException("Semantic search with a date range is not supported by this override");
}

V4Message getMessage(AuthSession session, String messageId) throws Exception;
}
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,30 @@ public List<V4Message> searchMessagesSemantic(String query, @Nullable Pagination
@Override
public List<V4Message> searchMessagesSemantic(String query, @Nullable String streamId,
@Nullable PaginationAttribute pagination) {
return searchMessagesSemantic(query, streamId, null, null, pagination);
}

/**
* {@inheritDoc}
*/
@Override
public List<V4Message> searchMessagesSemantic(String query, @Nullable String streamId, @Nullable Instant from,
@Nullable Instant to, @Nullable PaginationAttribute pagination) {
if (from != null && to != null && !from.isBefore(to)) {
throw new IllegalArgumentException("The 'from' instant must be before the 'to' instant.");
}
if (retrieverOverride != null) {
// overrides predating the date range only implement the search without it
return executeAndRetry("searchMessagesSemantic", messagesApi.getApiClient().getBasePath(),
() -> wrapOverrideException(
() -> retrieverOverride.searchMessagesSemantic(authSession, query, streamId, pagination)));
() -> wrapOverrideException(() -> from == null && to == null
? retrieverOverride.searchMessagesSemantic(authSession, query, streamId, pagination)
: retrieverOverride.searchMessagesSemantic(authSession, query, streamId, from, to, pagination)));
}
final SemanticSearchQuery searchQuery = new SemanticSearchQuery()
.text(query)
.threadId(streamId != null ? toUrlSafeIdIfNeeded(streamId) : null);
.threadId(streamId != null ? toUrlSafeIdIfNeeded(streamId) : null)
.fromDate(from != null ? from.toEpochMilli() : null)
.toDate(to != null ? to.toEpochMilli() : null);
return executeAndRetry("searchMessagesSemantic", messagesApi.getApiClient().getBasePath(),
() -> messagesApi.v4MessageSearchSemanticPost(
pagination != null ? pagination.getSkip() : null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,4 +281,21 @@ public interface OboMessageService {
*/
List<V4Message> searchMessagesSemantic(String query, @Nullable String streamId,
@Nullable PaginationAttribute pagination);

/**
* Searches for messages using a natural-language semantic query. Unlike the keyword based
* {@code /v1/message/search} endpoint, the query is interpreted by meaning rather than matched as
* {@code field:value} pairs.
*
* @param query the natural-language search query.
* @param streamId the stream to restrict the search to. If {@code null}, all accessible streams are searched.
* @param from only messages sent on or after this instant are returned. If {@code null}, no lower bound.
* @param to only messages sent before this instant are returned. If {@code null}, no upper bound.
* @param pagination the skip and limit for pagination.
* @return the list of matching messages.
* @throws IllegalArgumentException if {@code from} is not before {@code to}.
* @see <a href="https://developers.symphony.com/restapi/reference/semantic-search-messages">Semantic search messages</a>
*/
List<V4Message> searchMessagesSemantic(String query, @Nullable String streamId, @Nullable Instant from,
@Nullable Instant to, @Nullable PaginationAttribute pagination);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.symphony.bdk.core.util.IdUtil.fromUrlSafeId;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
Expand Down Expand Up @@ -38,6 +39,7 @@
import com.symphony.bdk.gen.api.model.MessageSearchQuery;
import com.symphony.bdk.gen.api.model.MessageStatus;
import com.symphony.bdk.gen.api.model.MessageSuppressionResponse;
import com.symphony.bdk.gen.api.model.SemanticSearchQuery;
import com.symphony.bdk.gen.api.model.StreamAttachmentItem;
import com.symphony.bdk.gen.api.model.V4ImportResponse;
import com.symphony.bdk.gen.api.model.V4Message;
Expand Down Expand Up @@ -290,6 +292,82 @@ void testSearchMessagesSemanticWithAllParameters() throws IOException {
messages.stream().map(V4Message::getMessageId).collect(Collectors.toList()));
}

@Test
void testSearchMessagesSemanticWithDateRange() throws Exception {
MessagesApi spiedMessagesApi = spy(new MessagesApi(mockApiClient.getApiClient("/agent")));
MessageService service = new MessageService(spiedMessagesApi, messageApi, messageSuppressionApi, streamsApi,
podApi, attachmentsApi, defaultApi, authSession, templateEngine, new RetryWithRecoveryBuilder<>());
mockApiClient.onPost(V4_SEARCH_MESSAGES_SEMANTIC,
JsonHelper.readFromClasspath("/message/get_message_stream_id.json"));
final Instant from = Instant.parse("2026-07-23T00:00:00Z");
final Instant to = Instant.parse("2026-07-26T00:00:00Z");

final List<V4Message> messages = service.searchMessagesSemantic("query", null, from, to, null);

assertEquals(2, messages.size());
ArgumentCaptor<SemanticSearchQuery> queryCaptor = ArgumentCaptor.forClass(SemanticSearchQuery.class);
verify(spiedMessagesApi).v4MessageSearchSemanticPost(isNull(), isNull(), eq(TOKEN), eq(TOKEN),
queryCaptor.capture());
assertEquals("query", queryCaptor.getValue().getText());
assertEquals(1784764800000L, queryCaptor.getValue().getFromDate());
assertEquals(1785024000000L, queryCaptor.getValue().getToDate());
}

@Test
void testSearchMessagesSemanticWithOpenEndedDateRange() throws Exception {
MessagesApi spiedMessagesApi = spy(new MessagesApi(mockApiClient.getApiClient("/agent")));
MessageService service = new MessageService(spiedMessagesApi, messageApi, messageSuppressionApi, streamsApi,
podApi, attachmentsApi, defaultApi, authSession, templateEngine, new RetryWithRecoveryBuilder<>());
mockApiClient.onPost(V4_SEARCH_MESSAGES_SEMANTIC,
JsonHelper.readFromClasspath("/message/get_message_stream_id.json"));
final Instant from = Instant.parse("2026-07-23T00:00:00Z");

service.searchMessagesSemantic("query", STREAM_ID, from, null, new PaginationAttribute(0, 10));

ArgumentCaptor<SemanticSearchQuery> queryCaptor = ArgumentCaptor.forClass(SemanticSearchQuery.class);
verify(spiedMessagesApi).v4MessageSearchSemanticPost(eq(0), eq(10), eq(TOKEN), eq(TOKEN),
queryCaptor.capture());
assertEquals(1784764800000L, queryCaptor.getValue().getFromDate());
assertNull(queryCaptor.getValue().getToDate());
}

@Test
void testSearchMessagesSemanticWithoutDateRangeSendsNoDates() throws Exception {
MessagesApi spiedMessagesApi = spy(new MessagesApi(mockApiClient.getApiClient("/agent")));
MessageService service = new MessageService(spiedMessagesApi, messageApi, messageSuppressionApi, streamsApi,
podApi, attachmentsApi, defaultApi, authSession, templateEngine, new RetryWithRecoveryBuilder<>());
mockApiClient.onPost(V4_SEARCH_MESSAGES_SEMANTIC,
JsonHelper.readFromClasspath("/message/get_message_stream_id.json"));

service.searchMessagesSemantic("query", STREAM_ID, null);

ArgumentCaptor<SemanticSearchQuery> queryCaptor = ArgumentCaptor.forClass(SemanticSearchQuery.class);
verify(spiedMessagesApi).v4MessageSearchSemanticPost(isNull(), isNull(), eq(TOKEN), eq(TOKEN),
queryCaptor.capture());
assertNull(queryCaptor.getValue().getFromDate());
assertNull(queryCaptor.getValue().getToDate());
}

@Test
void testSearchMessagesSemanticWithInvalidDateRangeThrows() {
final Instant date = Instant.parse("2026-07-23T00:00:00Z");

assertThrows(IllegalArgumentException.class,
() -> messageService.searchMessagesSemantic("query", null, date, date, null));
assertThrows(IllegalArgumentException.class,
() -> messageService.searchMessagesSemantic("query", null, date.plusSeconds(1), date, null));
}

@Test
void testSearchMessagesSemanticWithStreamDelegatesWithoutDateRange() {
MessageService service = spy(messageService);
doReturn(Collections.emptyList()).when(service)
.searchMessagesSemantic(anyString(), anyString(), isNull(), isNull(), isNull());

assertNotNull(service.searchMessagesSemantic("query", STREAM_ID, null));
verify(service).searchMessagesSemantic("query", STREAM_ID, null, null, null);
}

@Test
void testSearchMessagesSemanticSimpleDelegates() {
MessageService service = spy(messageService);
Expand Down Expand Up @@ -956,6 +1034,59 @@ void shouldDelegateSearchMessagesSemanticToOverrideWhenRegistered() throws Excep
verify(override).searchMessagesSemantic(eq(authSession), eq("query"), isNull(), isNull());
}

@Test
void shouldDelegateSearchMessagesSemanticWithDateRangeToOverrideWhenRegistered() throws Exception {
com.symphony.bdk.core.extension.MessageRetrieverOverride override =
mock(com.symphony.bdk.core.extension.MessageRetrieverOverride.class);
final Instant from = Instant.parse("2026-07-23T00:00:00Z");
final Instant to = Instant.parse("2026-07-26T00:00:00Z");
V4Message expected = new V4Message().messageId("override-semantic-msg");
when(override.searchMessagesSemantic(eq(authSession), eq("query"), isNull(), eq(from), eq(to), isNull()))
.thenReturn(Collections.singletonList(expected));

MessageService serviceWithOverride = new MessageService(messagesApi, messageApi, messageSuppressionApi, streamsApi,
podApi, attachmentsApi, defaultApi, authSession, templateEngine, new RetryWithRecoveryBuilder<>(), null,
override);

List<V4Message> result = serviceWithOverride.searchMessagesSemantic("query", null, from, to, null);

assertEquals(Collections.singletonList("override-semantic-msg"),
result.stream().map(V4Message::getMessageId).collect(Collectors.toList()));
verify(override).searchMessagesSemantic(eq(authSession), eq("query"), isNull(), eq(from), eq(to), isNull());
}

@Test
void overrideWithoutDateRangeSupport_rejectsSemanticSearchWithDateRange() {
com.symphony.bdk.core.extension.MessageRetrieverOverride override =
new com.symphony.bdk.core.extension.MessageRetrieverOverride() {
@Override
public List<V4Message> listMessages(AuthSession session, String streamId, Instant since, Instant until,
PaginationAttribute pagination) {
return Collections.emptyList();
}

@Override
public List<V4Message> searchMessages(AuthSession session, MessageSearchQuery query,
PaginationAttribute pagination, com.symphony.bdk.core.service.message.model.SortDir sortDir) {
return Collections.emptyList();
}

@Override
public List<V4Message> searchMessagesSemantic(AuthSession session, String query, String streamId,
PaginationAttribute pagination) {
return Collections.emptyList();
}

@Override
public V4Message getMessage(AuthSession session, String messageId) {
return null;
}
};

assertThrows(UnsupportedOperationException.class, () -> override.searchMessagesSemantic(authSession, "query",
null, Instant.parse("2026-07-23T00:00:00Z"), null, null));
}

@Test
void shouldDelegateGetMessageToOverrideWhenRegistered() throws Exception {
com.symphony.bdk.core.extension.MessageRetrieverOverride override =
Expand Down
Loading