001/* 002 * Copyright (c) 2023-2026, Agents-Flex (fuhai999@gmail.com). 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package com.agentsflex.core.message; 017 018import com.agentsflex.core.util.StringUtil; 019 020import java.util.*; 021 022public class AiMessage extends AbstractTextMessage<AiMessage> { 023 024 private Integer index; 025 private Integer promptTokens; 026 private Integer completionTokens; 027 private Integer totalTokens; 028 private Integer localPromptTokens; 029 private Integer localCompletionTokens; 030 private Integer localTotalTokens; 031 private String reasoningContent; 032 private List<ToolCall> toolCalls; 033 034 private String fullContent; 035 private String fullReasoningContent; 036 037 /** 038 * LLM 响应结束的原因(如 "stop", "length", "tool_calls" 等), 039 * 符合 OpenAI 等主流 API 的 finish_reason 语义。 040 */ 041 private String finishReason; 042 043 // 同 reasoningContent,只是某些框架会返回这个字段,而不是 finishReason 044 private String stopReason; 045 046 private Boolean finished; 047 048 public AiMessage() { 049 super(); 050 } 051 052 public AiMessage(String content) { 053 this.fullContent = content; 054 } 055 056 public void merge(AiMessage delta) { 057 if (delta.content != null) { 058 if (this.content == null) this.content = ""; 059 this.content += delta.content; 060 this.fullContent = this.content; 061 } 062 063 if (delta.reasoningContent != null) { 064 if (this.reasoningContent == null) this.reasoningContent = ""; 065 this.reasoningContent += delta.reasoningContent; 066 this.fullReasoningContent = this.reasoningContent; 067 } 068 069 if (delta.toolCalls != null && !delta.toolCalls.isEmpty()) { 070 if (this.toolCalls == null) this.toolCalls = new ArrayList<>(); 071 mergeToolCalls(delta.toolCalls); 072 } 073 if (delta.index != null) this.index = delta.index; 074 if (delta.promptTokens != null) this.promptTokens = delta.promptTokens; 075 if (delta.completionTokens != null) this.completionTokens = delta.completionTokens; 076 if (delta.totalTokens != null) this.totalTokens = delta.totalTokens; 077 if (delta.localPromptTokens != null) this.localPromptTokens = delta.localPromptTokens; 078 if (delta.localCompletionTokens != null) this.localCompletionTokens = delta.localCompletionTokens; 079 if (delta.localTotalTokens != null) this.localTotalTokens = delta.localTotalTokens; 080 if (delta.finishReason != null) this.finishReason = delta.finishReason; 081 if (delta.stopReason != null) this.stopReason = delta.stopReason; 082 } 083 084 private void mergeToolCalls(List<ToolCall> deltaCalls) { 085 if (deltaCalls == null || deltaCalls.isEmpty()) return; 086 087 if (this.toolCalls == null || this.toolCalls.isEmpty()) { 088 this.toolCalls = new ArrayList<>(deltaCalls); 089 return; 090 } 091 092 ToolCall lastCall = this.toolCalls.get(this.toolCalls.size() - 1); 093 094 // 正常情况下 delta 部分只有 1 条 095 ToolCall deltaCall = deltaCalls.get(0); 096 097 // 新增 098 if (isNewCall(deltaCall, lastCall)) { 099 this.toolCalls.add(deltaCall); 100 } 101 // 合并 102 else { 103 mergeSingleCall(lastCall, deltaCall); 104 } 105 } 106 107 private boolean isNewCall(ToolCall deltaCall, ToolCall lastCall) { 108 if (StringUtil.noText(deltaCall.getId()) && StringUtil.noText(deltaCall.getName())) { 109 return false; 110 } 111 112 if (StringUtil.hasText(deltaCall.getId())) { 113 return !deltaCall.getId().equals(lastCall.getId()); 114 } 115 116 if (StringUtil.hasText(deltaCall.getName())) { 117 return !deltaCall.getName().equals(lastCall.getName()); 118 } 119 120 return false; 121 } 122 123 private void mergeSingleCall(ToolCall existing, ToolCall delta) { 124 if (delta.getArguments() != null) { 125 if (existing.getArguments() == null) { 126 existing.setArguments(""); 127 } 128 existing.setArguments(existing.getArguments() + delta.getArguments()); 129 } 130 if (StringUtil.hasText(delta.getId())) { 131 existing.setId(delta.getId()); 132 } 133 if (StringUtil.hasText(delta.getName())) { 134 existing.setName(delta.getName()); 135 } 136 } 137 138 // ===== Getters & Setters (保持原有不变) ===== 139 public Integer getIndex() { 140 return index; 141 } 142 143 public void setIndex(Integer index) { 144 this.index = index; 145 } 146 147 public Integer getPromptTokens() { 148 return promptTokens; 149 } 150 151 public void setPromptTokens(Integer promptTokens) { 152 this.promptTokens = promptTokens; 153 } 154 155 public Integer getCompletionTokens() { 156 return completionTokens; 157 } 158 159 public void setCompletionTokens(Integer completionTokens) { 160 this.completionTokens = completionTokens; 161 } 162 163 public Integer getTotalTokens() { 164 return totalTokens; 165 } 166 167 public void setTotalTokens(Integer totalTokens) { 168 this.totalTokens = totalTokens; 169 } 170 171 public Integer getLocalPromptTokens() { 172 return localPromptTokens; 173 } 174 175 public void setLocalPromptTokens(Integer localPromptTokens) { 176 this.localPromptTokens = localPromptTokens; 177 } 178 179 public Integer getLocalCompletionTokens() { 180 return localCompletionTokens; 181 } 182 183 public void setLocalCompletionTokens(Integer localCompletionTokens) { 184 this.localCompletionTokens = localCompletionTokens; 185 } 186 187 public Integer getLocalTotalTokens() { 188 return localTotalTokens; 189 } 190 191 public void setLocalTotalTokens(Integer localTotalTokens) { 192 this.localTotalTokens = localTotalTokens; 193 } 194 195 public String getFullContent() { 196 return fullContent; 197 } 198 199 public void setFullContent(String fullContent) { 200 this.fullContent = fullContent; 201 } 202 203 public String getReasoningContent() { 204 return reasoningContent; 205 } 206 207 public void setReasoningContent(String reasoningContent) { 208 this.reasoningContent = reasoningContent; 209 } 210 211 public String getFinishReason() { 212 return finishReason; 213 } 214 215 public void setFinishReason(String finishReason) { 216 this.finishReason = finishReason; 217 } 218 219 public String getStopReason() { 220 return stopReason; 221 } 222 223 public void setStopReason(String stopReason) { 224 this.stopReason = stopReason; 225 } 226 227 @Override 228 public String getTextContent() { 229 return fullContent; 230 } 231 232 /** 233 * 创建并返回当前对象的副本。 234 * 235 * @return 一个新的、内容相同但内存独立的对象 236 */ 237 @Override 238 public AiMessage copy() { 239 AiMessage copy = new AiMessage(); 240 // 基本字段 241 copy.content = this.content; 242 copy.fullContent = this.fullContent; 243 copy.reasoningContent = this.reasoningContent; 244 copy.fullReasoningContent = this.fullReasoningContent; 245 copy.finishReason = this.finishReason; 246 copy.stopReason = this.stopReason; 247 copy.finished = this.finished; 248 249 // Token 字段 250 copy.index = this.index; 251 copy.promptTokens = this.promptTokens; 252 copy.completionTokens = this.completionTokens; 253 copy.totalTokens = this.totalTokens; 254 copy.localPromptTokens = this.localPromptTokens; 255 copy.localCompletionTokens = this.localCompletionTokens; 256 copy.localTotalTokens = this.localTotalTokens; 257 258 // ToolCalls: 深拷贝 List 和每个 ToolCall 259 if (this.toolCalls != null) { 260 copy.toolCalls = new ArrayList<>(); 261 for (ToolCall tc : this.toolCalls) { 262 if (tc != null) { 263 copy.toolCalls.add(tc.copy()); 264 } else { 265 copy.toolCalls.add(null); 266 } 267 } 268 } 269 270 // Metadata 271 if (this.metadataMap != null) { 272 copy.metadataMap = new HashMap<>(this.metadataMap); 273 } 274 275 return copy; 276 } 277 278 public boolean hasToolCalls() { 279 return toolCalls != null && !toolCalls.isEmpty(); 280 } 281 282 public List<ToolCall> getToolCalls() { 283 return toolCalls; 284 } 285 286 public void setToolCalls(List<ToolCall> toolCalls) { 287 this.toolCalls = toolCalls; 288 } 289 290 public String getFullReasoningContent() { 291 return fullReasoningContent; 292 } 293 294 public void setFullReasoningContent(String fullReasoningContent) { 295 this.fullReasoningContent = fullReasoningContent; 296 } 297 298 public int getEffectiveTotalTokens() { 299 if (this.totalTokens != null) return this.totalTokens; 300 if (this.promptTokens != null && this.completionTokens != null) { 301 return this.promptTokens + this.completionTokens; 302 } 303 if (this.localTotalTokens != null) return this.localTotalTokens; 304 if (this.localPromptTokens != null && this.localCompletionTokens != null) { 305 return this.localPromptTokens + this.localCompletionTokens; 306 } 307 return 0; 308 } 309 310 public Boolean getFinished() { 311 return finished; 312 } 313 314 public void setFinished(Boolean finished) { 315 this.finished = finished; 316 } 317 318 319 /** 320 * 判断当前对象是否为最终的 delta 对象。 321 * 322 * @return true 表示当前对象为最终的 delta 对象,否则为 false 323 */ 324 public boolean isFinalDelta() { 325 return (finished != null && finished); 326 } 327 328 public boolean hasFinishOrStopReason() { 329 return StringUtil.hasText(this.finishReason) 330 || StringUtil.hasText(this.stopReason); 331 } 332 333 334 @Override 335 public String toString() { 336 return "AiMessage{" + 337 "index=" + index + 338 ", promptTokens=" + promptTokens + 339 ", completionTokens=" + completionTokens + 340 ", totalTokens=" + totalTokens + 341 ", localPromptTokens=" + localPromptTokens + 342 ", localCompletionTokens=" + localCompletionTokens + 343 ", localTotalTokens=" + localTotalTokens + 344 ", reasoningContent='" + reasoningContent + '\'' + 345 ", toolCalls=" + toolCalls + 346 ", fullContent='" + fullContent + '\'' + 347 ", fullReasoningContent='" + fullReasoningContent + '\'' + 348 ", finishReason='" + finishReason + '\'' + 349 ", stopReason='" + stopReason + '\'' + 350 ", finished=" + finished + 351 ", content='" + content + '\'' + 352 ", metadataMap=" + metadataMap + 353 '}'; 354 } 355}