Skip to content

Commit e4d6c7d

Browse files
committed
feat(messagehistory): implement MessageHistory feature
Implemented core message history functionality: Classes added: - Constants: Field name constants for message history - ChatMessage: Model for individual chat messages with role, content, session tag - MessageHistorySchema: Schema builder for message history index - BaseMessageHistory: Abstract base class with common functionality - MessageHistory: Concrete implementation for storing/retrieving messages - Utils: Utility methods including currentTimestamp()
1 parent 74852a7 commit e4d6c7d

File tree

14 files changed

+1675
-264
lines changed

14 files changed

+1675
-264
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.redis.vl.extensions;
2+
3+
/** Constants used across RedisVL extensions. */
4+
public final class Constants {
5+
6+
// Message History Field Names
7+
public static final String ID_FIELD_NAME = "entry_id";
8+
public static final String ROLE_FIELD_NAME = "role";
9+
public static final String CONTENT_FIELD_NAME = "content";
10+
public static final String TOOL_FIELD_NAME = "tool_call_id";
11+
public static final String TIMESTAMP_FIELD_NAME = "timestamp";
12+
public static final String SESSION_FIELD_NAME = "session_tag";
13+
public static final String MESSAGE_VECTOR_FIELD_NAME = "vector_field";
14+
15+
// Cache Field Names
16+
public static final String REDIS_KEY_FIELD_NAME = "key";
17+
public static final String ENTRY_ID_FIELD_NAME = "entry_id";
18+
public static final String PROMPT_FIELD_NAME = "prompt";
19+
public static final String RESPONSE_FIELD_NAME = "response";
20+
public static final String CACHE_VECTOR_FIELD_NAME = "prompt_vector";
21+
public static final String INSERTED_AT_FIELD_NAME = "inserted_at";
22+
public static final String UPDATED_AT_FIELD_NAME = "updated_at";
23+
public static final String METADATA_FIELD_NAME = "metadata";
24+
25+
// Router Field Names
26+
public static final String TEXT_FIELD_NAME = "text";
27+
public static final String MODEL_NAME_FIELD_NAME = "model_name";
28+
public static final String EMBEDDING_FIELD_NAME = "embedding";
29+
public static final String DIMENSIONS_FIELD_NAME = "dimensions";
30+
public static final String ROUTE_VECTOR_FIELD_NAME = "vector";
31+
32+
private Constants() {
33+
// Prevent instantiation
34+
}
35+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package com.redis.vl.extensions.messagehistory;
2+
3+
import static com.redis.vl.extensions.Constants.*;
4+
5+
import com.github.f4b6a3.ulid.UlidCreator;
6+
import java.util.ArrayList;
7+
import java.util.HashMap;
8+
import java.util.List;
9+
import java.util.Map;
10+
11+
/**
12+
* Base class for message history implementations.
13+
*
14+
* <p>Matches the Python BaseMessageHistory from redisvl.extensions.message_history.base_history
15+
*/
16+
public abstract class BaseMessageHistory {
17+
18+
protected final String name;
19+
protected final String sessionTag;
20+
21+
/**
22+
* Initialize message history.
23+
*
24+
* @param name The name of the message history index
25+
* @param sessionTag Tag to be added to entries to link to a specific conversation session.
26+
* Defaults to instance ULID.
27+
*/
28+
protected BaseMessageHistory(String name, String sessionTag) {
29+
this.name = name;
30+
this.sessionTag = (sessionTag != null) ? sessionTag : UlidCreator.getUlid().toString();
31+
}
32+
33+
/** Clears the chat message history. */
34+
public abstract void clear();
35+
36+
/** Clear all conversation history and remove any search indices. */
37+
public abstract void delete();
38+
39+
/**
40+
* Remove a specific exchange from the conversation history.
41+
*
42+
* @param id The id of the entry to delete. If null then the last entry is deleted.
43+
*/
44+
public abstract void drop(String id);
45+
46+
/** Returns the full chat history. */
47+
public abstract List<Map<String, Object>> getMessages();
48+
49+
/**
50+
* Retrieve the recent conversation history in sequential order.
51+
*
52+
* @param topK The number of previous messages to return. Default is 5.
53+
* @param asText Whether to return the conversation as a list of content strings, or list of
54+
* message maps.
55+
* @param raw Whether to return the full Redis hash entry or just the role/content/tool_call_id.
56+
* @param sessionTag Tag of the entries linked to a specific conversation session. Defaults to
57+
* instance ULID.
58+
* @return List of messages (either as text strings or maps depending on asText parameter)
59+
* @throws IllegalArgumentException if topK is not an integer greater than or equal to 0
60+
*/
61+
public abstract <T> List<T> getRecent(int topK, boolean asText, boolean raw, String sessionTag);
62+
63+
/**
64+
* Insert a prompt:response pair into the message history.
65+
*
66+
* @param prompt The user prompt to the LLM
67+
* @param response The corresponding LLM response
68+
* @param sessionTag The tag to mark the messages with. Defaults to instance session tag.
69+
*/
70+
public abstract void store(String prompt, String response, String sessionTag);
71+
72+
/**
73+
* Insert a list of prompts and responses into the message history.
74+
*
75+
* @param messages The list of user prompts and LLM responses
76+
* @param sessionTag The tag to mark the messages with. Defaults to instance session tag.
77+
*/
78+
public abstract void addMessages(List<Map<String, String>> messages, String sessionTag);
79+
80+
/**
81+
* Insert a single prompt or response into the message history.
82+
*
83+
* @param message The user prompt or LLM response
84+
* @param sessionTag The tag to mark the message with. Defaults to instance session tag.
85+
*/
86+
public abstract void addMessage(Map<String, String> message, String sessionTag);
87+
88+
/**
89+
* Formats messages from Redis into either text strings or structured maps.
90+
*
91+
* @param messages The messages from the message history index
92+
* @param asText Whether to return as text strings or maps
93+
* @return Formatted messages
94+
*/
95+
@SuppressWarnings("unchecked")
96+
protected <T> List<T> formatContext(List<Map<String, Object>> messages, boolean asText) {
97+
List<T> context = new ArrayList<>();
98+
99+
for (Map<String, Object> message : messages) {
100+
ChatMessage chatMessage = ChatMessage.fromDict(message);
101+
102+
if (asText) {
103+
context.add((T) chatMessage.getContent());
104+
} else {
105+
Map<String, Object> chatMessageDict = new HashMap<>();
106+
chatMessageDict.put(ROLE_FIELD_NAME, chatMessage.getRole());
107+
chatMessageDict.put(CONTENT_FIELD_NAME, chatMessage.getContent());
108+
109+
if (chatMessage.getToolCallId() != null) {
110+
chatMessageDict.put(TOOL_FIELD_NAME, chatMessage.getToolCallId());
111+
}
112+
113+
context.add((T) chatMessageDict);
114+
}
115+
}
116+
117+
return context;
118+
}
119+
120+
public String getName() {
121+
return name;
122+
}
123+
124+
public String getSessionTag() {
125+
return sessionTag;
126+
}
127+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package com.redis.vl.extensions.messagehistory;
2+
3+
import static com.redis.vl.extensions.Constants.*;
4+
5+
import com.redis.vl.utils.Utils;
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
import lombok.AllArgsConstructor;
9+
import lombok.Builder;
10+
import lombok.Data;
11+
import lombok.NoArgsConstructor;
12+
13+
/**
14+
* A single chat message exchanged between a user and an LLM.
15+
*
16+
* <p>Matches the Python ChatMessage model from redisvl.extensions.message_history.schema
17+
*/
18+
@Data
19+
@Builder
20+
@NoArgsConstructor
21+
@AllArgsConstructor
22+
public class ChatMessage {
23+
24+
/** A unique identifier for the message. Generated from session_tag and timestamp. */
25+
private String entryId;
26+
27+
/** The role of the message sender (e.g., 'user', 'llm', 'system', 'tool'). */
28+
private String role;
29+
30+
/** The content of the message. */
31+
private String content;
32+
33+
/** Tag associated with the current conversation session. */
34+
private String sessionTag;
35+
36+
/** The time the message was sent, in UTC, rounded to milliseconds. */
37+
private Double timestamp;
38+
39+
/** An optional identifier for a tool call associated with the message. */
40+
private String toolCallId;
41+
42+
/**
43+
* Converts the ChatMessage to a Map suitable for storing in Redis.
44+
*
45+
* @return Map representation of the message
46+
*/
47+
public Map<String, Object> toDict() {
48+
// Generate timestamp if not set
49+
if (timestamp == null) {
50+
timestamp = Utils.currentTimestamp();
51+
}
52+
53+
// Generate entry_id if not set
54+
if (entryId == null) {
55+
entryId = sessionTag + ":" + timestamp;
56+
}
57+
58+
Map<String, Object> data = new HashMap<>();
59+
data.put(ID_FIELD_NAME, entryId);
60+
data.put(ROLE_FIELD_NAME, role);
61+
data.put(CONTENT_FIELD_NAME, content);
62+
data.put(SESSION_FIELD_NAME, sessionTag);
63+
data.put(TIMESTAMP_FIELD_NAME, timestamp);
64+
65+
// Only include tool_call_id if present
66+
if (toolCallId != null) {
67+
data.put(TOOL_FIELD_NAME, toolCallId);
68+
}
69+
70+
return data;
71+
}
72+
73+
/**
74+
* Creates a ChatMessage from a Map (typically from Redis).
75+
*
76+
* @param data Map containing message fields
77+
* @return ChatMessage instance
78+
*/
79+
public static ChatMessage fromDict(Map<String, Object> data) {
80+
return ChatMessage.builder()
81+
.entryId((String) data.get(ID_FIELD_NAME))
82+
.role((String) data.get(ROLE_FIELD_NAME))
83+
.content((String) data.get(CONTENT_FIELD_NAME))
84+
.sessionTag((String) data.get(SESSION_FIELD_NAME))
85+
.timestamp(convertToDouble(data.get(TIMESTAMP_FIELD_NAME)))
86+
.toolCallId((String) data.get(TOOL_FIELD_NAME))
87+
.build();
88+
}
89+
90+
private static Double convertToDouble(Object value) {
91+
if (value == null) return null;
92+
if (value instanceof Double) return (Double) value;
93+
if (value instanceof Number) return ((Number) value).doubleValue();
94+
if (value instanceof String) return Double.parseDouble((String) value);
95+
return null;
96+
}
97+
}

0 commit comments

Comments
 (0)