EventLogChatMemory.java
6.53 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
package com.xly.agent;
import com.xly.service.EventProjectionService;
import com.xly.service.LedgerService;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.ToolExecutionResultMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.ChatMemory;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 事件日志上的对话记忆:{@link #add} 把模型环消息逐条 append 成事件(rightPush 原子——
* 对话流与确认端点并发写互不覆盖,替代旧 chat:mem 整包读改写),{@link #messages()} 读取
* {@link EventProjectionService} 的四段式投影。日志即唯一事实源,本类不持有任何会话状态。
*
* <p>写者分工:用户事件由控制器在收到请求时先落账(前端立即可见),本类对 UserMessage 做
* 去重跳过;{@code internalUserTurn=true} 时(反编造护栏重试的注入话术)例外——落一条
* {@code internal} 标记的用户事件,LLM 可见、前端历史不显示。模型消息(tool_call/tool_result/ai)
* 只由本类落账。
*/
public class EventLogChatMemory implements ChatMemory {
private static final int LLM_EVENT_WINDOW = 400;
private final String convId;
private final LedgerService log;
private final EventProjectionService projection;
private final int charBudget;
private final boolean internalUserTurn;
/** system prompt 每次由 provider 提供,不落日志(保持日志纯业务事件)。 */
private volatile String systemText;
/**
* 本轮实际喂给模型的用户文本(原话 + 编排层附加的 grounding/状态后缀)。日志只存原话;
* 投影时把当前轮用户消息替换为它。P2 编排层不再加后缀后,此字段恒空。
*/
private volatile String currentTurnUserText;
public EventLogChatMemory(String convId, LedgerService log, EventProjectionService projection,
int charBudget, boolean internalUserTurn) {
this.convId = convId;
this.log = log;
this.projection = projection;
this.charBudget = charBudget;
this.internalUserTurn = internalUserTurn;
}
@Override
public Object id() {
return convId;
}
@Override
public void add(ChatMessage m) {
if (m instanceof SystemMessage sm) {
systemText = sm.text();
return;
}
if (m instanceof UserMessage um) {
String text = um.hasSingleText() ? um.singleText() : String.valueOf(um.contents());
if (!internalUserTurn && alreadyLoggedPrefixOf(text)) {
currentTurnUserText = text;
return;
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("text", text);
if (internalUserTurn) {
data.put("internal", true);
}
log.append(convId, "user", data);
return;
}
if (m instanceof AiMessage am) {
if (am.hasToolExecutionRequests()) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("payload", ChatMessageSerializer.messagesToJson(List.of(am)));
data.put("text", am.text() == null ? "" : am.text());
List<String> tools = new ArrayList<>();
for (ToolExecutionRequest r : am.toolExecutionRequests()) {
tools.add(r.name());
}
data.put("tools", tools);
log.append(convId, "tool_call", data);
} else if (am.text() != null && !am.text().isBlank()) {
log.append(convId, "ai", Map.of("text", am.text()));
}
return;
}
if (m instanceof ToolExecutionResultMessage tr) {
String text = tr.text() == null ? "" : tr.text();
Map<String, Object> data = new LinkedHashMap<>();
data.put("tcId", tr.id() == null ? "" : tr.id());
data.put("name", tr.toolName() == null ? "" : tr.toolName());
data.put("text", text);
data.put("digest", digest(text));
log.append(convId, "tool_result", data);
}
}
/**
* 控制器已把本轮用户**原话**落账(最近的用户事件是喂给模型文本的前缀、且其后无模型事件)
* → 跳过,避免重复。编排层可能在原话后附加 grounding/状态后缀,故用前缀而非全等判定。
*/
private boolean alreadyLoggedPrefixOf(String text) {
List<Map<String, Object>> tail = log.events(convId, 6);
for (int i = tail.size() - 1; i >= 0; i--) {
String type = String.valueOf(tail.get(i).get("type"));
switch (type) {
case "user":
String logged = String.valueOf(tail.get(i).get("text"));
return !logged.isEmpty() && text.startsWith(logged);
case "form_submit", "ai", "tool_call", "tool_result", "assistant":
return false;
default:
// confirm/proposal 等按钮事件可能插在中间,继续往前找
}
}
return false;
}
@Override
public List<ChatMessage> messages() {
List<ChatMessage> out = projection.project(
systemText, log.events(convId, LLM_EVENT_WINDOW), charBudget);
String full = currentTurnUserText;
if (full != null) {
// 当前轮用户消息换成实际喂给模型的完整文本(原话+编排后缀)
for (int i = out.size() - 1; i >= 0; i--) {
if (out.get(i) instanceof UserMessage u && u.hasSingleText()
&& full.startsWith(u.singleText())) {
out.set(i, UserMessage.from(full));
break;
}
}
}
return out;
}
/** 日志是唯一事实源,不因模型环异常清史;删除会话走 ConversationService 级联。 */
@Override
public void clear() {
}
private static String digest(String text) {
String t = text.replace('\n', ' ').trim();
return t.length() > EventProjectionService.TOOL_DIGEST_LEN
? t.substring(0, EventProjectionService.TOOL_DIGEST_LEN) + "…" : t;
}
}