AgentChatController.java
5.43 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
package com.xly.web;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xly.agent.ReActAgent;
import com.xly.service.ConversationService;
import com.xly.service.OpService;
import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.tool.ToolExecution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 单 agent 对话入口。{@code POST /xlyAi/api/agent/chat} 以 SSE 流式返回。
*
* <p>帧格式:{@code {"type":"token|reset|done|error"}} 或写提议 {@code {"type":"write_proposal","opId","summary"}}。
* {@code reset} 在工具执行时清掉模型的调用旁白;{@code write_proposal} 让前端渲染确认卡片
* (确认走确定性端点 {@code /api/agent/op/{id}/confirm},不经过 LLM)。
*/
@RestController
@RequestMapping("/api/agent")
public class AgentChatController {
private static final Logger log = LoggerFactory.getLogger(AgentChatController.class);
private final ReActAgent agent;
private final ObjectMapper mapper;
private final ConversationService conversations;
private final OpService ops;
private final ExecutorService exec = Executors.newCachedThreadPool();
public AgentChatController(ReActAgent agent, ObjectMapper mapper,
ConversationService conversations, OpService ops) {
this.agent = agent;
this.mapper = mapper;
this.conversations = conversations;
this.ops = ops;
}
public static class ChatReq {
public String text;
public String userid;
public String conversationId;
}
@PostMapping(value = "/chat", produces = "text/event-stream;charset=UTF-8")
public SseEmitter chat(@RequestBody ChatReq req) {
SseEmitter emitter = new SseEmitter(180_000L);
final String userInput = req.text == null ? "" : req.text;
final String convId = (req.conversationId != null && !req.conversationId.isBlank())
? req.conversationId
: ((req.userid == null ? "anon" : req.userid) + ":default");
conversations.touch(req.userid == null ? "anon" : req.userid, convId, userInput);
exec.submit(() -> {
try {
TokenStream ts = agent.chat(convId, userInput);
ts.onPartialResponse(token -> send(emitter, "token", token))
.onToolExecuted(te -> handleToolExecuted(emitter, convId, te))
.onCompleteResponse(resp -> {
send(emitter, "done", "");
emitter.complete();
})
.onError(err -> {
log.warn("agent chat stream error (conv={})", convId, err);
send(emitter, "error", err.getMessage() == null ? "服务异常" : err.getMessage());
emitter.complete();
})
.start();
} catch (Exception e) {
log.error("agent invoke failed (conv={})", convId, e);
send(emitter, "error", "服务异常:" + e.getMessage());
emitter.complete();
}
});
return emitter;
}
/** 工具执行回调:清掉工具前的旁白(reset);若是写提议,关联会话并推确认卡片。 */
private void handleToolExecuted(SseEmitter emitter, String convId, ToolExecution te) {
send(emitter, "reset", "");
try {
String toolName = te.request() == null ? "" : te.request().name();
if (("proposeUpdate".equals(toolName) || "proposeDelete".equals(toolName)
|| "proposeCreate".equals(toolName)) && te.result() != null) {
JsonNode r = mapper.readTree(te.result());
String opId = r.path("opId").asText(null);
if (opId != null && !opId.isBlank()) {
ops.attachConversation(opId, convId);
Map<String, Object> card = new LinkedHashMap<>();
card.put("type", "write_proposal");
card.put("opId", opId);
card.put("summary", r.path("summary").asText(""));
sendEvent(emitter, card);
}
}
} catch (Exception e) {
log.warn("handle proposeUpdate result failed", e);
}
}
private void send(SseEmitter emitter, String type, String content) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("type", type);
m.put("content", content);
sendEvent(emitter, m);
}
private void sendEvent(SseEmitter emitter, Map<String, Object> m) {
try {
emitter.send(SseEmitter.event().data(mapper.writeValueAsString(m), MediaType.APPLICATION_JSON));
} catch (IOException | IllegalStateException e) {
// 客户端已断开或 emitter 已结束——忽略。
}
}
}