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