forked from bernardladenthin/java-llama.cpp
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathChatMessage.java
More file actions
218 lines (197 loc) · 8.18 KB
/
Copy pathChatMessage.java
File metadata and controls
218 lines (197 loc) · 8.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
//
// SPDX-License-Identifier: MIT
package net.ladenthin.llama.value;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import org.jspecify.annotations.Nullable;
/**
* A single message in a chat conversation: a role ({@code "user"}, {@code "assistant"},
* {@code "system"}, or {@code "tool"}) and its textual content. Used by {@link net.ladenthin.llama.Session}
* to accumulate conversation turns and by {@link net.ladenthin.llama.parameters.ChatRequest} / {@link ChatResponse}
* for the typed chat API.
* <p>
* Tool-call turns have role {@code "assistant"}, possibly empty content, and a non-empty
* {@link #getToolCalls()} list. Tool-result turns have role {@code "tool"}, the tool's
* output as content, and {@link #getToolCallId()} pointing back at the originating call.
* </p>
* <p>
* Multimodal turns carry a non-null {@link #getParts()} list of {@link ContentPart}s
* (text and image references). When parts are present they take precedence over
* {@link #getContent()} during serialization; the upstream OAI chat path
* (see {@link net.ladenthin.llama.parameters.InferenceParameters#withMessages(java.util.List)}) emits an array-form
* {@code content} field that the compiled-in {@code mtmd} pipeline understands.
* </p>
*
* <p>{@code equals}/{@code hashCode} are generated by Lombok over all fields.
* {@code toString} is intentionally handwritten (not Lombok-generated) so that
* conversation traces in logs render as "{@code role: content}" or
* "{@code role (tool_calls=N): content}" instead of a verbose field dump.</p>
*/
@EqualsAndHashCode
public final class ChatMessage {
private final String role;
private final String content;
private final @Nullable String toolCallId;
private final List<ToolCall> toolCalls;
private final @Nullable List<ContentPart> parts;
/**
* Plain user/assistant/system message.
*
* @param role the message role
* @param content the message text
*/
public ChatMessage(String role, String content) {
this(role, content, null, Collections.<ToolCall>emptyList(), null);
}
/**
* Full constructor including tool-related fields.
*
* @param role the message role
* @param content the message text (may be empty for assistant tool-call turns)
* @param toolCallId for tool-result turns ({@code role="tool"}), the id of the originating call; {@code null} otherwise
* @param toolCalls for assistant tool-call turns, the list of calls; empty otherwise
*/
public ChatMessage(String role, String content, @Nullable String toolCallId, List<ToolCall> toolCalls) {
this(role, content, toolCallId, toolCalls, null);
}
/**
* Multimodal constructor: build a message whose content is a list of
* {@link ContentPart}s (text and/or image references). The {@link #getContent()}
* accessor returns the concatenation of the text parts for legacy callers that
* cannot consume the array form.
*
* @param role the message role
* @param parts ordered list of content parts (must not be {@code null} or empty)
*/
public ChatMessage(String role, List<ContentPart> parts) {
this(
role,
concatText(requireNonNull(parts)),
null,
Collections.<ToolCall>emptyList(),
Collections.unmodifiableList(new java.util.ArrayList<ContentPart>(requireNonEmpty(parts))));
}
private static List<ContentPart> requireNonNull(List<ContentPart> parts) {
if (parts == null) {
throw new IllegalArgumentException("parts must not be null");
}
return parts;
}
private ChatMessage(
String role,
String content,
@Nullable String toolCallId,
List<ToolCall> toolCalls,
@Nullable List<ContentPart> parts) {
this.role = role;
this.content = content;
this.toolCallId = toolCallId;
this.toolCalls = toolCalls == null
? Collections.<ToolCall>emptyList()
: Collections.unmodifiableList(new java.util.ArrayList<ToolCall>(toolCalls));
this.parts = parts;
}
private static List<ContentPart> requireNonEmpty(List<ContentPart> parts) {
if (parts.isEmpty()) {
throw new IllegalArgumentException("parts must not be empty (size=" + parts.size() + ")");
}
return parts;
}
private static String concatText(Iterable<ContentPart> parts) {
if (parts == null) return "";
StringBuilder sb = new StringBuilder();
for (ContentPart p : parts) {
if (p.getType() == ContentPart.Type.TEXT) {
if (sb.length() > 0) sb.append('\n');
sb.append(p.getText());
}
}
return sb.toString();
}
/**
* Factory for a tool-result turn.
*
* @param toolCallId the id of the originating tool call
* @param content the tool's output as a string
* @return a {@link ChatMessage} with role {@code "tool"}
*/
public static ChatMessage toolResult(String toolCallId, String content) {
return new ChatMessage("tool", content, toolCallId, Collections.<ToolCall>emptyList());
}
/**
* Factory for an assistant turn that issues tool calls.
*
* @param content optional reasoning text accompanying the tool calls (may be empty)
* @param toolCalls the tool calls to issue
* @return a {@link ChatMessage} with role {@code "assistant"}
*/
public static ChatMessage assistantToolCalls(String content, List<ToolCall> toolCalls) {
return new ChatMessage("assistant", content == null ? "" : content, null, toolCalls);
}
/**
* Convenience factory for a {@code "user"} turn mixing text and one or more
* images. Equivalent to {@code new ChatMessage("user", parts)}.
*
* @param parts ordered text and image parts; at least one is required
* @return a multimodal user message
*/
public static ChatMessage userMultimodal(ContentPart... parts) {
return new ChatMessage("user", Arrays.asList(parts));
}
/**
* Message role accessor.
* @return the message role string
*/
public String getRole() {
return role;
}
/**
* Message content accessor.
* @return the message text content
*/
public String getContent() {
return content;
}
/**
* Tool-call id for tool-result turns.
* @return the originating tool call id, or {@link Optional#empty()} for non-tool messages
*/
public Optional<String> getToolCallId() {
return Optional.ofNullable(toolCallId);
}
/**
* Tool calls issued by an assistant turn.
* @return the calls list, never {@code null}; empty when the message is not a tool-call turn
*/
public List<ToolCall> getToolCalls() {
// Return a fresh unmodifiable view (mirrors getParts) so the stored list is never
// exposed directly — the caller cannot mutate this value type's internal state.
return Collections.unmodifiableList(toolCalls);
}
/**
* Multimodal content parts accessor.
* @return an unmodifiable list of text and image parts, or {@link Optional#empty()}
* for legacy text-only messages built via {@link #ChatMessage(String, String)}
*/
public Optional<List<ContentPart>> getParts() {
return parts == null ? Optional.empty() : Optional.of(Collections.unmodifiableList(parts));
}
/**
* Whether this message carries multimodal parts (i.e. was constructed via
* {@link #ChatMessage(String, List)} or {@link #userMultimodal(ContentPart...)}).
* @return {@code true} when {@link #getParts()} is non-empty
*/
public boolean hasParts() {
return parts != null;
}
@Override
public String toString() {
if (!toolCalls.isEmpty()) return role + " (tool_calls=" + toolCalls.size() + "): " + content;
if (toolCallId != null) return role + " (tool_call_id=" + toolCallId + "): " + content;
return role + ": " + content;
}
}