diff --git a/src/prerna/engine/impl/model/inferencetracking/ModelInferenceLogsUtils.java b/src/prerna/engine/impl/model/inferencetracking/ModelInferenceLogsUtils.java index 108398df62..75618c2af9 100644 --- a/src/prerna/engine/impl/model/inferencetracking/ModelInferenceLogsUtils.java +++ b/src/prerna/engine/impl/model/inferencetracking/ModelInferenceLogsUtils.java @@ -1495,42 +1495,61 @@ public static boolean doSetRoomToPinned(String userId, String roomId, boolean pi /** * Searches messages for a user and project by keyword. Handles message_data as * a binary field (bytea/blob/varbinary). Converts/casts as necessary for each - * DB so text search via LIKE is possible. + * DB so text search via LIKE is possible. Results are grouped to one row per + * room in SQL before limit/offset are applied, so pagination operates on rooms + * rather than raw message rows. * * @param userId the user to search for - * @param projectId the project to search within + * @param projectId the project to search within, or null/blank to search all + * projects for the user * @param keyword the text keyword to find in message bodies - * @return a list of matching messages (room_id, message_text, message_id) + * @return a list of matching rooms, one row per room (room_id, room_name, and + * date_created from that room's most recent matching message) */ public static List> searchMessages(String userId, String projectId, String keyword) { + return searchMessages(userId, projectId, keyword, -1, 0, false, false); + } + + public static List> searchMessages(String userId, String projectId, String keyword, + long limit, long offset, boolean includeUnnamedRooms, boolean includeChildRooms) { IRDBMSEngine modelInferenceLogsDb = SystemEngineRegistry.getModelInferenceLogsDb(); SelectQueryStruct qs = new SelectQueryStruct(); - // Always select room_id and message_id qs.addSelector(new QueryColumnSelector("ROOM__ROOM_ID", "room_id")); - qs.addSelector(new QueryColumnSelector("MESSAGE__MESSAGE_ID", "message_id")); + qs.addSelector(new QueryColumnSelector("ROOM__ROOM_NAME", "room_name")); + qs.addSelector(new QueryColumnSelector("ROOM__DATE_CREATED", "date_created")); - // Build a selector for message_text out of message_data, adapted to DB type + // Use the search-specific conversion so malformed searchable content cannot + // abort an otherwise unrelated room/project search. QueryFunctionSelector messageTextSelector = modelInferenceLogsDb.getQueryUtil() .getBlobToStringFunctionSelector(new QueryColumnSelector("MESSAGE__MESSAGE_DATA"), "message_text"); - qs.addSelector(messageTextSelector); - // JOIN, filters, and ordering - qs.addRelation("MESSAGE__ROOM_ID", "ROOM__ROOM_ID", "left.join"); + // JOIN, filters, grouping, and ordering + qs.addRelation("MESSAGE__ROOM_ID", "ROOM__ROOM_ID", "inner.join"); qs.addExplicitFilter( SimpleQueryFilter.makeColToValFilter("ROOM__IS_ACTIVE", "==", true, PixelDataType.BOOLEAN)); - qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__PROJECT_ID", "==", projectId)); + if (projectId != null && !projectId.trim().isEmpty()) { + qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__PROJECT_ID", "==", projectId)); + } qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__USER_ID", "==", userId)); + if (!includeUnnamedRooms) { + qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__ROOM_NAME", "!=", null)); + qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__ROOM_NAME", "!=", "")); + } + if (!includeChildRooms) { + qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__PARENT_ROOM_ID", "==", null)); + } + qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter(messageTextSelector, + "?like", keyword, PixelDataType.CONST_STRING)); - // Add filter on decoded message text - qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter(messageTextSelector, // use the computed selector (the - // decoded/casted field) - "?like", keyword.toLowerCase(), // (may want '?ilike' if framework supports, for case-insensitive) - PixelDataType.CONST_STRING)); - - qs.addOrderBy("ROOM__DATE_CREATED", "DESC"); - qs.addOrderBy("MESSAGE__DATE_CREATED", "DESC"); - + qs.addOrderBy(new QueryColumnOrderBySelector("date_created", "DESC")); + qs.addOrderBy(new QueryColumnOrderBySelector("room_id", "DESC")); + if (limit > 0) { + qs.setLimit(limit); + } + if (offset > 0) { + qs.setOffSet(offset); + } return QueryExecutionUtility.flushRsToMap(modelInferenceLogsDb, qs); } @@ -1788,11 +1807,18 @@ public static List> doVerifyConversation(String userId, Stri */ public static List> getUserConversations(String userId, String projectId, long limit, long offset, String sortDir, String search, Boolean pinned) { - return getUserConversations(userId, projectId, limit, offset, sortDir, search, pinned, null); + return getUserConversations(userId, projectId, limit, offset, sortDir, search, pinned, null, false, false); } public static List> getUserConversations(String userId, String projectId, long limit, long offset, String sortDir, String search, Boolean pinned, String roomOptionsSearch) { + return getUserConversations(userId, projectId, limit, offset, sortDir, search, pinned, roomOptionsSearch, + false, false); + } + + public static List> getUserConversations(String userId, String projectId, long limit, + long offset, String sortDir, String search, Boolean pinned, String roomOptionsSearch, + boolean includeUnnamedRooms, boolean includeChildRooms) { IRDBMSEngine modelInferenceLogsDb = SystemEngineRegistry.getModelInferenceLogsDb(); SelectQueryStruct qs = new SelectQueryStruct(); qs.addSelector(new QueryColumnSelector("ROOM__ROOM_ID")); @@ -1812,6 +1838,13 @@ public static List> getUserConversations(String userId, Stri if (projectId != null) { subQs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__PROJECT_ID", "==", projectId)); } + if (!includeUnnamedRooms) { + subQs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__ROOM_NAME", "!=", null)); + subQs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__ROOM_NAME", "!=", "")); + } + if (!includeChildRooms) { + subQs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__PARENT_ROOM_ID", "==", null)); + } qs.addExplicitFilter(SimpleQueryFilter.makeColToSubQuery("ROOM__ROOM_ID", "==", subQs)); // SEARCH diff --git a/src/prerna/engine/impl/model/inferencetracking/reactors/GetUserConversationRoomsReactor.java b/src/prerna/engine/impl/model/inferencetracking/reactors/GetUserConversationRoomsReactor.java index 7eee081f40..e5f2e8e9a6 100644 --- a/src/prerna/engine/impl/model/inferencetracking/reactors/GetUserConversationRoomsReactor.java +++ b/src/prerna/engine/impl/model/inferencetracking/reactors/GetUserConversationRoomsReactor.java @@ -37,13 +37,15 @@ import prerna.sablecc2.om.ReactorKeysEnum; import prerna.sablecc2.om.nounmeta.NounMetadata; -public class GetUserConversationRoomsReactor extends AbstractReactor { - - public GetUserConversationRoomsReactor() { - this.keysToGet = new String[] { ReactorKeysEnum.PROJECT.getKey(), ReactorKeysEnum.LIMIT.getKey(), - ReactorKeysEnum.OFFSET.getKey(), ReactorKeysEnum.SEARCH.getKey(), ReactorKeysEnum.SORT.getKey(), - ReactorKeysEnum.PINNED.getKey(), "roomOptionsSearch" }; - this.keyRequired = new int[] { 0, 0, 0, 0, 0, 0, 0 }; +public class GetUserConversationRoomsReactor extends AbstractReactor { + private static final String INCLUDE_UNNAMED_ROOMS = "includeUnnamedRooms"; + private static final String INCLUDE_CHILD_ROOMS = "includeChildRooms"; + + public GetUserConversationRoomsReactor() { + this.keysToGet = new String[] { ReactorKeysEnum.PROJECT.getKey(), ReactorKeysEnum.LIMIT.getKey(), + ReactorKeysEnum.OFFSET.getKey(), ReactorKeysEnum.SEARCH.getKey(), ReactorKeysEnum.SORT.getKey(), + ReactorKeysEnum.PINNED.getKey(), "roomOptionsSearch", INCLUDE_UNNAMED_ROOMS, INCLUDE_CHILD_ROOMS }; + this.keyRequired = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0 }; } @Override @@ -53,10 +55,13 @@ public NounMetadata execute() { if (user == null) { throw new IllegalArgumentException("You are not properly logged in"); } - String projectId = this.keyValue.get(this.keysToGet[0]); - if (projectId == null) { - projectId = this.insight.getContextProjectId(); - } + String projectId = this.keyValue.get(this.keysToGet[0]); + if (projectId == null) { + projectId = this.insight.getContextProjectId(); + } + if (projectId == null) { + projectId = this.insight.getProjectId(); + } long limit = getLong(ReactorKeysEnum.LIMIT.getKey(), -1L); long offset = getLong(ReactorKeysEnum.OFFSET.getKey(), -1L); @@ -68,7 +73,11 @@ public NounMetadata execute() { sortDir = "DESC"; } - String search = this.keyValue.get(ReactorKeysEnum.SEARCH.getKey()); + String search = this.keyValue.get(ReactorKeysEnum.SEARCH.getKey()); + if (search != null && !search.trim().isEmpty() + && (projectId == null || projectId.trim().isEmpty())) { + throw new IllegalArgumentException("A project must be provided or available from the current insight"); + } // Optional pinned filter: true/false to filter, null/absent to ignore Boolean pinned = null; @@ -79,17 +88,19 @@ public NounMetadata execute() { // Optional free-text search against the OPTIONS JSON column. String roomOptionsSearch = this.keyValue.get("roomOptionsSearch"); - if (roomOptionsSearch != null) { + if (roomOptionsSearch != null) { roomOptionsSearch = roomOptionsSearch.trim(); if (roomOptionsSearch.isEmpty()) { roomOptionsSearch = null; } - } - - // Call new overload of getUserConversations - List> output = ModelInferenceLogsUtils.getUserConversations( - user.getPrimaryLoginToken().getId(), projectId, limit, offset, sortDir, search, pinned, - roomOptionsSearch); + } + boolean includeUnnamedRooms = getBoolean(INCLUDE_UNNAMED_ROOMS, false); + boolean includeChildRooms = getBoolean(INCLUDE_CHILD_ROOMS, false); + + // Call new overload of getUserConversations + List> output = ModelInferenceLogsUtils.getUserConversations( + user.getPrimaryLoginToken().getId(), projectId, limit, offset, sortDir, search, pinned, + roomOptionsSearch, includeUnnamedRooms, includeChildRooms); return new NounMetadata(output, PixelDataType.VECTOR); } @@ -113,8 +124,12 @@ protected String getDescriptionForKey(String key) { return "Sort direction by room creation date. Accepts ASC or DESC (default is DESC)."; } else if (ReactorKeysEnum.PINNED.getKey().equals(key)) { return "Optional pinned filter: true for pinned rooms only, false for unpinned rooms only, omit for no pinned filter."; - } else if ("roomOptionsSearch".equals(key)) { - return "Optional free-text search term applied against the room's options JSON. Any room whose options contain this substring is included."; + } else if ("roomOptionsSearch".equals(key)) { + return "Optional free-text search term applied against the room's options JSON. Any room whose options contain this substring is included."; + } else if (INCLUDE_UNNAMED_ROOMS.equals(key)) { + return "Whether to include rooms with a null or empty name. Defaults to false."; + } else if (INCLUDE_CHILD_ROOMS.equals(key)) { + return "Whether to include rooms that have a parent room. Defaults to false."; } return super.getDescriptionForKey(key); } diff --git a/src/prerna/engine/impl/model/inferencetracking/reactors/SearchRoomMessagesReactor.java b/src/prerna/engine/impl/model/inferencetracking/reactors/SearchRoomMessagesReactor.java index de7db84f1e..96a5fbc79e 100644 --- a/src/prerna/engine/impl/model/inferencetracking/reactors/SearchRoomMessagesReactor.java +++ b/src/prerna/engine/impl/model/inferencetracking/reactors/SearchRoomMessagesReactor.java @@ -43,61 +43,107 @@ public class SearchRoomMessagesReactor extends AbstractReactor { private static final Logger classLogger = LogManager.getLogger(SearchRoomMessagesReactor.class); + private static final String INCLUDE_UNNAMED_ROOMS = "includeUnnamedRooms"; + private static final String INCLUDE_CHILD_ROOMS = "includeChildRooms"; + public SearchRoomMessagesReactor() { - // this expects projectId and search term - this.keysToGet = new String[] { ReactorKeysEnum.PROJECT.getKey(), ReactorKeysEnum.SEARCH.getKey(), }; - this.keyRequired = new int[] { 0, 1 }; + this.keysToGet = new String[] { + ReactorKeysEnum.PROJECT.getKey(), + ReactorKeysEnum.SEARCH.getKey(), + ReactorKeysEnum.LIMIT.getKey(), + ReactorKeysEnum.OFFSET.getKey(), + INCLUDE_UNNAMED_ROOMS, + INCLUDE_CHILD_ROOMS + }; + this.keyRequired = new int[] { 0, 1, 0, 0, 0, 0 }; } @Override public NounMetadata execute() { organizeKeys(); - // Get user User user = this.insight.getUser(); if (user == null) { throw new IllegalArgumentException("You are not properly logged in"); } String userId = user.getPrimaryLoginToken().getId(); - // Get projectId String projectId = this.keyValue.get(this.keysToGet[0]); - if (projectId == null) { + if (projectId == null || projectId.trim().isEmpty()) { projectId = this.insight.getContextProjectId(); } + if (projectId == null || projectId.trim().isEmpty()) { + projectId = this.insight.getProjectId(); + } + if (projectId == null || projectId.trim().isEmpty()) { + projectId = null; + } - // Get keyword String keyword = this.keyValue.get(this.keysToGet[1]); if (keyword == null || keyword.trim().isEmpty()) { throw new IllegalArgumentException("Search keyword must be provided"); } - // Query messages + Long requestedLimit; + Long requestedOffset; + try { + requestedLimit = getLong(ReactorKeysEnum.LIMIT.getKey()); + requestedOffset = getLong(ReactorKeysEnum.OFFSET.getKey()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Limit and offset must be whole numbers", e); + } + if (requestedLimit != null && requestedLimit <= 0) { + throw new IllegalArgumentException("Limit must be greater than zero"); + } + if (requestedOffset != null && requestedOffset < 0) { + throw new IllegalArgumentException("Offset must be zero or greater"); + } + if (requestedOffset != null && requestedOffset > 0 && requestedLimit == null) { + throw new IllegalArgumentException("A positive limit is required when offset is provided"); + } + long limit = requestedLimit == null ? -1L : requestedLimit; + long offset = requestedOffset == null ? 0L : requestedOffset; + boolean includeUnnamedRooms = getBoolean(INCLUDE_UNNAMED_ROOMS, false); + boolean includeChildRooms = getBoolean(INCLUDE_CHILD_ROOMS, false); + List> results; try { - results = ModelInferenceLogsUtils.searchMessages(userId, projectId, keyword); + results = ModelInferenceLogsUtils.searchMessages(userId, projectId, keyword, limit, offset, + includeUnnamedRooms, includeChildRooms); } catch (Exception e) { classLogger.error("Error searching room messages", e); throw new RuntimeException("Could not search room messages: " + e.getMessage(), e); } - // Return results as VECTOR return new NounMetadata(results, PixelDataType.VECTOR); } @Override public String getReactorDescription() { - return "This reactor searches through the messages in the user's conversation rooms for a given keyword within a specified project. " - + "It returns the matching messages, including relevant details such as room ID, message text, and message ID, for rooms the user has access to."; + return "Searches through the messages in the user's conversation rooms for a given keyword. " + + "Returns one row per matching room: room_id, room_name, and the latest matching date_created. " + + "Case-insensitive matching is handled by the query framework's ?like comparator. " + + "Unnamed and child rooms are excluded unless explicitly included. " + + "Falls back to the current insight's context/project when projectId is omitted; " + + "if none is available, searches all projects for the user. " + + "Supports limit and offset for pagination over matching rooms."; } @Override protected String getDescriptionForKey(String key) { if (key.equals(ReactorKeysEnum.PROJECT.getKey())) { - return "The project ID for which to search room messages. If no project ID is passed, then all rooms for the user will be searched."; + return "Optional project ID to scope the search. Falls back to the current insight's project, then searches all projects for the user if no project is available."; } else if (key.equals(ReactorKeysEnum.SEARCH.getKey())) { - return "The search term to use to search for within the messages. All messages containing this text (case-insensitive) will be returned."; + return "The keyword to search for within message content (case-insensitive)."; + } else if (key.equals(ReactorKeysEnum.LIMIT.getKey())) { + return "Maximum number of results to return. Defaults to no cap when omitted."; + } else if (key.equals(ReactorKeysEnum.OFFSET.getKey())) { + return "Number of results to skip for pagination."; + } else if (key.equals(INCLUDE_UNNAMED_ROOMS)) { + return "Whether to include rooms with a null or empty name. Defaults to false."; + } else if (key.equals(INCLUDE_CHILD_ROOMS)) { + return "Whether to include rooms that have a parent room. Defaults to false."; } return super.getDescriptionForKey(key); } -} \ No newline at end of file +}