Browse Source
- 添加 CopilotKitController 实现 SSE 流式响应 - 重构 SysEntityController 和 DynamicDataController 移除 creatorId 过滤 - 新增 AgentResponse 和 Result 统一响应结构 - 扩展 CrmAgent 支持同步和流式聊天接口 - 更新 application.yml 添加 streaming-chat-model 配置 - 完善 CrmTools 工具类增加更新和查询功能main
11 changed files with 331 additions and 29 deletions
@ -0,0 +1,22 @@ |
|||||||
|
package com.example.ainative.ai; |
||||||
|
|
||||||
|
import lombok.AllArgsConstructor; |
||||||
|
import lombok.Builder; |
||||||
|
import lombok.Data; |
||||||
|
import lombok.NoArgsConstructor; |
||||||
|
|
||||||
|
@Data |
||||||
|
@AllArgsConstructor |
||||||
|
@NoArgsConstructor |
||||||
|
@Builder |
||||||
|
public class AgentResponse { |
||||||
|
/** |
||||||
|
* AI 生成的对话内容 |
||||||
|
*/ |
||||||
|
private String content; |
||||||
|
|
||||||
|
/** |
||||||
|
* 业务状态标识 (比如: SUCCESS, NEED_INFO, ERROR) |
||||||
|
*/ |
||||||
|
private String status; |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
package com.example.ainative.common; |
||||||
|
|
||||||
|
import lombok.AllArgsConstructor; |
||||||
|
import lombok.Data; |
||||||
|
import lombok.NoArgsConstructor; |
||||||
|
|
||||||
|
@Data |
||||||
|
@AllArgsConstructor |
||||||
|
@NoArgsConstructor |
||||||
|
public class Result<T> { |
||||||
|
private Integer code; |
||||||
|
private String msg; |
||||||
|
private T data; |
||||||
|
|
||||||
|
public static <T> Result<T> success(T data) { |
||||||
|
return new Result<>(200, "success", data); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> Result<T> error(String msg) { |
||||||
|
return new Result<>(500, msg, null); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,171 @@ |
|||||||
|
package com.example.ainative.controller; |
||||||
|
|
||||||
|
import com.example.ainative.ai.CrmAgent; |
||||||
|
import com.example.ainative.config.UserContextHolder; |
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||||
|
import lombok.Data; |
||||||
|
import lombok.RequiredArgsConstructor; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.http.ResponseEntity; |
||||||
|
import org.springframework.web.bind.annotation.*; |
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; |
||||||
|
|
||||||
|
import java.util.LinkedHashMap; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.UUID; |
||||||
|
|
||||||
|
@RestController |
||||||
|
@RequestMapping("/api/copilotkit") |
||||||
|
@RequiredArgsConstructor |
||||||
|
@Slf4j |
||||||
|
@CrossOrigin(origins = "*") |
||||||
|
public class CopilotKitController { |
||||||
|
|
||||||
|
private final CrmAgent crmAgent; |
||||||
|
private static final ObjectMapper objectMapper = new ObjectMapper(); |
||||||
|
|
||||||
|
@GetMapping({"/info", ""}) |
||||||
|
public Map<String, Object> getInfo() { |
||||||
|
return Map.of( |
||||||
|
"actions", List.of(), |
||||||
|
"agents", Map.of( |
||||||
|
"default", Map.of("description", "Default Agent") |
||||||
|
) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@PostMapping(value = "", produces = MediaType.TEXT_EVENT_STREAM_VALUE) |
||||||
|
public ResponseEntity<Object> handleSingleRoute(@RequestBody CopilotSingleRouteRequest request) { |
||||||
|
if ("info".equals(request.getMethod())) { |
||||||
|
return ResponseEntity.ok() |
||||||
|
.contentType(MediaType.APPLICATION_JSON) |
||||||
|
.body(getInfo()); |
||||||
|
} |
||||||
|
|
||||||
|
log.info("CopilotKit single-route request: method={}", request.getMethod()); |
||||||
|
AgentRunInput input = request.getBody(); |
||||||
|
if (input == null) { |
||||||
|
input = new AgentRunInput(); |
||||||
|
} |
||||||
|
return buildStreamResponse(input); |
||||||
|
} |
||||||
|
|
||||||
|
@PostMapping(value = "/agent/{agentId}/run", produces = MediaType.TEXT_EVENT_STREAM_VALUE) |
||||||
|
public ResponseEntity<SseEmitter> handleRestRoute( |
||||||
|
@PathVariable String agentId, |
||||||
|
@RequestBody AgentRunInput input) { |
||||||
|
log.info("CopilotKit REST request: agentId={}", agentId); |
||||||
|
if (input == null) { |
||||||
|
input = new AgentRunInput(); |
||||||
|
} |
||||||
|
return buildStreamResponse(input); |
||||||
|
} |
||||||
|
|
||||||
|
private <T> ResponseEntity<T> buildStreamResponse(AgentRunInput input) { |
||||||
|
SseEmitter emitter = new SseEmitter(180_000L); |
||||||
|
|
||||||
|
String threadId = input.getThreadId() != null ? input.getThreadId() : UUID.randomUUID().toString(); |
||||||
|
String runId = input.getRunId() != null ? input.getRunId() : UUID.randomUUID().toString(); |
||||||
|
String messageId = UUID.randomUUID().toString(); |
||||||
|
|
||||||
|
List<Map<String, Object>> messages = input.getMessages(); |
||||||
|
if (messages == null || messages.isEmpty()) { |
||||||
|
sendEvent(emitter, aguiEvent("RUN_STARTED", "threadId", threadId, "runId", runId)); |
||||||
|
sendEvent(emitter, aguiEvent("RUN_FINISHED", "threadId", threadId, "runId", runId)); |
||||||
|
emitter.complete(); |
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
ResponseEntity<T> resp = (ResponseEntity<T>) ResponseEntity.ok() |
||||||
|
.contentType(MediaType.TEXT_EVENT_STREAM) |
||||||
|
.body(emitter); |
||||||
|
return resp; |
||||||
|
} |
||||||
|
|
||||||
|
String lastUserMessage = messages.stream() |
||||||
|
.filter(m -> "user".equals(m.get("role"))) |
||||||
|
.reduce((first, second) -> second) |
||||||
|
.map(m -> { |
||||||
|
Object content = m.get("content"); |
||||||
|
if (content instanceof String) { |
||||||
|
return (String) content; |
||||||
|
} |
||||||
|
if (content instanceof List<?> parts) { |
||||||
|
StringBuilder sb = new StringBuilder(); |
||||||
|
for (Object part : parts) { |
||||||
|
if (part instanceof Map<?, ?> partMap && "text".equals(partMap.get("type"))) { |
||||||
|
sb.append(partMap.get("text")); |
||||||
|
} |
||||||
|
} |
||||||
|
return sb.toString(); |
||||||
|
} |
||||||
|
return ""; |
||||||
|
}) |
||||||
|
.orElse(""); |
||||||
|
|
||||||
|
log.info("User message: {}", lastUserMessage); |
||||||
|
String currentUser = UserContextHolder.getCurrentUser(); |
||||||
|
|
||||||
|
sendEvent(emitter, aguiEvent("RUN_STARTED", "threadId", threadId, "runId", runId)); |
||||||
|
sendEvent(emitter, aguiEvent("TEXT_MESSAGE_START", "messageId", messageId, "role", "assistant")); |
||||||
|
|
||||||
|
crmAgent.chat("copilot-streaming-session", lastUserMessage, currentUser) |
||||||
|
.onPartialResponse(token -> { |
||||||
|
sendEvent(emitter, aguiEvent("TEXT_MESSAGE_CONTENT", "messageId", messageId, "delta", token)); |
||||||
|
}) |
||||||
|
.onCompleteResponse(response -> { |
||||||
|
sendEvent(emitter, aguiEvent("TEXT_MESSAGE_END", "messageId", messageId)); |
||||||
|
sendEvent(emitter, aguiEvent("RUN_FINISHED", "threadId", threadId, "runId", runId)); |
||||||
|
emitter.complete(); |
||||||
|
}) |
||||||
|
.onError(err -> { |
||||||
|
log.error("AI streaming error:", err); |
||||||
|
sendEvent(emitter, aguiEvent("TEXT_MESSAGE_END", "messageId", messageId)); |
||||||
|
sendEvent(emitter, aguiEvent("RUN_ERROR", "message", err.getMessage() != null ? err.getMessage() : "Unknown error")); |
||||||
|
sendEvent(emitter, aguiEvent("RUN_FINISHED", "threadId", threadId, "runId", runId)); |
||||||
|
emitter.completeWithError(err); |
||||||
|
}) |
||||||
|
.start(); |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
ResponseEntity<T> resp = (ResponseEntity<T>) ResponseEntity.ok() |
||||||
|
.contentType(MediaType.TEXT_EVENT_STREAM) |
||||||
|
.body(emitter); |
||||||
|
return resp; |
||||||
|
} |
||||||
|
|
||||||
|
private Map<String, Object> aguiEvent(String type, String... kvPairs) { |
||||||
|
Map<String, Object> event = new LinkedHashMap<>(); |
||||||
|
event.put("type", type); |
||||||
|
for (int i = 0; i < kvPairs.length; i += 2) { |
||||||
|
event.put(kvPairs[i], kvPairs[i + 1]); |
||||||
|
} |
||||||
|
return event; |
||||||
|
} |
||||||
|
|
||||||
|
private void sendEvent(SseEmitter emitter, Map<String, Object> data) { |
||||||
|
try { |
||||||
|
emitter.send(SseEmitter.event().data(data)); |
||||||
|
} catch (Exception e) { |
||||||
|
log.warn("Failed to send SSE event: {}", e.getMessage()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Data |
||||||
|
public static class CopilotSingleRouteRequest { |
||||||
|
private String method; |
||||||
|
private Map<String, Object> params; |
||||||
|
private AgentRunInput body; |
||||||
|
} |
||||||
|
|
||||||
|
@Data |
||||||
|
public static class AgentRunInput { |
||||||
|
private String threadId; |
||||||
|
private String runId; |
||||||
|
private List<Map<String, Object>> messages; |
||||||
|
private List<Map<String, Object>> context; |
||||||
|
private List<Map<String, Object>> tools; |
||||||
|
private Map<String, Object> forwardedProps; |
||||||
|
private Map<String, Object> state; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue