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}