首次提交
This commit is contained in:
commit
3438971d10
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target/
|
||||
/.idea/
|
||||
*.iml
|
||||
30
README.md
Normal file
30
README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Agent Foundation
|
||||
|
||||
Java Spring Boot base platform for multiple enterprise agents (CRM assistant, knowledge-base assistant, customer service assistant).
|
||||
|
||||
## Core API
|
||||
- POST `/v1/chat/completions`
|
||||
- GET `/v1/healthz`
|
||||
|
||||
## WeCom CRM Bot Integration
|
||||
- Callback URL: `/v1/integrations/wecom/callback`
|
||||
- Receive enterprise WeCom text messages and route to `crm-assistant`
|
||||
- Send the model response back to the same WeCom user
|
||||
|
||||
### Required env vars
|
||||
- `DEEPSEEK_API_KEY`
|
||||
- `WECOM_CORP_ID`
|
||||
- `WECOM_AGENT_ID`
|
||||
- `WECOM_SECRET`
|
||||
- `WECOM_TOKEN`
|
||||
|
||||
### WeCom backend settings
|
||||
1. Create a WeCom self-built app.
|
||||
2. Enable message receive API and set callback URL to:
|
||||
- `https://your-domain/v1/integrations/wecom/callback`
|
||||
3. Set callback Token equal to `WECOM_TOKEN`.
|
||||
4. Make sure app has send-message permission to internal users.
|
||||
|
||||
## Run
|
||||
1. Set env vars.
|
||||
2. Start app from IDE or `mvn spring-boot:run`.
|
||||
23
pom.xml
Normal file
23
pom.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<groupId>com.company</groupId>
|
||||
<artifactId>agent-foundation</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<properties><java.version>17</java.version></properties>
|
||||
<dependencies>
|
||||
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
|
||||
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency>
|
||||
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
|
||||
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
|
||||
<dependency><groupId>com.fasterxml.jackson.dataformat</groupId><artifactId>jackson-dataformat-xml</artifactId></dependency>
|
||||
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
|
||||
</dependencies>
|
||||
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
|
||||
</project>
|
||||
@ -0,0 +1,7 @@
|
||||
package com.company.agentfoundation;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@SpringBootApplication
|
||||
public class AgentFoundationApplication {
|
||||
public static void main(String[] args) { SpringApplication.run(AgentFoundationApplication.class, args); }
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.company.agentfoundation.api;
|
||||
import java.util.List;
|
||||
public record DeepseekChatRequest(String model, List<Message> messages, Double temperature, Boolean stream) {
|
||||
public record Message(String role, String content) {}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.company.agentfoundation.api;
|
||||
import java.util.List;
|
||||
public record DeepseekChatResponse(List<Choice> choices, Usage usage) {
|
||||
public record Choice(Message message) {}
|
||||
public record Message(String role, String content) {}
|
||||
public record Usage(Integer prompt_tokens, Integer completion_tokens, Integer total_tokens) {}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.company.agentfoundation.api;
|
||||
import com.company.agentfoundation.config.FoundationProperties;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.util.retry.Retry;
|
||||
import java.time.Duration;
|
||||
@Component
|
||||
public class DeepseekClient {
|
||||
private final FoundationProperties properties;
|
||||
private final WebClient webClient;
|
||||
public DeepseekClient(FoundationProperties properties, WebClient.Builder builder) {
|
||||
this.properties = properties;
|
||||
this.webClient = builder.baseUrl(properties.getDeepseek().getBaseUrl()).defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).build();
|
||||
}
|
||||
public DeepseekChatResponse chat(DeepseekChatRequest request) {
|
||||
if (properties.getDeepseek().getApiKey() == null || properties.getDeepseek().getApiKey().isBlank()) throw new IllegalStateException("DEEPSEEK_API_KEY is missing");
|
||||
return webClient.post()
|
||||
.uri(properties.getDeepseek().getChatPath())
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + properties.getDeepseek().getApiKey())
|
||||
.bodyValue(request)
|
||||
.retrieve()
|
||||
.bodyToMono(DeepseekChatResponse.class)
|
||||
.retryWhen(Retry.backoff(properties.getDeepseek().getMaxRetries(), Duration.ofMillis(300)))
|
||||
.block(Duration.ofMillis(properties.getDeepseek().getReadTimeoutMs()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.company.agentfoundation.config;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(FoundationProperties.class)
|
||||
public class FoundationConfig {}
|
||||
@ -0,0 +1,78 @@
|
||||
package com.company.agentfoundation.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@ConfigurationProperties(prefix = "foundation")
|
||||
public class FoundationProperties {
|
||||
private final Auth auth = new Auth();
|
||||
private final ModelRouter modelRouter = new ModelRouter();
|
||||
private final Deepseek deepseek = new Deepseek();
|
||||
private final Wecom wecom = new Wecom();
|
||||
|
||||
public Auth getAuth() { return auth; }
|
||||
public ModelRouter getModelRouter() { return modelRouter; }
|
||||
public Deepseek getDeepseek() { return deepseek; }
|
||||
public Wecom getWecom() { return wecom; }
|
||||
|
||||
public static class Auth {
|
||||
private Map<String, String> keys = new HashMap<>();
|
||||
public Map<String, String> getKeys() { return keys; }
|
||||
public void setKeys(Map<String, String> keys) { this.keys = keys; }
|
||||
}
|
||||
|
||||
public static class ModelRouter {
|
||||
private String defaultModel = "deepseek-chat";
|
||||
private Map<String, String> routeByAgent = new HashMap<>();
|
||||
public String getDefaultModel() { return defaultModel; }
|
||||
public void setDefaultModel(String defaultModel) { this.defaultModel = defaultModel; }
|
||||
public Map<String, String> getRouteByAgent() { return routeByAgent; }
|
||||
public void setRouteByAgent(Map<String, String> routeByAgent) { this.routeByAgent = routeByAgent; }
|
||||
}
|
||||
|
||||
public static class Deepseek {
|
||||
private String baseUrl;
|
||||
private String chatPath;
|
||||
private String apiKey;
|
||||
private int readTimeoutMs = 60000;
|
||||
private int maxRetries = 2;
|
||||
|
||||
public String getBaseUrl() { return baseUrl; }
|
||||
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
|
||||
public String getChatPath() { return chatPath; }
|
||||
public void setChatPath(String chatPath) { this.chatPath = chatPath; }
|
||||
public String getApiKey() { return apiKey; }
|
||||
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
|
||||
public int getReadTimeoutMs() { return readTimeoutMs; }
|
||||
public void setReadTimeoutMs(int readTimeoutMs) { this.readTimeoutMs = readTimeoutMs; }
|
||||
public int getMaxRetries() { return maxRetries; }
|
||||
public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; }
|
||||
}
|
||||
|
||||
public static class Wecom {
|
||||
private boolean enabled;
|
||||
private String corpId;
|
||||
private Integer agentId;
|
||||
private String secret;
|
||||
private String token;
|
||||
private String crmAgentId = "crm-assistant";
|
||||
private String callbackPath = "/v1/integrations/wecom/callback";
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
public String getCorpId() { return corpId; }
|
||||
public void setCorpId(String corpId) { this.corpId = corpId; }
|
||||
public Integer getAgentId() { return agentId; }
|
||||
public void setAgentId(Integer agentId) { this.agentId = agentId; }
|
||||
public String getSecret() { return secret; }
|
||||
public void setSecret(String secret) { this.secret = secret; }
|
||||
public String getToken() { return token; }
|
||||
public void setToken(String token) { this.token = token; }
|
||||
public String getCrmAgentId() { return crmAgentId; }
|
||||
public void setCrmAgentId(String crmAgentId) { this.crmAgentId = crmAgentId; }
|
||||
public String getCallbackPath() { return callbackPath; }
|
||||
public void setCallbackPath(String callbackPath) { this.callbackPath = callbackPath; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.company.agentfoundation.controller;
|
||||
import com.company.agentfoundation.domain.ChatRequest;
|
||||
import com.company.agentfoundation.domain.ChatResponse;
|
||||
import com.company.agentfoundation.security.AgentAuthService;
|
||||
import com.company.agentfoundation.security.AgentContext;
|
||||
import com.company.agentfoundation.service.ChatService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@RestController
|
||||
@RequestMapping("/v1")
|
||||
public class ChatController {
|
||||
private final AgentAuthService authService;
|
||||
private final ChatService chatService;
|
||||
public ChatController(AgentAuthService authService, ChatService chatService) { this.authService = authService; this.chatService = chatService; }
|
||||
@PostMapping("/chat/completions")
|
||||
public ResponseEntity<ChatResponse> chat(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey, @Valid @RequestBody ChatRequest request) {
|
||||
AgentContext context = authService.authenticate(agentId, internalKey);
|
||||
return ResponseEntity.ok(chatService.chat(context, request));
|
||||
}
|
||||
@GetMapping("/healthz")
|
||||
public ResponseEntity<String> healthz() { return ResponseEntity.ok("ok"); }
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package com.company.agentfoundation.controller;
|
||||
|
||||
import com.company.agentfoundation.security.UnauthorizedException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(UnauthorizedException.class)
|
||||
public ResponseEntity<Map<String, Object>> unauthorized(UnauthorizedException ex) {
|
||||
return build(HttpStatus.UNAUTHORIZED, ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<Map<String, Object>> badRequest(IllegalArgumentException ex) {
|
||||
return build(HttpStatus.BAD_REQUEST, ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<Map<String, Object>> generic(Exception ex) {
|
||||
return build(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> build(HttpStatus status, String message) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("timestamp", Instant.now());
|
||||
body.put("status", status.value());
|
||||
body.put("error", status.getReasonPhrase());
|
||||
body.put("message", message);
|
||||
return ResponseEntity.status(status).body(body);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.company.agentfoundation.domain;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import java.util.List;
|
||||
public record ChatRequest(String model, @NotEmpty List<Message> messages, Double temperature) {
|
||||
public record Message(@NotBlank String role, @NotBlank String content) {}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.company.agentfoundation.domain;
|
||||
import java.time.Instant;
|
||||
public record ChatResponse(String traceId, String agentId, String model, String content, Usage usage, Instant timestamp) {
|
||||
public record Usage(Integer promptTokens, Integer completionTokens, Integer totalTokens) {}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
|
||||
public record WecomApiResponse(Integer errcode, String errmsg) {
|
||||
public boolean success() {
|
||||
return errcode != null && errcode == 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
|
||||
import com.company.agentfoundation.config.FoundationProperties;
|
||||
import com.company.agentfoundation.domain.ChatRequest;
|
||||
import com.company.agentfoundation.domain.ChatResponse;
|
||||
import com.company.agentfoundation.security.AgentContext;
|
||||
import com.company.agentfoundation.service.ChatService;
|
||||
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v1/integrations/wecom")
|
||||
public class WecomCallbackController {
|
||||
private final FoundationProperties properties;
|
||||
private final ChatService chatService;
|
||||
private final WecomClient wecomClient;
|
||||
private final XmlMapper xmlMapper = new XmlMapper();
|
||||
|
||||
public WecomCallbackController(FoundationProperties properties, ChatService chatService, WecomClient wecomClient) {
|
||||
this.properties = properties;
|
||||
this.chatService = chatService;
|
||||
this.wecomClient = wecomClient;
|
||||
}
|
||||
|
||||
@GetMapping("/callback")
|
||||
public String verify(@RequestParam("msg_signature") String signature,
|
||||
@RequestParam("timestamp") String timestamp,
|
||||
@RequestParam("nonce") String nonce,
|
||||
@RequestParam("echostr") String echostr) {
|
||||
if (!validSignature(signature, timestamp, nonce)) {
|
||||
throw new IllegalArgumentException("Invalid WeCom signature");
|
||||
}
|
||||
return echostr;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/callback", consumes = {MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_XML_VALUE})
|
||||
public String receive(@RequestParam("msg_signature") String signature,
|
||||
@RequestParam("timestamp") String timestamp,
|
||||
@RequestParam("nonce") String nonce,
|
||||
@RequestBody String body) throws Exception {
|
||||
if (!validSignature(signature, timestamp, nonce)) {
|
||||
throw new IllegalArgumentException("Invalid WeCom signature");
|
||||
}
|
||||
|
||||
WecomIncomingMessage msg = xmlMapper.readValue(body, WecomIncomingMessage.class);
|
||||
if (msg == null || msg.getMsgType() == null || !"text".equalsIgnoreCase(msg.getMsgType())) {
|
||||
return "success";
|
||||
}
|
||||
|
||||
String userText = msg.getContent() == null ? "" : msg.getContent().trim();
|
||||
if (userText.isBlank()) {
|
||||
return "success";
|
||||
}
|
||||
|
||||
ChatRequest req = new ChatRequest(
|
||||
null,
|
||||
List.of(
|
||||
new ChatRequest.Message("system", "你是企业CRM智能助手。请给出简洁、可执行的客户跟进建议。"),
|
||||
new ChatRequest.Message("user", userText)
|
||||
),
|
||||
0.3
|
||||
);
|
||||
|
||||
ChatResponse chat = chatService.chat(new AgentContext(properties.getWecom().getCrmAgentId()), req);
|
||||
String reply = (chat.content() == null || chat.content().isBlank()) ? "我已收到你的请求,但暂时无法生成建议。" : chat.content();
|
||||
wecomClient.sendText(msg.getFromUserName(), reply);
|
||||
|
||||
return "success";
|
||||
}
|
||||
|
||||
private boolean validSignature(String signature, String timestamp, String nonce) {
|
||||
String token = properties.getWecom().getToken();
|
||||
if (token == null || token.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] arr = new String[]{token, timestamp, nonce};
|
||||
Arrays.sort(arr);
|
||||
String raw = String.join("", arr);
|
||||
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
byte[] sha1 = digest.digest(raw.getBytes(StandardCharsets.UTF_8));
|
||||
String localSignature = HexFormat.of().formatHex(sha1);
|
||||
return signature.equalsIgnoreCase(localSignature);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
|
||||
import com.company.agentfoundation.config.FoundationProperties;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Service
|
||||
public class WecomClient {
|
||||
private final FoundationProperties properties;
|
||||
private final WebClient webClient;
|
||||
|
||||
private volatile String cachedToken;
|
||||
private volatile Instant tokenExpireAt = Instant.EPOCH;
|
||||
|
||||
public WecomClient(FoundationProperties properties, WebClient.Builder builder) {
|
||||
this.properties = properties;
|
||||
this.webClient = builder.baseUrl("https://qyapi.weixin.qq.com").build();
|
||||
}
|
||||
|
||||
public void sendText(String toUser, String content) {
|
||||
String token = getAccessToken();
|
||||
WecomSendMessageRequest req = new WecomSendMessageRequest();
|
||||
req.setToUser(toUser);
|
||||
req.setAgentId(properties.getWecom().getAgentId());
|
||||
WecomSendMessageRequest.Text text = new WecomSendMessageRequest.Text();
|
||||
text.setContent(content);
|
||||
req.setText(text);
|
||||
|
||||
WecomApiResponse response = webClient.post()
|
||||
.uri(uriBuilder -> uriBuilder.path("/cgi-bin/message/send").queryParam("access_token", token).build())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(req)
|
||||
.retrieve()
|
||||
.bodyToMono(WecomApiResponse.class)
|
||||
.block();
|
||||
|
||||
if (response == null || !response.success()) {
|
||||
throw new IllegalStateException("WeCom send failed: " + (response == null ? "null" : response.errmsg()));
|
||||
}
|
||||
}
|
||||
|
||||
private String getAccessToken() {
|
||||
if (cachedToken != null && Instant.now().isBefore(tokenExpireAt)) {
|
||||
return cachedToken;
|
||||
}
|
||||
|
||||
WecomTokenResponse response = webClient.get()
|
||||
.uri(uriBuilder -> uriBuilder.path("/cgi-bin/gettoken")
|
||||
.queryParam("corpid", properties.getWecom().getCorpId())
|
||||
.queryParam("corpsecret", properties.getWecom().getSecret())
|
||||
.build())
|
||||
.retrieve()
|
||||
.bodyToMono(WecomTokenResponse.class)
|
||||
.block();
|
||||
|
||||
if (response == null || !response.success() || response.accessToken() == null) {
|
||||
throw new IllegalStateException("WeCom gettoken failed: " + (response == null ? "null" : response.errmsg()));
|
||||
}
|
||||
|
||||
cachedToken = response.accessToken();
|
||||
int expires = response.expiresIn() == null ? 7200 : response.expiresIn();
|
||||
tokenExpireAt = Instant.now().plusSeconds(Math.max(300, expires - 120));
|
||||
return cachedToken;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
|
||||
@JacksonXmlRootElement(localName = "xml")
|
||||
public class WecomIncomingMessage {
|
||||
@JacksonXmlProperty(localName = "ToUserName")
|
||||
private String toUserName;
|
||||
@JacksonXmlProperty(localName = "FromUserName")
|
||||
private String fromUserName;
|
||||
@JacksonXmlProperty(localName = "MsgType")
|
||||
private String msgType;
|
||||
@JacksonXmlProperty(localName = "Content")
|
||||
private String content;
|
||||
|
||||
public String getToUserName() { return toUserName; }
|
||||
public void setToUserName(String toUserName) { this.toUserName = toUserName; }
|
||||
public String getFromUserName() { return fromUserName; }
|
||||
public void setFromUserName(String fromUserName) { this.fromUserName = fromUserName; }
|
||||
public String getMsgType() { return msgType; }
|
||||
public void setMsgType(String msgType) { this.msgType = msgType; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WecomSendMessageRequest {
|
||||
@JsonProperty("touser")
|
||||
private String toUser;
|
||||
@JsonProperty("msgtype")
|
||||
private String msgType = "text";
|
||||
@JsonProperty("agentid")
|
||||
private Integer agentId;
|
||||
private Text text;
|
||||
@JsonProperty("safe")
|
||||
private Integer safe = 0;
|
||||
|
||||
public String getToUser() { return toUser; }
|
||||
public void setToUser(String toUser) { this.toUser = toUser; }
|
||||
public String getMsgType() { return msgType; }
|
||||
public void setMsgType(String msgType) { this.msgType = msgType; }
|
||||
public Integer getAgentId() { return agentId; }
|
||||
public void setAgentId(Integer agentId) { this.agentId = agentId; }
|
||||
public Text getText() { return text; }
|
||||
public void setText(Text text) { this.text = text; }
|
||||
public Integer getSafe() { return safe; }
|
||||
public void setSafe(Integer safe) { this.safe = safe; }
|
||||
|
||||
public static class Text {
|
||||
private String content;
|
||||
@JsonProperty("mentioned_list")
|
||||
private List<String> mentionedList = List.of();
|
||||
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public List<String> getMentionedList() { return mentionedList; }
|
||||
public void setMentionedList(List<String> mentionedList) { this.mentionedList = mentionedList; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public record WecomTokenResponse(
|
||||
@JsonProperty("access_token") String accessToken,
|
||||
@JsonProperty("expires_in") Integer expiresIn,
|
||||
Integer errcode,
|
||||
String errmsg
|
||||
) {
|
||||
public boolean success() {
|
||||
return errcode == null || errcode == 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.company.agentfoundation.security;
|
||||
import com.company.agentfoundation.config.FoundationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
public class AgentAuthService {
|
||||
private final FoundationProperties properties;
|
||||
public AgentAuthService(FoundationProperties properties) { this.properties = properties; }
|
||||
public AgentContext authenticate(String agentId, String internalKey) {
|
||||
String expected = properties.getAuth().getKeys().get(agentId);
|
||||
if (expected == null || !expected.equals(internalKey)) throw new UnauthorizedException("Invalid agent credentials");
|
||||
return new AgentContext(agentId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
package com.company.agentfoundation.security;
|
||||
public record AgentContext(String agentId) {}
|
||||
@ -0,0 +1,4 @@
|
||||
package com.company.agentfoundation.security;
|
||||
public class UnauthorizedException extends RuntimeException {
|
||||
public UnauthorizedException(String message) { super(message); }
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package com.company.agentfoundation.service;
|
||||
import com.company.agentfoundation.api.DeepseekChatRequest;
|
||||
import com.company.agentfoundation.api.DeepseekChatResponse;
|
||||
import com.company.agentfoundation.api.DeepseekClient;
|
||||
import com.company.agentfoundation.domain.ChatRequest;
|
||||
import com.company.agentfoundation.domain.ChatResponse;
|
||||
import com.company.agentfoundation.security.AgentContext;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
@Service
|
||||
public class ChatService {
|
||||
private final DeepseekClient deepseekClient;
|
||||
private final ModelRouter modelRouter;
|
||||
public ChatService(DeepseekClient deepseekClient, ModelRouter modelRouter) { this.deepseekClient = deepseekClient; this.modelRouter = modelRouter; }
|
||||
public ChatResponse chat(AgentContext context, ChatRequest request) {
|
||||
String traceId = UUID.randomUUID().toString();
|
||||
String model = modelRouter.routeModel(context.agentId(), request.model());
|
||||
DeepseekChatResponse resp = deepseekClient.chat(new DeepseekChatRequest(
|
||||
model,
|
||||
request.messages().stream().map(m -> new DeepseekChatRequest.Message(m.role(), m.content())).toList(),
|
||||
request.temperature(),
|
||||
false));
|
||||
String content = "";
|
||||
if (resp != null && resp.choices() != null && !resp.choices().isEmpty() && resp.choices().get(0).message() != null) content = resp.choices().get(0).message().content();
|
||||
ChatResponse.Usage usage = null;
|
||||
if (resp != null && resp.usage() != null) usage = new ChatResponse.Usage(resp.usage().prompt_tokens(), resp.usage().completion_tokens(), resp.usage().total_tokens());
|
||||
return new ChatResponse(traceId, context.agentId(), model, content, usage, Instant.now());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.company.agentfoundation.service;
|
||||
import com.company.agentfoundation.config.FoundationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
public class ModelRouter {
|
||||
private final FoundationProperties properties;
|
||||
public ModelRouter(FoundationProperties properties) { this.properties = properties; }
|
||||
public String routeModel(String agentId, String requestedModel) {
|
||||
if (requestedModel != null && !requestedModel.isBlank()) return requestedModel;
|
||||
return properties.getModelRouter().getRouteByAgent().getOrDefault(agentId, properties.getModelRouter().getDefaultModel());
|
||||
}
|
||||
}
|
||||
36
src/main/resources/application.yml
Normal file
36
src/main/resources/application.yml
Normal file
@ -0,0 +1,36 @@
|
||||
server:
|
||||
port: 8080
|
||||
spring:
|
||||
application:
|
||||
name: agent-foundation
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
foundation:
|
||||
auth:
|
||||
keys:
|
||||
crm-assistant: crm-dev-key
|
||||
kb-assistant: kb-dev-key
|
||||
cs-assistant: cs-dev-key
|
||||
model-router:
|
||||
default-model: deepseek-chat
|
||||
route-by-agent:
|
||||
crm-assistant: deepseek-chat
|
||||
kb-assistant: deepseek-chat
|
||||
cs-assistant: deepseek-chat
|
||||
deepseek:
|
||||
base-url: https://api.deepseek.com
|
||||
chat-path: /chat/completions
|
||||
api-key: ${DEEPSEEK_API_KEY:}
|
||||
read-timeout-ms: 60000
|
||||
max-retries: 2
|
||||
wecom:
|
||||
enabled: true
|
||||
corp-id: ${WECOM_CORP_ID:}
|
||||
agent-id: ${WECOM_AGENT_ID:1000002}
|
||||
secret: ${WECOM_SECRET:}
|
||||
token: ${WECOM_TOKEN:}
|
||||
crm-agent-id: crm-assistant
|
||||
callback-path: /v1/integrations/wecom/callback
|
||||
Loading…
x
Reference in New Issue
Block a user